Skip to main content

Handling Signals, Queries, & Updates

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 the 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.

Handling Messages

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

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.

Initializing the Workflow first

Initialize your Workflow's state before handling messages. This prevents your handler from reading uninitialized instance variables.

To see why, refer to the diagram. It shows that your Workflow processes messages before the first run of your Workflow's main method.

The message handler runs first in several scenarios, such as:

  • When using Signal-with-Start.
  • When your Worker experiences delays, such as when the Task Queue it polls gets backlogged.
  • When messages arrive immediately after a Workflow continues as new but before it resumes.

For all languages except Go and TypeScript, use your constructor to set up state. Annotate your constructor as a Workflow Initializer and take the same arguments as your Workflow's main method.

Note that you can't make blocking calls from your constructor. If you need to block, make your Signal or Update handler wait for an initialization flag.

In Go and TypeScript, register any message handlers only after completing initialization.

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:

  • Returning immediately from a handler
  • 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.

See the links below for examples of solving this in your SDK.

Authoring message handler patterns

See examples of the above patterns.

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.
note

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.

caution

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. Similarly, allowing a failing Activity or Child Workflow to exhaust its retries, so that it throws an Activity Failure or Child Workflow Failure will cause the entire Workflow to fail. Note that for Activities, this will only happen if you change the default Activity Retry Policy, since by default they retry forever. 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

Doing any of the following will fail the Update and cause the client to receive the error:

Unlike with Signals, the Workflow will keep going in these cases.

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 an Update 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 Update Handlers

Use these links to see a simple update handler.

Writing Query Handlers

Author queries using these per-language guides.