Temporal Operation Handler - Python SDK
The Temporal Operation Handler is pre-release.
TemporalOperationHandler is marked experimental in the SDKs and may change in backwards-incompatible ways.
A Nexus Service publishes Operations that other teams call across Namespace boundaries.
TemporalOperationHandler is how you implement those Operations.
For the conceptual model, see Temporal Operation Handler. This page shows how to write handlers in the Python SDK, migrate from earlier APIs, and compose Workflow, Update, Signal, and Activity backings.
What you can do with it
Back an Operation with whichever primitive fits the work. A multi-step process is a Workflow. A single durable step is an Activity, with no Workflow wrapped around it. A change to something already running is an Update. The caller sees the same Operation contract either way, and you can change your choice later without touching callers.
Combine messaging and a backing in one handler. A handler can Signal a running Workflow to unblock it and then return a different Execution's result for the caller to await. These are not separate handler types you pick between; they compose inside one start handler.
Get observability across the Namespace boundary without wiring it. The Client handed to your handler propagates bidirectional links and request Ids on every call it makes. The caller-side and handler-side Executions are connected in the UI and in Event History, so a single trace crosses the boundary between two teams' Namespaces.
Stay idempotent through retries. The server retries Nexus start requests, and the request Id travels with them. Deriving the backing Execution's Id from it means a retry targets the same Execution rather than starting a second one.
Cancel through the same handler. The Operation token records which kind of Execution backs the Operation, so a cancellation request reaches the right place. The default behavior is usually what you want, and each kind can be overridden when it is not.
Grow a handler without rewriting it. An Operation that starts out completing inline can later gain an async backing, or send a Signal, without changing handler type or breaking its contract.
The Nexus-aware Client
A TemporalOperationHandler start handler receives three things: a context, a Client, and the Operation input.
The Client is what makes the linking automatic, so prefer it over constructing your own inside a handler. Reaching for your own Client still works, but messages sent that way are not connected back to the caller.
The Client exposes two kinds of call, and the distinction shapes how you write the handler. The examples below use the Python SDK APIs.
Async backings — at most one per Operation invocation. These determine what the Operation is, and their result is delivered to the caller through the Nexus completion callback when the underlying Execution finishes.
- Start a Workflow — the Operation completes when the Workflow returns
- Update a Workflow — the Operation completes when the Update completes
- Start an Activity — the Operation completes when the Activity returns; see Nexus Standalone Activity
Sync messaging — as many as you need. Reach these through the underlying Temporal Client that the Nexus-aware Client exposes. They take effect during the handler call, still get link propagation, and do not require an async backing.
- Signal — delivered during the handler call to a Workflow that is already running
- Signal-with-Start — delivers the Signal, starting the Workflow first if it is not already running
A handler that only sends messages returns a synchronous result, and the Operation completes immediately.
Write an Operation handler
The examples below use a Nexus Service with a startGreeting Operation backed by a Workflow, an updateShippingAddress Operation backed by an Update, a cancelOrder Operation that sends a Signal, and a greet Operation backed by an Activity.
Back an Operation with a Workflow
Call startWorkflow on the Client and return its result. The Operation completes when the Workflow returns, delivering the Workflow's return value to the caller.
@nexus.temporal_operation
async def start_greeting(
self,
_ctx: nexus.TemporalStartOperationContext,
client: nexus.TemporalNexusClient,
input: GreetingInput,
) -> nexus.TemporalOperationResult[GreetingOutput]:
return await client.start_workflow(
GreetingWorkflow.run, input, id=f"greeting-{input.name}"
)
Back an Operation with an Update
Back an Operation with an Update when it changes something already running. The target Workflow has to exist already, and the Operation completes when the Update completes.
@nexus.temporal_operation
async def update_shipping_address(
self,
_ctx: nexus.TemporalStartOperationContext,
client: nexus.TemporalNexusClient,
input: UpdateAddressInput,
) -> nexus.TemporalOperationResult[AddressOutput]:
return await client.start_workflow_update(
f"order-{input.order_id}",
OrderWorkflow.update_shipping_address,
input,
)
Three constraints apply to Update-backed Operations, and the first two fail the Operation rather than degrading:
- Only the accepted stage is supported. A Nexus Operation can only back an asynchronous Update, so the wait-for-stage must be "accepted".
- The Update Id defaults to the Nexus request Id. Leaving it unset is what you usually want: a retried start request carries the same request Id, so it targets the same Update rather than running a second one.
The result is async in the normal case, carrying an update-workflow Operation token. If the Update has already completed by the time it is accepted — a retried request with the same Update Id, or an Update that completes immediately — you get a synchronous result instead.
Send a Signal from an Operation
Reach the Workflow Client through the injected Client, send the message, and return a synchronous result. The Operation completes during the handler call, and the Signal is linked back to the caller.
@nexus.temporal_operation
async def cancel_order(
self,
_ctx: nexus.TemporalStartOperationContext,
client: nexus.TemporalNexusClient,
input: CancelOrderInput,
) -> nexus.TemporalOperationResult[None]:
await client.client.get_workflow_handle(
f"order-{input.order_id}"
).signal("requestCancellation", input)
return nexus.TemporalOperationResult.sync(None)
The same Client also offers Signal-with-Start, and a handler may send several messages before returning.
Back an Operation with an Activity
Call startActivity when the work is a single durable step. The Activity runs with no parent Workflow, so the options require an Activity Id and at least one timeout. See Nexus Standalone Activity.
@nexus.temporal_operation
async def greet(
self,
ctx: nexus.TemporalStartOperationContext,
client: nexus.TemporalNexusClient,
input: GreetingInput,
) -> nexus.TemporalOperationResult[GreetingOutput]:
return await client.start_activity(
activities.greet,
input,
id=f"greet-{ctx.request_id}",
task_queue=TASK_QUEUE_NAME,
start_to_close_timeout=timedelta(seconds=10),
)
Coming from the earlier handler APIs
Skip this section if you are new to Nexus.
Earlier SDK versions had a separate helper per pattern. Existing handlers will keep working, there is no forced migration.
Operations already in progress are not a concern either. If you need to cancel one, for example, a Workflow-backed Operation started by one of the earlier APIs is cancelled by TemporalOperationHandler just as it would have been before.
| If you used | Use instead |
|---|---|
The Workflow-run helper (WorkflowRunOperation, NewWorkflowRunOperation, @workflow_run_operation) | TemporalOperationHandler with a Workflow backing |
The synchronous handler (OperationHandler.sync, nexus.NewSyncOperation, @sync_operation) | TemporalOperationHandler returning a sync result |
| A Workflow wrapping a single Activity | TemporalOperationHandler with an Activity backing |
| A Temporal Client fetched inside a handler | The Client injected into the start handler |
Two things improve when you migrate. Messages and Executions get bidirectional linking, which hand-fetched Clients do not produce. And one handler type covers every case, so an Operation can change what backs it without changing shape.
Migrating a Workflow-backed Operation
The earlier helper reached the Client through the Operation context and returned a Workflow handle or method reference, rather than being handed a Client and returning an Operation result:
@nexus.workflow_run_operation
async def start_greeting(
self, ctx: nexus.WorkflowRunOperationContext, input: GreetingInput
) -> nexus.WorkflowHandle[GreetingOutput]:
return await ctx.start_workflow(
GreetingWorkflow.run, input, id=f"greeting-{input.name}"
)
Replace it with Back an Operation with a Workflow.
Migrating a synchronous Operation
A messaging Operation used to be a synchronous handler that fetched its own Client, which is why those messages produced no links:
@nexusrpc.handler.sync_operation
async def cancel_order(
self, ctx: nexusrpc.handler.StartOperationContext, input: CancelOrderInput
) -> None:
await nexus.client().get_workflow_handle(
f"order-{input.order_id}"
).signal("requestCancellation", input)
Replace it with Send a Signal from an Operation.
- Temporal Operation Handler for the conceptual model.
- Nexus Services and Nexus Operations for the underlying concepts.
- Nexus Client Code Generator to generate Service contracts and typed models from one schema.
- Activity-backed Nexus Operations for Activity-backed Operations.
- Bidirectional linking for what the Nexus-aware Client gives you.
- Python Nexus feature guide