Request-Response via Updates 
Overview
Workflow Updates enable synchronous request-response interactions where clients receive immediate, typed responses while the Workflow continues processing. Updates modify Workflow state, validate inputs, and return results directly to the caller with strong consistency guarantees.
Problem
In distributed systems, you often need Workflows that provide immediate feedback to clients (validation results, confirmation IDs), require strong consistency guarantees for operations, need typed error handling for validation failures, should validate inputs before accepting work, and allow external systems to modify Workflow state synchronously.
Without Updates, clients must use Signals and poll via Queries (complex, eventually consistent), wait for entire Workflow completion (slow), implement complex coordination logic, and handle race conditions between Signals and Queries.
Solution
Temporal's Update API executes an Update handler that can validate inputs, modify state, and return values synchronously. The Update is recorded in Workflow history before returning, providing strong consistency.
The following describes each step in the diagram:
- The client sends an Update request to the Workflow.
- The Workflow validates the input. If validation fails, it returns a typed error response.
- If validation succeeds, the Workflow modifies its state and returns a typed response to the client.
Implementation
The following examples show a task assignment Workflow that accepts tasks via Updates with validation. The Update validator rejects requests when the task limit is reached, and the Update handler assigns the task and returns a result.
# workflows.py
import uuid
from dataclasses import dataclass
from temporalio import workflow
MAX_TASKS = 10
@dataclass
class AssignmentResult:
assignment_id: str
task_name: str
total_tasks: int
@workflow.defn
class TaskWorkflow:
def __init__(self) -> None:
self.tasks: list[str] = []
@workflow.run
async def run(self) -> None:
await workflow.wait_condition(lambda: False)
@workflow.update
async def assign_task(self, task_name: str) -> AssignmentResult:
assignment_id = str(uuid.uuid4())
self.tasks.append(task_name)
return AssignmentResult(
assignment_id=assignment_id,
task_name=task_name,
total_tasks=len(self.tasks),
)
@assign_task.validator
def validate_assign_task(self, task_name: str) -> None:
if len(self.tasks) >= MAX_TASKS:
raise ValueError("Task limit reached")
@workflow.query
def get_tasks(self) -> list[str]:
return list(self.tasks)// workflow.go
type TaskWorkflow struct{}
const MaxTasks = 10
func (w *TaskWorkflow) Run(ctx workflow.Context) error {
tasks := []string{}
err := workflow.SetUpdateHandlerWithOptions(
ctx,
"AssignTask",
func(ctx workflow.Context, taskName string) (AssignmentResult, error) {
assignmentID := uuid.New().String()
tasks = append(tasks, taskName)
return AssignmentResult{
AssignmentID: assignmentID,
TaskName: taskName,
TotalTasks: len(tasks),
}, nil
},
workflow.UpdateHandlerOptions{
Validator: func(taskName string) error {
if len(tasks) >= MaxTasks {
return fmt.Errorf("task limit reached")
}
return nil
},
},
)
if err != nil {
return err
}
err = workflow.SetQueryHandler(ctx, "GetTasks", func() ([]string, error) {
return tasks, nil
})
if err != nil {
return err
}
workflow.GetSignalChannel(ctx, "").Receive(ctx, nil)
return nil
}// TaskWorkflow.java
@WorkflowInterface
public interface TaskWorkflow {
@WorkflowMethod
void run();
@UpdateMethod
AssignmentResult assignTask(String taskName);
@QueryMethod
List<String> getTasks();
}
public class TaskWorkflowImpl implements TaskWorkflow {
private static final int MAX_TASKS = 10;
private List<String> tasks = new ArrayList<>();
@Override
public void run() {
Workflow.await(() -> false);
}
@UpdateValidatorMethod(updateName = "assignTask")
protected void validateAssignTask(String taskName) {
if (tasks.size() >= MAX_TASKS) {
throw new IllegalStateException("Task limit reached");
}
}
@Override
public AssignmentResult assignTask(String taskName) {
String assignmentId = UUID.randomUUID().toString();
tasks.add(taskName);
return new AssignmentResult(assignmentId, taskName, tasks.size());
}
@Override
public List<String> getTasks() {
return new ArrayList<>(tasks);
}
}// workflow.ts
import * as wf from '@temporalio/workflow';
interface AssignmentResult {
assignmentId: string;
taskName: string;
totalTasks: number;
}
export const assignTaskUpdate = wf.defineUpdate<AssignmentResult, [string]>('assignTask');
export const getTasksQuery = wf.defineQuery<string[]>('getTasks');
const MAX_TASKS = 10;
export async function taskWorkflow(): Promise<void> {
const tasks: string[] = [];
wf.setHandler(
assignTaskUpdate,
(taskName: string): AssignmentResult => {
const assignmentId = wf.uuid4();
tasks.push(taskName);
return { assignmentId, taskName, totalTasks: tasks.length };
},
{
validator: (taskName: string): void => {
if (tasks.length >= MAX_TASKS) {
throw new Error('Task limit reached');
}
},
}
);
wf.setHandler(getTasksQuery, (): string[] => tasks);
await wf.condition(() => false);
}In all SDKs, the validator runs before the Update handler. If the validator throws an exception, the Update is rejected and the client receives a typed error. If the validator passes, the Update handler modifies state and returns a typed result. The Update is recorded in Workflow history before the response is returned to the client.
When to use
The Update pattern is a good fit for request-response patterns requiring immediate confirmation, input validation before accepting work, synchronous state modifications with typed responses, operations requiring strong consistency guarantees, and entity Workflows that need external state Updates.
It is not a good fit for fire-and-forget operations (use Signals), read-only operations (use Queries), high-throughput scenarios where latency matters (Updates are slower than Signals), or operations that do not need an immediate response.
Benefits and trade-offs
Updates provide a synchronous response — the client receives a typed return value immediately. Validation failures return as typed exceptions. The Update is recorded in history before returning, providing strong consistency. You can modify Workflow state directly from external systems.
The trade-offs to consider are that Updates are slower than Signals (they require a history write). The Update handler blocks Workflow Task execution. Update handlers consume Workflow Task execution time. Updates are more complex than Signals for notifications. Update arguments and return values are limited by the Workflow history event size (typically 2 MB per event). Each Update adds events to Workflow history, contributing to the 50K event limit. There is a maximum of 10 in-flight Updates per Workflow execution and a maximum of 2,000 total Updates in Workflow history.
Comparison with alternatives
| Approach | Use case | Response type | Latency | Consistency |
|---|---|---|---|---|
| Update | Request-response | Sync typed value | Higher | Strong |
| Signal | Fire-and-forget | None | Lower | Eventual |
| Query | Read-only | Sync typed value | Lowest | Eventual |
Best practices
- Validate early. Check inputs at the start of the Update handler to fail fast.
- Handle errors. Throw typed exceptions for validation failures.
- Return quickly. Do not perform long operations in the Update handler.
- Ensure idempotency. Track processed Update IDs if Updates can be retried.
- Set timeouts. Configure appropriate Update timeouts.
- Maintain state consistency. Ensure state modifications are atomic within the handler.
Common pitfalls
- Performing long operations in the Update handler. Update handlers block Workflow Task execution. Offload long-running work to Activities and use
Workflow.awaitin the handler to wait for results. - Exceeding the 2,000 total Updates limit. Each accepted Update adds events to history. Use Continue-As-New before reaching the limit. The server sets
SuggestContinueAsNewat 90% of the limit. - Not setting Update timeouts. Without a client-side timeout, the caller blocks indefinitely if the Worker is unavailable. Always set a context timeout or deadline.
- Ignoring Update ID for deduplication. Without an Update ID, retried requests create duplicate Updates. Provide a unique
updateIdfor idempotency, especially with Update-with-Start. - Using Updates for fire-and-forget. Updates require a Worker to be online and responsive. For fire-and-forget operations, use Signals instead.
Related patterns
- Signal: Fire-and-forget state modifications.
- Query: Read-only state inspection.
- Entity Workflow: Long-running Workflows representing business entities.
- Early Return: Returning intermediate results before Workflow completion.
Sample code
- Safe Message Handlers (Python) — Concurrent Update handling with validation.
- Safe Message Passing (Java) — Concurrent Update handling with validation.
- Update with Start - Shopping Cart (Go) — Update-with-Start for lazy initialization.