new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

Temporal · Develop · all subjects

message-passing

194 notes in this subject, read out of this brain and free to use. This is page 3 of 4.

Ruby SDK dynamic handlers best practices

Dynamic Handlers should be used judiciously as a fallback mechanism rather than the primary approach. Overusing them can lead to maintainability and debugging issues. Instead, Signals, Queries, Updates, Workflows, or Activities should be defined statically whenever possible, with clear names that indicate their purpose. Reserve Dynamic Handlers for cases where the handler names are not known at compile time and need to be looked up dynamically at runtime. They are meant to handle edge cases and act as a catch-all, not as the main way of invoking logic.

Ruby SDK async Activity in Update handler example

Here is an example of an async Update handler that executes an Activity: The CallGreetingService Activity simulates a network call. In the GreetingWorkflow.apply_language_with_lookup Update handler, if the requested language is not cached, a Mutex is used to ensure only one handler instance schedules the Activity at a time. The Activity is executed with `Temporalio::Workflow.execute_activity(CallGreetingService, new_language, start_to_close_timeout: 10)`. If the Activity returns nil (language not supported), an ApplicationError is raised. The greeting is cached in @greetings, then set_language is called to update the current language.

Query handler basics - Rust SDK

A Query is a synchronous operation that retrieves state from a Workflow Execution. Query handlers are defined as methods on the Workflow struct and registered with the Workflow runtime. Query handlers can't mutate Workflow state and can't perform async operations like executing Activities.

Query handler implementation - Rust SDK

Query handlers are defined with the #[query] attribute on a method that takes &self, &WorkflowContextView, and input parameters, and returns a value. Example: #[query] pub fn get_languages(&self, _ctx: &WorkflowContextView, input: GetLanguagesInput) -> Vec<Language>.

Send Query from client - Rust SDK

To send a Query, call query() on a Workflow handle with the handler function, input parameters, and WorkflowQueryOptions. Example: wf_handle.query(GreetingsWorkflow::get_languages, GetLanguagesInput { include_unsupported: true }, WorkflowQueryOptions::default()).await?

Query characteristics - Rust SDK

Sending a Query doesn't add events to a Workflow's Event History. You can send Queries to closed Workflow Executions within a Namespace's Workflow retention period, including Workflows that have completed, failed, or timed out. Querying terminated Workflows is not supported. A Worker must be online and polling the Task Queue to process a Query.

Signal handler basics - Rust SDK

A Signal is an asynchronous message sent to a running Workflow Execution to change its state and control its flow. Signal handlers are defined with the #[signal] attribute and don't return values. They can trigger async work like Activities and timers depending on SDK capabilities.

Signal handler implementation - Rust SDK

Signal handlers are defined with the #[signal] attribute on a method that takes &mut self, &mut SyncWorkflowContext<Self>, and input parameters. They don't return values. Example: #[signal] pub fn approve(&mut self, _ctx: &mut SyncWorkflowContext<Self>, input: ApproveInput) { self.approved_for_release = true; }

Send Signal from client - Rust SDK

To send a Signal from a client, call signal() on a Workflow handle with the handler function, input parameters, and WorkflowSignalOptions. Example: wf_handle.signal(GreetingsWorkflow::approve, ApproveInput { name: "Ziggy".to_string() }, WorkflowSignalOptions::default()).await?

Send Signal from Workflow - Rust SDK

To send a Signal from one Workflow to another, use external_workflow() with workflow ID and optional run ID, then call signal(). Example: ctx.external_workflow("workflow-id-1", Some("run-id-1".into())).signal(GreetingsWorkflow::approve, ApproveInput { name: "Ziggy".to_string() }, SignalWorkflowOptions::default()).await?

Signal-with-Start - Rust SDK

Signal-with-Start allows sending a Signal when starting a Workflow. Create signal input as payloads, then pass to WorkflowStartOptions using start_signal(WorkflowStartSignal::new(signal_name).maybe_input(signal_input).build()). Example: .start_signal(WorkflowStartSignal::new("approve").maybe_input(signal_input).build())

Signal constraints - Rust SDK

Signals can only be sent to Workflow Executions that haven't closed. When sending a Signal from a client, the call returns when the server accepts the Signal; it does not wait for the Signal to be delivered to the Workflow Execution.

Update handler basics - Rust SDK

An Update is a trackable synchronous request sent to a running Workflow Execution that can change Workflow state, control its flow, and return a result. The sender waits until the Worker accepts or rejects the Update, and may wait further to receive a returned value or an exception.

Update handler implementation - Rust SDK

Update handlers are defined with the #[update] attribute on a method that takes &mut self, &mut SyncWorkflowContext<Self>, and input parameters, and returns a value. Example: #[update] pub fn set_language(&mut self, _ctx: &mut SyncWorkflowContext<Self>, input: SetLanguageInput) -> Language { let previous_language = self.language; self.language = input.language; previous_language }

Update validator implementation - Rust SDK

Update validators are defined with the #[update_validator(handler_name)] attribute on a method that takes &self, &WorkflowContextView, and input parameters by reference, and returns Result<(), Box<dyn std::error::Error + Send + Sync>>. To reject an Update, raise an exception of any type in the validator.

Update validator characteristics - Rust SDK

Validators are always optional and used to reject Updates before they are written to History. When a validator raises an error, the Update is rejected and WorkflowExecutionUpdateAccepted is not added to Event History; the caller receives an 'Update failed' error. Without a validator, Updates are always accepted. The WorkflowExecutionUpdateAccepted event is written to History whether acceptance was automatic or programmatic.

Send Update from client - Rust SDK

To send an Update and wait for completion, call execute_update() on a Workflow handle with the handler function, input parameters, and WorkflowExecuteUpdateOptions. Example: let previous_language = wf_handle.execute_update(GreetingsWorkflow::set_language, SetLanguageInput { language: Language::French }, WorkflowExecuteUpdateOptions::default()).await?

Update-with-Start - Rust SDK

Use start_update() to receive an UpdateHandle as soon as the Update is accepted, rather than waiting for completion. The UpdateHandle can be used later to fetch results. This is useful for async Update handlers performing long-running operations. Example: let update_handle = main_wf_handle.start_update(GreetingsWorkflow::set_language, SetLanguageInput { language: Language::French }, WorkflowStartUpdateOptions::default()).await?

Update event history - Rust SDK

WorkflowExecutionUpdateAccepted is added to Event History when the Worker confirms that the Update passed validation. WorkflowExecutionUpdateCompleted is added to Event History when the Worker confirms that the Update has finished.

Async handlers - Rust SDK

Signal and Update handlers can be defined as async fn as well as fn. Using async fn allows you to use await with Activities, Child Workflows, Timers, and other async operations. Handler executions and the main Workflow method run concurrently, with switching occurring at await calls.

Async Update handler example - Rust SDK

An async Update handler can execute an Activity and return its result. Example: #[update] async fn set_language_activity(ctx: &mut WorkflowContext<Self>, language: Language) -> Result<Language, Box<dyn std::error::Error + Send + Sync>> { let greeting = ctx.execute_activity(GreetingActivities::call_greeting_service, Language::French, ActivityOptions::start_to_close_timeout(Duration::from_secs(10))).await?; }

Wait condition pattern - Rust SDK

Use ctx.wait_condition() to prevent handler code from proceeding until a condition is true. Pass a function that returns a boolean and optionally set a timeout. Use cases: wait for a Signal or Update to arrive, wait in a handler until appropriate to continue, wait in the main Workflow until all active handlers have finished.

Wait condition example - Rust SDK

Example of waiting for approval Signal in main Workflow: ctx.wait_condition(|s| !s.approved_for_release).await?

Ensure handlers complete before Workflow finishes - Rust SDK

When a Workflow uses async Signal or Update handlers, the main Workflow method can return or continue-as-new while a handler is still waiting on an async task like an Activity. This may interrupt the handler before it finishes crucial work and cause client errors when retrieving Update results. Use ctx.wait_condition() in the main Workflow to ensure all handlers complete before the Workflow ends.

Message handler guidelines - Rust SDK

When writing message handlers: Message handlers are defined as methods on the Workflow struct and registered with the Workflow runtime. The parameters and return values of handlers and the main Workflow function must be serializable. Prefer structs to multiple input parameters to allow for forward-compatible changes.

Continue-As-New with message handlers restriction

If you use Updates or Signals, do not call Continue-As-New from the handlers. Instead, wait for your handlers to finish in your main Workflow before you run Continue-As-New.

CancelledFailure indicates Workflow cancellation

The TypeScript SDK provides CancelledFailure as a Temporal error class to handle Workflow cancellation. You should not raise or manually implement this error class as it is tied to Temporal platform logic.

Publish events from Workflow Streams

Bind a topic name to its event type once via stream.topic<T>(name), then call publish() on the returned handle to append events. The handle carries the topic name and type T so call sites do not have to repeat them on every publish. Repeated calls with the same name return the same handle instance. publish() is non-blocking, applies no backpressure, and runs the default payload converter to encode each value. The codec chain (encryption, compression, etc.) runs once on the Signal or Update envelope that carries the batch, not per item.

Type safety in Workflow Streams publishing

In TypeScript, the type parameter T on stream.topic<T>(name) is a compile-time annotation only; TypeScript has no runtime type representation, so the library cannot enforce per-topic type uniformity at the publish site. If two publishers bind the same topic name to different types, the mismatch is not caught at publish time. The subscriber gets a decode error when it processes events from the mismatched publisher. A pre-built Payload may be passed to publish() regardless of the handle's type T, taking the zero-copy fast path.

Publish from client to Workflow stream

Any process with a Temporal Client and the target Workflow Id can publish to that Workflow's stream by constructing a WorkflowStreamClient with WorkflowStreamClient.create(client, workflowId). Then use it the same way as the Workflow-side handle: bind a topic, publish through it, and let await using flush on scope exit. When events originate in an Activity, publish from the Activity directly rather than returning them for the Workflow to forward, to keep Workflow state independent of streamed output so retried Activity attempts surface to subscribers without polluting the Workflow's durable state.

Publish from Activity to Workflow stream

Inside an Activity scheduled by a Workflow, use WorkflowStreamClient.fromWithinActivity() to infer the Temporal Client and the parent Workflow Id from the Activity context, so you do not have to thread them through the Activity's input. For a standalone Activity (one started directly via Client.activity.start rather than from a Workflow), there is no parent Workflow context to infer, so fromWithinActivity() throws. Fall back to the general pattern with Context.current().client and the target Workflow Id threaded through the Activity's input.

Explicit control over batch flushing in Workflow Streams

Two operations give explicit control over when batches ship: forceFlush: true on a publish for latency, and await client.flush() for confirmation that prior publications have landed. Pass { forceFlush: true } on a publish to wake the background flusher so the current buffer ships without waiting for the next interval. The call returns immediately after appending to the buffer and signaling the flusher; it does not wait for delivery to the Workflow or to subscribers. Use await client.flush() when you need a mid-stream barrier; successful completion is proof that the Temporal server has received all prior publications, so subsequent work depending on those events being durable can proceed. Exiting await using already flushes on its way out.

Subscribe to Workflow stream topics

To subscribe, use the same client construction as publishing: WorkflowStreamClient.create(client, workflowId) from any process that has a Temporal Client or fromWithinActivity() inside an Activity. Once you have a client, iterate a topic handle's subscribe(), the counterpart to publish(). The handle's bound type drives decoding, so each item.data arrives as T via the client's payload converter. The codec chain is applied once at the Update envelope, not per item. The iterator handles re-polling, pagination when a poll response hits the ~1 MB cap, and Workflow-side log truncation transparently.

Subscribe to heterogeneous Workflow Stream topics

A topic handle binds one name to one type, so it only fits a single-type subscription. To consume multiple topics whose payload types differ, call client.subscribe() directly with a list of names or subscribe() with no arguments for every topic. The default overload yields WorkflowStreamItem<Payload>, so each item arrives as the raw Payload carrying encoding metadata. Dispatch on item.topic and decode the payload with defaultPayloadConverter.fromPayload<T>(item.data). A single iterator over multiple topics avoids the cancellation race that two concurrent subscribers would create.

Close Workflow Stream with fixed sleep or acknowledgment

A subscriber's for await does not know when the publisher is done. Two approaches provide overlap before the Workflow returns: Fixed sleep—sleep between the terminator and the return so any in-flight poll has time to fetch the terminator before the Workflow exits. Acknowledgment handshake—the subscriber sends a Signal once it has the terminator; the Workflow waits up to a timeout, returning as soon as the ack arrives. subscribe() exits cleanly when the Workflow reaches COMPLETED, FAILED, CANCELLED, TERMINATED, or TIMED_OUT, but does not distinguish among them. Call await temporalClient.workflow.getHandle(workflowId).describe() after the loop returns to inspect the Workflow's terminal status.

Workflow Streams deduplication publisherTtl setting

At each Continue-As-New, deduplicate entries whose lastSeen is older than publisherTtl are dropped. lastSeen is updated on each successful publish (not on each retry attempt), so a publisher that retries through a long partition without success can still age out. A publisher that returns after a longer pause may produce a duplicate. Tune upward if publishers can be silent for extended windows by passing { publisherTtl: '...' } to stream.continueAsNew(buildArgs, options).

Workflow Streams maxRetryDuration for batch flushing

A WorkflowStreamClient retries a failed batch for up to maxRetryDuration. If the duration elapses with the batch still pending, the client gives up, the pending batch is dropped, and a FlushTimeoutError is raised. On timeout, the dropped batch is at-most-once: it may or may not have reached the Workflow. Subsequent publishes resume cleanly with the next sequence. The FlushTimeoutError is raised from inside the background flusher task and terminates it. Until you call await client.flush() or exit the await using scope, subsequent publishes accumulate in the buffer with no flusher to ship them.

WorkflowStreamClient single event loop requirement

WorkflowStreamClient is single-event-loop. The client buffer is mutated on the publish path and read from the background flusher inside one Node event loop. Do not call publish() from a Worker thread. Route events back to the loop that owns the client.

Workflow Stream backpressure handling

publish() is non-blocking and applies no backpressure. From an Activity or other client, it appends to the client's in-memory buffer and returns. From inside a Workflow, it appends synchronously to the in-memory log (no buffer, nothing to flush). Subscribers pull from the Workflow's log on their own schedule, so a slow subscriber does not slow down publishers. If a publisher emits faster than batches can ship to the server, the buffer grows: the process uses more memory, the stream falls further behind real time, and at the limit Signals cannot keep up. If your application needs to bound this (to cap memory, to keep the stream close to real time, or to apply a policy when the publisher overruns the network), apply that policy upstream of publish(). The choice (block, drop, error, sample) is application-specific.

LLM streaming pattern with Workflow Streams

The LLM streaming pattern has an Activity call the model and publish deltas as they arrive, the Workflow kick off the Activity and wait for the consumer to acknowledge end-of-stream, and the consumer subscribe and accumulate deltas, clearing accumulated state on RETRY before continuing. If the Activity can retry, a retried attempt is a fresh publisher, so its output appears in the stream alongside output from the previous attempt. The Activity publishes a RETRY event when Context.current().info.attempt > 1 to let the UI respond appropriately. Termination uses an ack handshake: the consumer signals the Workflow once it has received the close event. forceFlush: true is used only on the first delta and on the RETRY sentinel, where latency matters; subsequent deltas batch at the configured batchInterval.

Query handler basics in TypeScript

A Query is a synchronous operation that retrieves state from a Workflow Execution. Query handlers are defined using `wf.defineQuery()` to create a query type, then `wf.setHandler()` to register the handler in the Workflow function. A Query handler cannot be async, cannot perform async operations like executing an Activity, must not mutate Workflow state, and can only return a value.

Signal handler basics in TypeScript

A Signal is an asynchronous message sent to a running Workflow Execution to change its state and control its flow. Signal handlers are defined using `wf.defineSignal()` to create a signal type, then `wf.setHandler()` to register the handler in the Workflow function. A Signal handler can be async, can mutate Workflow state, but cannot return a value. The response is sent immediately from the server without waiting for the Workflow to process the Signal.

Update handler basics in TypeScript

An Update is a trackable synchronous request sent to a running Workflow Execution that can change Workflow state, control its flow, and return a result. Update handlers are defined using `wf.defineUpdate()` to create an update type, then `wf.setHandler()` to register the handler in the Workflow function. The sender must wait until the Worker accepts or rejects the Update. An Update handler can be async and can mutate Workflow state and return a value.

Update validators in TypeScript

Update validators are optional functions that run before an Update is written to History. A validator is passed in `UpdateHandlerOptions` when calling `workflow.setHandler()`. The validator must be a non-async function that accepts the same argument types as the handler and returns void. To reject an Update, throw an error of any type in the validator. When a validator throws an error, the Update is rejected and `WorkflowExecutionUpdateAccepted` will not be added to Event History; the caller receives an 'Update failed' error. Without a validator, Updates are always accepted. When a validator does not throw an error, `WorkflowExecutionUpdateAccepted` is written to History.

Send Query in TypeScript client

Use `WorkflowHandle.query()` to send a Query to a Workflow Execution. Example: `const supportedLanguages = await handle.query(getLanguages, { includeUnsupported: false });`. Sending a Query does not add events to a Workflow's Event History. Queries can be sent to closed Workflow Executions within a Namespace's Workflow retention period, including completed, failed, or timed out Workflows, but not to terminated Workflows. A Worker must be online and polling the Task Queue to process a Query.

Send Signal from client in TypeScript

Use `WorkflowHandle.signal()` to send a Signal to a Workflow Execution. Example: `await handle.signal(greetingWorkflow.approve, { name: 'me' });`. The call returns when the server accepts the Signal; it does not wait for the Signal to be delivered to the Workflow Execution. The `WorkflowExecutionSignaled` Event appears in the Workflow's Event History. Signals can only be sent to Workflow Executions that haven't closed.

Send Signal from Workflow using External Signal in TypeScript

A Workflow can send a Signal to another Workflow, called an External Signal, using `getExternalWorkflowHandle()`. Example: Import `getExternalWorkflowHandle` from '@temporalio/workflow', get the external handle with `const handle = getExternalWorkflowHandle('workflow-id-123')`, then call `await handle.signal(joinSignal, { userId: 'user-1', groupId: 'group-1' });`. When an External Signal is sent, a `SignalExternalWorkflowExecutionInitiated` Event appears in the sender's Event History and a `WorkflowExecutionSignaled` Event appears in the recipient's Event History. Using `getExternalWorkflowHandle` helps ensure Workflows remain deterministic by recording interactions as Events in the Workflow's Event History.

Signal-With-Start in TypeScript

Signal-With-Start allows a Client to send a Signal to a Workflow Execution, starting the Execution if it is not already running. Use `Client.workflow.signalWithStart()`. Example: `await client.workflow.signalWithStart(yourWorkflow, { workflowId: 'workflow-id-123', taskQueue: 'my-taskqueue', args: [{ foo: 1 }], signal: joinSignal, signalArgs: [{ userId: 'user-1', groupId: 'group-1' }] });`. Signal-With-Start is limited to Client use and cannot be called from a Workflow.

Send Update from client in TypeScript

An Update is a synchronous, blocking call that can change Workflow state, control its flow, and return a result. A client sending an Update must wait until the Server delivers the Update to a Worker. Workers must be available and responsive. If you need a response as soon as the Server receives the request, use a Signal instead. You can't send Updates directly from one Workflow to another. If you need to send Updates across Workflows, like to Child Workflows, use an Activity.

Execute Update and wait for result in TypeScript

Use `WorkflowHandle.executeUpdate()` to send an Update to a Workflow Execution and wait for the Update to complete. This code blocks until the Update finishes. Example: `let previousLanguage = await handle.executeUpdate(setLanguage, { args: [Language.CHINESE] });`. When the Worker confirms that the Update passed validation, `WorkflowExecutionUpdateAccepted` is added to the Event History. When the Worker confirms that the Update has finished, `WorkflowExecutionUpdateCompleted` is added to the Event History.

Start Update without waiting for completion in TypeScript

Use `WorkflowHandle.startUpdate()` to start an Update and receive a `WorkflowUpdateHandle` as soon as the Update is accepted or rejected, without waiting for all asynchronous operations to complete. Example: `const updateHandle = await handle.startUpdate(setLanguage, { args: [Language.ENGLISH], waitForStage: WorkflowUpdateStage.ACCEPTED });` then `previousLanguage = await updateHandle.result();`. This is useful when Update handlers are async and perform long-running asynchronous operations such as calling an Activity.

Update-With-Start in TypeScript

Update-With-Start lets you send an Update that checks whether an already-running Workflow with that ID exists. If the Workflow exists, the Update is processed. If the Workflow does not exist, a new Workflow Execution is started with the given ID, and the Update is processed before the main Workflow method starts to execute. Use `executeUpdateWithStart()` to start an Update and wait for the result in one go, or use `startUpdateWithStart()` to start an Update and receive a `WorkflowUpdateHandle`. These calls return once the requested Update wait stage has been reached or when the request times out. You must provide a `WithStartWorkflowOperation` to define the Workflow that will be started if necessary and its arguments. You must specify a `WorkflowIdConflictPolicy` when creating the `WithStartWorkflowOperation`. A `WithStartWorkflowOperation` can only be used once. Temporal Server version 1.28 or later is recommended for open source server users.

Send messages without type safety in TypeScript

When you don't have access to the Workflow Definition or it isn't written in TypeScript, you can use APIs that aren't type-safe and use dynamic method invocation. Pass message type names as strings instead of message type objects to: `client.workflow.start()`, `WorkflowHandle.query()`, `WorkflowHandle.signal()`, `WorkflowHandle.executeUpdate()`, `WorkflowHandle.startUpdate()`. Pass Workflow IDs to `client.workflow.getHandle()` and `getExternalWorkflowHandle()` to get Workflow handles.

Async Signal and Update handlers in TypeScript

Signal and Update handlers can be async functions. Using async allows you to use await with Activities, Child Workflows, durable `workflow.sleep()` Timers, `workflow.condition()` conditions, and more. Async handlers execute concurrently with the main Workflow method and with other handler executions, with context switching occurring between them at await calls. It is essential to understand potential issues to use async handlers safely, including proper synchronization of concurrent access to state.

Use workflow.condition to wait for state conditions

`workflow.condition()` prevents code from proceeding until a condition is true. Pass a function that returns true or false. Common use cases: waiting for a Signal or Update to arrive, waiting in a handler until it is appropriate to continue, waiting in the main Workflow until all active handlers have finished. Example: `await wf.condition(() => approvedForRelease);`.

Wait for all handlers to finish before Workflow completes

When using async Signal or Update handlers, the main Workflow method can return or Continue-as-New while a handler is still waiting on an async task, interrupting the handler before it finishes crucial work. Use `workflow.condition(wf.allHandlersFinished)` to ensure all handlers complete before the Workflow finishes. Example: `await wf.condition(wf.allHandlersFinished); return workflowOutput;`. By default, your Worker will log a warning when you allow a Workflow Execution to finish with unfinished handler executions. You can silence warnings on a per-handler basis by setting `unfinishedPolicy` in `SignalHandlerOptions` or `UpdateHandlerOptions` when calling `workflow.setHandler()`.

Use mutex lock for concurrent handler execution control

Use a Mutex (from async-mutex library: `import { Mutex } from 'async-mutex'`) to prevent concurrent handler execution and ensure handlers don't interfere with each other. Wrap critical sections with `await lock.runExclusive(async () => { /* code */ })`. This ensures only one handler instance can execute a specific section of code at any given time, preventing race conditions when multiple handlers are executing concurrently and accessing shared state.

Continue-as-New restrictions with Updates

Temporal does not support Continue-as-New functionality within Update handlers. Complete all handlers before using Continue-as-New. Use Continue-as-New from your main Workflow Definition method, just as you would complete or fail a Workflow Execution.

Define message handlers guidelines in TypeScript

When writing message handlers: Define a message type as a global variable using `defineQuery()`, `defineSignal()`, or `defineUpdate()`. This is what your client code will use to send a message to the workflow. Message handlers are defined by calling `workflow.setHandler()` in your Workflow function. The parameters and return values of handlers and the main Workflow function must be serializable. Prefer using a single object over multiple input parameters, as a single object allows you to add fields without changing the signature.

Static Signal and Query definition in TypeScript

If you know the name of your Signals and Queries upfront, declare them outside the Workflow Definition. Example: `export const unblockSignal = wf.defineSignal('unblock'); export const isBlockedQuery = wf.defineQuery<boolean>('isBlocked');`. This technique helps provide type safety because you can export the type signature of the Signal or Query to be called by the Client.

Give your agent this brain