Temporal Workflow message passing - Signals, Queries, & Updates
Workflows can be thought of as stateful web services that can receive messages. The Workflow can have powerful message handlers akin to endpoints that react to the incoming messages in combination with the current state of the Workflow. Temporal supports three types of messages: Signals, Queries, and Updates:
- Queries are read requests. They can read the current state of the Workflow but cannot block in doing so.
- Signals are asynchronous write requests. They cause changes in the running Workflow, but you cannot await any response or error.
- Updates are synchronous, tracked write requests. The sender of the Update can wait for a response on completion or an error on failure.
How to choose between Signals, Updates, and Queries as a Workflow author?
This section will help you write Workflows that receive messages.
For write requests
Unlike Signals, Updates must be synchronous. That is, they must wait for the Worker running the Workflow to acknowledge the request.
Use Signals instead of Updates when:
- The Workflow's clients want to quickly move on after sending an asynchronous message.
- The clients care more about the latency of sending the message than they do about the latency of the end-to-end operation.
- The clients don't want to rely on the Worker being available.
- Clients need to Signal-With-Start atomically (since there is currently no Update-with-Start operation.)
Use Updates instead of Signals when:
- The Workflow's clients want to track the completion of the message.
- The clients need a result or an exception from your message without having to query subsequently.
- You’d like to “validate” the Update before accepting it into the Workflow and its history.
- The clients want a low-latency end-to-end operation and are willing to wait for it to finish or be validated.
For read requests
You normally want to do a Query, because:
- Queries are efficient–they never add entries to the Workflow Event History, whereas an Update would (if accepted).
- Queries can operate on completed Workflows.
However, because Queries cannot block, sometimes Updates are best. When your goal is to do a read once the Workflow achieves a certain desired state, you have two options:
- You could poll periodically with Queries until the Workflow is ready.
- You could write your read operation as an Update, which will give you better efficiency and latency, though it will write an entry to the Workflow Event History.
For read/write requests
Use an Update for synchronous read/write requests. If your request must be asynchronous, consider sending a Signal followed by polling with a Query.
Sending Messages
This section will help you write clients that send messages to Workflows.
Sending Signals
You can send Signals from any Temporal Client, the Temporal CLI, or you can Signal one Workflow to another.
You can also Signal-With-Start to lazily initialize a Workflow while sending a Signal.
Send a Signal from a Temporal Client or the CLI
Send a Signal from one Workflow to another.
Signal-With-Start
Signal-With-Start is a great tool for lazily initializing Workflows. When you send this operation, if there is a running Workflow Execution with the given Workflow Id, it will be Signaled. Otherwise, a new Workflow Execution starts and is immediately sent the Signal.
Sending Updates
- In Pre-release (API is subject to change)
- Introduced in Temporal Server version 1.21
- Available in Go SDK since v1.23.0
- Available in Java SDK since v1.20.0
- Available in Python SDK since v1.4.0
- Available in .NET SDK since v0.1.0-beta2
- Available in TypeScript SDK since v1.9.0
- Available in PHP SDK since v2.8.0
Workflow Updates are currently disabled by default on Temporal Server.
To enable sending Updates, set the frontend.enableUpdateWorkflowExecution and frontend.enableUpdateWorkflowExecutionAsyncAccepted dynamic config values to true
.
For example, with the Temporal CLI, run these commands:
temporal server start-dev --dynamic-config-value frontend.enableUpdateWorkflowExecution=true
temporal server start-dev --dynamic-config-value frontend.enableUpdateWorkflowExecutionAsyncAccepted=true
Updates can be sent from a Temporal Client or the Temporal CLI to a Workflow Execution. This call is synchronous and will call into the corresponding Update handler. If you’d rather make an asynchronous request, you should use Signals.
In most languages (except Go), you may call executeUpdate
to complete an Update and get its result.
Alternatively, to start an Update, you may call startUpdate
and pass in the Workflow Update Stage as an argument. You have two choices on what to await:
- Accepted - wait until the Worker is contacted, which ensures that the Update is persisted. See Update Validators for more information.
- Completed - wait until the handler finishes and returns a result. (This is equivalent to
executeUpdate
.)
The start call will give you a handle you can use to track the Update, determine whether it was Accepted, and ultimately get its result or an error.
If you want to send an Update to another Workflow such as a Child Workflow from within a Workflow, you should do so within an Activity and use the Temporal Client as normal.
Sending Queries
Queries can be sent from a Temporal Client or the Temporal CLI to a Workflow Execution--even if this Workflow has Completed. This call is synchronous and will call into the corresponding Query handler. You can also send a built-in "Stack Trace Query" for debugging.
Stack Trace Query
In many SDKs, the Temporal Client exposes a predefined __stack_trace
Query that returns the call stack of all the threads owned by that Workflow Execution.
This is a great way to troubleshoot a Workflow Execution in production.
For example, if a Workflow Execution has been stuck at a state for longer than an expected period of time, you can send a __stack_trace
Query to return the current call stack.
The __stack_trace
Query name does not require special handling in your Workflow code.
Stack Trace Queries are available only for running Workflow Executions.
Handling Signals, Updates, and Queries
When Signals, Updates, and Queries arrive at your Workflow, the handlers for these messages will operate on the current state of your Workflow and can use whatever fields you have set. In this section, we’ll give you an overview of how messages work with Temporal and cover how to write correct and robust handlers by covering topics like atomicity, guaranteeing completion before the Workflow exits, exceptions, and idempotency.
Message handler concurrency
If your Workflow receives messages, you may need to consider how those messages interact with one another or with the main Workflow method. Behind the scenes, Temporal is running a loop that looks like this:
Diagram that shows the execution ordering of Workflows
Every time the Workflow wakes up--generally, it wakes up when it needs to--it will process messages in the order they were received, followed by making progress in the Workflow’s main method.
This execution is on a single thread–while this means you don’t have to worry about parallelism, you do need to worry about concurrency if you have written Signal and Update handlers that can block. These can run interleaved with the main Workflow and with one another, resulting in potential race conditions. These methods should be made reentrant.
Message handler patterns
Here are several common patterns for write operations, Signal and Update handlers. They don't apply to pure read operations, i.e. Queries or Update Validators:
- Synchronous, returning immediately.
- Waiting for the Workflow to be ready to process them.
- Kicking off activities and other asynchronous tasks
- Injecting work into the main Workflow.
- Finishing handlers before the Workflow completes.
- Ensuring your messages are processed exactly once.
Synchronous handlers
Synchronous handlers don’t kick off any long-running operations or otherwise block. They're guaranteed to run atomically.
Waiting
A Signal or Update handler can block waiting for the Workflow to reach a certain state using a Wait Condition. See the links below to find out how to use this with your SDK.
Running asynchronous tasks
Sometimes, you need your message handler to wait for long-running operations such as executing an Activity. When this happens, the handler will yield control back to the loop. This means that your handlers can have race conditions if you’re not careful. You can guard your handlers with concurrency primitives like mutexes or semaphores, but you should use versions of these primitives provided for Workflows in most languages. See the links below for examples of how to use them in your SDK.
Inject work into the main Workflow
Sometimes you want to process work provided by messages in the main Workflow. Perhaps you’d like to accumulate several messages before acting on any of them. For example, message handlers might put work into a queue, which can then be picked up and processed in an event loop that you yourself write. This option is considered advanced but offers powerful flexibility. And if you serialize the handling of your messages inside your main Workflow, you can avoid using concurrency primitives like mutexes and semaphores. See the links above for how to do this in your SDK.
Finishing handlers before the Workflow completes
You should generally finish running all handlers before the Workflow run completes or continues as new. For some Workflows, this means you should explicitly check to make sure that all the handlers have completed before finishing. You can await a condition called All Handlers Finished at the end of your Workflow.
If you don’t need to ensure that your handlers complete, you may specify your handler’s Handler Unfinished Policy as Abandon to turn off the warnings. However, note that clients waiting for Updates will get Not Found errors if they're waiting for Updates that never complete before the Workflow run completes.
See the links below for how to ensure handlers are finished in your SDK.
Ensuring your messages are processed exactly once
Many developers want their message handlers to run exactly once--to be idempotent--in cases where the same Signal or Update is delivered twice or sent by two different call sites. Temporal deduplicates messages for you on the server, but there is one important case when you need to think about this yourself when authoring a Workflow, and one when sending Signals and Updates.
When your workflow Continues-As-New, you should handle deduplication yourself in your message handler. This is because Temporal's built-in deduplication doesn't work across Continue-As-New boundaries, meaning you would risk processing messages twice for such Workflows if you don't check for duplicate messages yourself.
To deduplicate in your message handler, you can use an idempotency key.
Clients can provide an idempotency key. This can be important because Temporal's SDKs provide a randomized key by default, which means Temporal only deduplicates retries from the same call. For Updates, if you craft an Update ID, temporal will deduplicate any calls that use that key. This is useful when you have two different callsites that may send the same Update, or when your client itself may get retried. For Signals, you can provide a key as part of your Signal arguments.
Inside your message handler, you can check your idempotency key--the Update ID or the one you provided to the Signal--to check whether the Workflow has already handled the update.
Update Validators
When you define an Update handler, you may optionally define an Update Validator: a read operation that's responsible for accepting or rejecting the Update. You can use Validators to verify arguments or make sure the Workflow is ready to accept your Updates.
- If it accepts, the Update will become part of your Workflow’s history and the client will be notified that the operation has been Accepted. The Update handler will then run until it returns a value.
- If it rejects, the client will be informed that it was Rejected, and the Workflow will have no indication that it was ever requested, similar to a Query handler.
Like Queries, Validators are not allowed to block.
Once the Update handler is finished and has returned a value, the operation is considered Completed.
Exceptions in message handlers
When throwing an exception in a message handler, you should decide whether to make it an Application Failure. The implications are different between Signals and Updates.
The following content applies in every SDK except the Go SDK. See below.
Exceptions in Signals
In Signal handlers, throw Application Failures only for unrecoverable errors, because the entire Workflow will fail. If you throw any other exception, by default, it will cause a Workflow Task Failure. This means the Workflow will get stuck and will retry the handler periodically until the exception is fixed, for example by a code change.
Exceptions in Updates
When you want to fail the Update and for the client to receive that error:
- Reject the Update by throwing any exception from your Validator, OR
- Throw an Application Failure from your Update handler. Unlike with Signals, the Workflow will keep going.
If you throw any other exception, by default, it will cause a Workflow Task Failure. This means the Workflow will get stuck and will retry the handler periodically until the exception is fixed, for example by a code change or infrastructure coming back online. Note that this will cause a delay for clients waiting for a result.
Errors and panics in message handlers in the Go SDK
In Go, returning an error behaves like an Application Failure in the other SDKs. Panics behave like non-Application Failure exceptions in other languages, in that they cause a Workflow Task Failure.
Writing Signal Handlers
Use these links to see a simple Signal handler.
Writing Query Handlers
Author queries using these per-language guides.