Skip to main content

Overview

Workflow Update combines the capabilities of Signals and Queries in a single API call:
  • Bidirectional - Send data to workflow and receive results back
  • Validated - Workflow can reject updates before processing
  • Efficient - Rejected updates leave no trace in workflow history
  • Synchronous - Caller blocks until update is accepted or rejected
Workflow Update is a complex feature that acts as a proxy between the API caller and workflow code, enabling workflows to expose custom APIs.

Update vs Signal vs Query

Signal

Fire-and-forget
  • Cannot be rejected
  • Always writes events
  • No return value
  • Cannot know if processed

Query

Read-only
  • Cannot modify state
  • Returns data
  • No history events
  • Synchronous response

Update

Read-write with validation
  • Can be rejected
  • Only accepted updates write events
  • Returns data
  • Synchronous accept/reject

Key Requirement: No Persistence on Reject

Updates must be “cheap” when rejected:
  1. No “Update Received” event - Updates arrive as Messages, not Events
  2. No “Update Rejected” event - Rejected updates simply disappear
  3. First event is “Update Accepted” - Contains the original request payload
  4. Update outcome in “Update Completed” event - Success or failure result
  5. Speculative Workflow Task - Task to ship update is not persisted in mutable state
Update rejection must leave no traces in workflow history. This requirement drives much of the implementation complexity.

Update Registry

Updates are managed through the update.Registry interface:
  • Location: workflow.ContextImpl struct
  • Stores: Admitted and accepted updates (in-flight only)
  • Completed updates: Accessed through UpdateStore interface
  • Mutable state: Contains UpdateInfo map linking ID to acceptance/completion info

Update States

Update Lifecycle

1

Client sends update request

Request arrives at Frontend service via UpdateWorkflowExecution API.
resp, err := client.UpdateWorkflow(ctx, &workflowpb.UpdateWorkflowExecutionRequest{
    Namespace: "my-namespace",
    WorkflowExecution: &commonpb.WorkflowExecution{
        WorkflowId: "my-workflow",
        RunId: runId,
    },
    Request: &updatepb.Request{
        Meta: &updatepb.Meta{UpdateId: "update-1"},
        Input: &updatepb.Input{Name: "myUpdate", Args: args},
    },
})
2

Update admitted to registry

History service admits update to the registry. Update is now in-memory only.
  • No persistence write at this stage
  • Stored in update.Registry
  • Linked to workflow execution
3

Speculative workflow task created

A speculative (non-persisted) workflow task is created to ship the update to a worker.See Workflow Lifecycle for details.
4

Worker receives update via message

Update is shipped to worker as a Message (not an Event).Worker can:
  • Accept the update (validation passed)
  • Reject the update (validation failed)
5

Worker responds with acceptance or rejection

If accepted: WorkflowExecutionUpdateAcceptedEvent written to historyIf rejected: Update disappears with no history trace
6

Update handler executes (if accepted)

Workflow code executes the update handler.Handler can:
  • Complete successfully with result
  • Fail with error
7

Update completed

WorkflowExecutionUpdateCompletedEvent written with outcome (success or failure).Client receives the result or error.

Message Protocol

Updates use the Message Protocol instead of Events:
  • Messages - Arbitrary data shipped to worker outside history events
  • Commands - Must produce events, so cannot be used for updates
  • Update request - Shipped as Message to avoid events on rejection
  • Update outcome - Cannot use Commands (would force event creation)
The Message Protocol was introduced specifically to support Update’s “no event on reject” requirement.

Speculative Workflow Tasks

Updates rely on speculative workflow tasks:
  • Not persisted in mutable state
  • Transient - Exist only to ship the update
  • Converted to normal task if update is accepted
  • Discarded if update is rejected
Normal workflow tasks must be persisted before being sent to workers. If an update is rejected, we cannot have any persisted state. Speculative tasks are created in-memory only and are either promoted to normal tasks (on accept) or discarded (on reject).

Update Validation

Workflows can validate updates before accepting:
// Worker-side validation example
func (w *Workflow) validateUpdate(ctx workflow.Context, args UpdateArgs) error {
    // Validate inputs
    if args.Amount < 0 {
        return fmt.Errorf("amount must be positive")
    }
    
    // Validate state
    if w.isCompleted {
        return fmt.Errorf("workflow already completed")
    }
    
    return nil // Accept
}

func (w *Workflow) handleUpdate(ctx workflow.Context, args UpdateArgs) (UpdateResult, error) {
    // Update handler executes after validation passes
    w.balance += args.Amount
    return UpdateResult{NewBalance: w.balance}, nil
}

Update Events

WorkflowExecutionUpdateAcceptedEvent

Written when update is accepted:
updateId
string
Unique identifier for the update
request
UpdateRequest
Original update request including input payload
acceptedRequestMessageId
string
Message ID from the workflow task that accepted it

WorkflowExecutionUpdateCompletedEvent

Written when update completes:
updateId
string
Identifier matching the accepted event
outcome
Outcome
Success or failure result from update handler
meta
UpdateMeta
Metadata about the update execution

In-Memory Queue

Updates use the in-memory queue for speculative tasks:
  • Purpose: Schedule speculative workflow tasks without persistence
  • Location: service/history/queues/inmemory_queue.go
  • Behavior: Tasks exist only in memory until promoted or discarded
See Workflow Lifecycle for implementation details.

Effect Package

Update implementation uses the effect package for managing side effects:
  • Purpose: Defer and batch side effects until commit
  • Location: service/history/workflow/effect/
  • Usage: Ensures update acceptance/rejection is atomic
See Workflow Lifecycle for details on effect management.

Best Practices

Update validation should be quick since the client is blocked waiting.
  • Avoid expensive computations in validators
  • Don’t call activities from validators
  • Return rejection quickly if validation fails
Provide idempotent update IDs to safely retry:
  • Use deterministic IDs (e.g., based on request data)
  • Server deduplicates updates with same ID
  • Enables safe retry on client timeout
Update handlers can fail - design for this:
  • Return meaningful error messages
  • Distinguish validation failures from execution failures
  • Client receives failure in response
Updates are powerful but complex:
  • Use Signals if you don’t need return values
  • Use Queries if you don’t need to modify state
  • Updates add overhead - use judiciously

Monitoring

Key metrics for update operations:
  • update_admitted_total - Total updates admitted
  • update_accepted_total - Updates accepted by workflows
  • update_rejected_total - Updates rejected by workflows
  • update_completed_total - Updates completed successfully
  • update_failed_total - Updates that failed during execution
  • update_latency - Time from admission to completion

Limitations

  • No update on closed workflows - Cannot update completed/terminated workflows
  • Limited concurrent updates - Default limit of 10 in-flight updates per workflow
  • No update history introspection - Cannot list all updates sent to a workflow
  • Speculative task overhead - Each update creates a workflow task

See Also

Workflow Lifecycle

Understand speculative tasks and message protocol

Event Sourcing

Learn how update events are stored

Build docs developers (and LLMs) love