diff --git a/SKILL.md b/SKILL.md index b8fe682..e96a777 100644 --- a/SKILL.md +++ b/SKILL.md @@ -76,6 +76,8 @@ Check if `temporal` CLI is installed. If not, follow the instructions at `refere - **`references/core/dev-management.md`** - Dev cycle & management of server and workers - **`references/core/ai-patterns.md`** - AI/LLM pattern concepts - Language-specific info at `references/{your_language}/ai-patterns.md`, if available. Currently Python only. +- **`references/core/nexus.md`** - Temporal Nexus: connecting Temporal Applications across Namespaces via typed Service contracts and Endpoints + - Language-specific info at `references/{your_language}/nexus.md` ## Task Queue Priority and Fairness diff --git a/references/core/nexus.md b/references/core/nexus.md new file mode 100644 index 0000000..6df4574 --- /dev/null +++ b/references/core/nexus.md @@ -0,0 +1,176 @@ +# Temporal Nexus + +This document is the cross-language conceptual reference for Temporal Nexus. After reading it, see `references/{your_language}/nexus.md` for SDK-specific APIs. + +## Overview + +Nexus connects Temporal Applications across (and within) isolated Namespaces through typed Service contracts and a managed reverse-proxy Endpoint. Each team owns its own Namespace for security and fault isolation and exposes only a stable contract via a Nexus Endpoint. Nexus is peer-to-peer, not hierarchical: caller and handler Workflows are siblings communicating across Namespace boundaries. The Nexus platform is Generally Available for Temporal Cloud and self-hosted deployments. + +## When to use Nexus + +- Cross-team or cross-Namespace orchestration where caller and handler are owned and deployed independently. +- Exposing reusable functionality behind a stable Service contract so callers do not depend on internal Workflow IDs, Signals, Queries, or Task Queues. +- Composing functionality across multiple Services and teams via multi-level calls (Workflow A -> Nexus Op -> Workflow B -> Nexus Op -> Workflow C). +- Connecting Namespaces across regions or clouds without requiring direct connectivity or shared configuration. + +## Core vocabulary + +- **Nexus Service**: A named collection of Nexus Operations exposed as a contract for sharing across team boundaries. Multiple Services can run in the same Worker. +- **Nexus Operation**: A unit of work within a Service; can be synchronous or asynchronous, with an Operation token used to re-attach to long-running asynchronous Operations. +- **Nexus Endpoint**: A fully managed reverse proxy that routes requests from a caller Workflow to a single target Namespace and Task Queue. Callers only know the Endpoint name; the target Namespace, Task Queue, and implementation are encapsulated. +- **Nexus Registry**: The catalog that manages Endpoints; in Temporal Cloud it is global across an Account, in self-hosted deployments it is scoped to a Cluster. +- **Nexus Machinery**: The built-in delivery machinery that handles at-least-once execution, automatic retries, rate limiting, concurrency limiting, circuit breaking, and load balancing. +- **Nexus Task**: The task type handler Workers poll from the Endpoint's target Task Queue to process Nexus Operation requests. + +## Operation lifecycle modes + +Operations are defined using SDK builder functions: **New-Workflow-Run-Operation** for asynchronous Operations (starts a Workflow) and **New-Sync-Operation** for synchronous Operations (invokes a Query/Signal/Update or runs other reliable code via the SDK Client). + +### Synchronous + +Synchronous Operations must complete within the 10-second handler deadline, measured from the caller's Nexus Machinery. They complete as part of the start request, so they do **not** have a `NexusOperationStarted` event in the caller's history. Canonical caller-side event sequence: + +1. `ScheduleNexusOperation` command issued by the caller Worker. +2. `NexusOperationScheduled` event recorded. +3. Handler processes the request via New-Sync-Operation and responds with the result. +4. `NexusOperationCompleted` or `NexusOperationFailed` event recorded. + +For longer work, use New-Workflow-Run-Operation. + +### Asynchronous + +Asynchronous Operations start a Workflow and can run up to 60 days (the maximum Schedule-to-Close in Temporal Cloud). Canonical caller-side event sequence: + +1. `ScheduleNexusOperation` command issued by the caller Worker. +2. `NexusOperationScheduled` event recorded. +3. Handler processes the request via New-Workflow-Run-Operation and responds with the start Operation response. +4. `NexusOperationStarted` event recorded. +5. Handler Workflow completes and a Nexus completion Callback is delivered to the caller's Nexus Machinery. +6. `NexusOperationCompleted` or `NexusOperationFailed` event recorded. + +Terminal events on the caller side are one of: `NexusOperationStarted`, `NexusOperationCompleted`, `NexusOperationFailed`, `NexusOperationCanceled`, or `NexusOperationTimedOut`. + +## The three timeouts + +Set timeouts on the caller when scheduling the Operation. + +- **Schedule-to-Close**: Total end-to-end cap from schedule to completion. The Nexus Machinery automatically retries internally until this timeout expires, at which point the Operation fails with a `NexusOperationTimedOut` event. Maximum in Temporal Cloud is 60 days. +- **Schedule-to-Start**: How long the caller will wait for the Operation to be started (or completed, for sync). Fails with `TIMEOUT_TYPE_SCHEDULE_TO_START`. No enforcement if zero/unset. Requires Temporal Server 1.31.0 or later. +- **Start-to-Close**: How long the caller will wait after an asynchronous Operation has started. Fails with `TIMEOUT_TYPE_START_TO_CLOSE`. **Applies only to asynchronous Operations; synchronous Operations ignore this timeout.** No enforcement if zero/unset. Requires Temporal Server 1.31.0 or later. + +## Automatic retries and circuit breaking + +The Nexus Machinery retries on retryable Nexus errors and upstream timeouts up to the default Retry Policy's max attempts and expiration interval, until Schedule-to-Start or Schedule-to-Close is exceeded. To stop retries, the handler returns a non-retryable Nexus error. + +Circuit breaking is per caller-Namespace/Endpoint destination pair; each pair trips and resets independently. The breaker trips by default after **5 consecutive retryable errors**, opens for **60 seconds**, then transitions to half-open and allows a single probe request; success returns it to closed, failure reopens for another 60 seconds. Consecutive request timeouts (e.g., no Workers polling the handler Task Queue) count as retryable errors and trip the breaker. Different Operations within the same destination pair share the trip count, so a single Operation may have fewer than 5 attempts when the breaker opens. + +Circuit breaker state surfaces in Pending Nexus Operations and Pending Callbacks; when open, pending Operations show `State: Blocked` with a `BlockedReason: The circuit breaker is open.` + +## Execution semantics and idempotency + +The Nexus Machinery provides **at-least-once** execution: handlers may be invoked multiple times for the same Operation until Schedule-to-Close expires. Handlers should be idempotent (highly recommended, similar to Activities). To upgrade to exactly-once, back the Operation with a Workflow that uses a `WorkflowIDReusePolicy` of `RejectDuplicates`, which permits only one Execution per Workflow ID within a Namespace for the Retention Period. + +## Cancellation vs termination + +- **Cancellation**: Cancelling a caller Workflow automatically propagates to all pending Nexus Operations and their underlying handler Workflows; a canceled handler Workflow reports a Canceled Failure to the caller. +- **Termination**: Terminating a caller Workflow **abandons** all pending Nexus Operations; no cancel request is sent to the handler Namespace, so handler Workflows keep running until they time out or are manually stopped. Termination also prevents compensation logic from running. **Prefer cancellation over termination.** + +## Attaching multiple callers to a handler Workflow + +Operations started with New-Workflow-Run-Operation automatically attach a completion Callback to the handler Workflow. Additional callers can attach to the same handler Workflow using a Workflow-ID-Conflict-Policy of `Use-Existing`. Each handler Workflow has a per-Workflow Callback limit (2000 total Callbacks per Workflow Execution in Temporal Cloud); callers that exceed the limit receive an error. A single Workflow Execution can have a maximum of 30 in-flight Nexus Operations. When a handler Workflow uses Continue-As-New, existing completion Callbacks are copied to the new Execution; the previous Execution's Callbacks remain in `Standby` state indefinitely. + +## Errors + +By default, handler errors are retryable unless they are one of the following: + +- Application Failures explicitly marked as non-retryable. +- Nexus Operation errors that resolve the Operation as failed or canceled. +- Non-retryable Nexus errors. + +When the caller's Nexus Machinery receives an error: + +- **Non-retryable** -> `NexusOperationFailed` event is added to the caller's history. +- **Retryable** -> automatically retried; surfaces in Pending Operations. + +Caller-side error shape: a Nexus Operation Failure containing the operation name, token, and failure reason; the `cause` field indicates the type (for example, Application Error or Canceled Error). + +Observed handler error category strings in the encyclopedia include `INTERNAL` and `UPSTREAM_TIMEOUT`, surfaced as `handler error (CATEGORY): message` with `applicationFailureInfo.type: "NexusHandlerError"`. + +## Deployment patterns + +Two deployment patterns: + +- **Collocated (default)**: Operation handlers run in the same Worker and on the same Task Queue as the underlying Workflows; the Endpoint targets that Task Queue. Supports Eager Workflow Start when the handler starts a Workflow in the same Worker, executing the first Workflow Task locally while still recording durable state. Use by default. +- **Router-queue**: A dedicated Nexus Worker polls a "router" Task Queue and starts Workflows on different Task Queues in the same Namespace. Use when you need independent scaling of Nexus routing from Workflow execution, different IAM permissions per Worker fleet, or to add Nexus without modifying existing Workers. + +## Endpoints and Registry + +- One Endpoint targets one Namespace plus one Task Queue; the supported `EndpointSpec` target type is `Worker`. Endpoints are **not** general-purpose proxies and do not route to multiple backends. +- Multiple Endpoints can target different Task Queues in the same Namespace. +- Endpoint names must be unique within the Registry. Adding an Endpoint deploys it immediately for runtime use. +- Access is **deny by default**: the Access Policy is an explicit allowlist of caller Namespaces, and no callers are allowed by default even if in the same Namespace as the target. +- Everything except the Endpoint name can be edited; new Operations route to the updated target immediately. Changing the target Namespace is permitted but: in-flight async completion callbacks still point to the original handler Namespace, Cancel requests route to the new target, and Workflow ID uniqueness is per-Namespace (Signal-With-Start can create duplicates in the new target). **Drain existing Operations before changing the target Namespace.** +- The Registry is global across the Account in Temporal Cloud, Cluster-scoped self-hosted. +- Manage via the Temporal UI, CLI, Terraform provider, or Cloud Ops API; the Operator API is available for self-hosted. + +### RBAC + +In Temporal Cloud the Registry enforces RBAC: viewing/searching Endpoints requires the Read-only role (or higher) at the Account level; managing Endpoints requires the Developer role (or higher) **plus** Namespace Admin on the target Namespace. Self-hosted deployments can implement a custom Authorizer plugin. + +## Security and payload encryption + +- Temporal Cloud has built-in mTLS for all cross-Namespace Nexus traffic (start, cancel, and completion callbacks) across cells and regions; self-hosted relies on Cluster security. +- Workers authenticate to their Namespace using mTLS or API key. +- On each Operation, Temporal Cloud verifies the caller's Namespace is in the Endpoint's allowlist before routing the request. +- Endpoints are only accessible from within a Temporal Cloud Account through the Temporal SDK and are not externally accessible. +- Nexus uses the **same Data Converter** as Workflows and Activities. A Codec used for encryption also encrypts Nexus payloads. Caller and handler Workers must have compatible Data Converters. The sender encrypts: the caller encrypts the input, the handler encrypts the result. + +Three approaches for cross-Namespace payload encryption: + +| Approach | When to pick | +|---|---| +| Same encryption key on both Namespaces. | Simplest; no additional configuration. | +| Per-Namespace key with the KMS key ID in payload metadata. | Each Namespace keeps its own key; the Codec Server needs KMS decrypt permissions for all relevant keys. | +| Wrapper types (for example, `EndpointValue`) for endpoint-specific encryption keys. | Teams that do not want to share Namespace encryption keys across teams. | + +Options 1 and 2 work with the standard Data Converter; option 3 is advanced. + +## Observability + +- `temporal workflow describe` surfaces **Pending Nexus Operations** with fields including `Endpoint`, `Service`, `Operation`, `OperationToken`, `State`, `Attempt`, `ScheduleToCloseTimeout`, `NextAttemptScheduleTime`, `LastAttemptCompleteTime`, `LastAttemptFailure`, and `BlockedReason`. +- Cancellation requests on async Operations surface the same pattern with `CancelationState`, `CancelationAttempt`, `CancelationRequestedTime`, `CancelationLastAttemptCompleteTime`, `CancelationLastAttemptFailure`, and `CancelationBlockedReason`. +- `temporal workflow describe` also lists **Pending Callbacks** (the async completion callbacks sent from handler Namespace to caller Namespace) with `URL`, `Trigger`, `State`, `Attempt`, and `RegistrationTime`. +- **Bi-directional links** automatically connect caller Nexus Operation events to the corresponding handler Workflow events (and back), wired by SDK builder functions like New-Workflow-Run-Operation. +- Tracing integrates with OpenTelemetry / OpenTracing via an interceptor on the Client or Worker; per-SDK samples exist. +- Metrics are available at three layers: SDK metrics from the Nexus Worker (including `nexus_poll_no_task`, `nexus_task_schedule_to_start_latency`, `nexus_task_execution_failed`, `nexus_task_execution_latency`, `nexus_task_endtoend_latency`), Temporal Cloud metrics (`RespondWorkflowTaskCompleted`, `PollNexusTaskQueue`, `RespondNexusTaskCompleted`, `RespondNexusTaskFailed`), and OSS Cluster metrics (History Service, Concurrency Limiter, Frontend Service). + +## Limits (Temporal Cloud) + +- Nexus requests count toward the Namespace RPS limit on both caller and target Namespaces. +- 100 Endpoints per Account by default (can be raised via support ticket). +- 30 in-flight Nexus Operations per Workflow Execution. +- 2000 total Callbacks per Workflow Execution (governs how many Nexus callers can attach to a handler Workflow). +- **Less than 10 seconds** maximum for a handler to process a single Nexus start or cancel request. Available handler time is often shorter because the deadline is measured from the calling History Service and the request must transit matching. On timeout, the handler receives a context-deadline-exceeded error and the caller retries with exponential backoff until Schedule-to-Close. +- **60-day** maximum Schedule-to-Close for any Nexus Operation; the caller may configure shorter but the server caps at 60 days. + +## CLI surfaces + +Use the following groups; the orchestrator's `skill-temporal-cli` covers each subcommand in depth. + +- `temporal operator nexus endpoint ...` for self-hosted deployments. +- `tcld nexus endpoint ...` for Temporal Cloud. +- `temporal workflow describe` surfaces Pending Nexus Operations and Pending Callbacks. + +## Versioning + +Task Routing is the simplest way to version Nexus Service code; for backward-incompatible changes, use a different Service name and Task Queue (for example, `prod.payments.v2`) and let callers migrate on their own schedule. + +## Per-language references + +For SDK-specific APIs, types, and code samples, see: + +- `references/python/nexus.md` +- `references/typescript/nexus.md` +- `references/go/nexus.md` +- `references/java/nexus.md` +- `references/dotnet/nexus.md` diff --git a/references/dotnet/dotnet.md b/references/dotnet/dotnet.md index a7f1c54..f1b9ac4 100644 --- a/references/dotnet/dotnet.md +++ b/references/dotnet/dotnet.md @@ -200,3 +200,4 @@ See `references/dotnet/testing.md` for info on writing tests. - **`references/dotnet/data-handling.md`** — Data converters, payload encryption, etc. - **`references/dotnet/versioning.md`** — Patching API, workflow type versioning, Worker Versioning - **`references/dotnet/determinism-protection.md`** — Runtime task detection, .NET Task determinism rules +- **`references/dotnet/nexus.md`** — .NET SDK Nexus APIs (Public Preview): `[NexusService]`/`[NexusServiceHandler]`, `OperationHandler.Sync`, `WorkflowRunOperationHandler.FromHandleFactory`, `Workflow.CreateNexusWorkflowClient`, cancellation diff --git a/references/dotnet/nexus.md b/references/dotnet/nexus.md new file mode 100644 index 0000000..db8c274 --- /dev/null +++ b/references/dotnet/nexus.md @@ -0,0 +1,221 @@ +# .NET SDK Nexus Reference + +Use Temporal Nexus from the .NET SDK to connect Temporal Applications within and across Namespaces using a Nexus Endpoint, a Nexus Service contract, and Nexus Operations. + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +Concept-level explanations (Endpoints, Services, Operations, sync vs async, cross-Namespace semantics) live in `references/core/nexus.md`. This file documents .NET-specific identifiers, APIs, and idioms only. + +## Prerequisites + +- Temporal CLI `v1.3.0` or higher. +- Temporal .NET SDK `v1.9.0` or higher. +- NuGet packages: `Temporalio` (workflows, worker, client, `Temporalio.Nexus` utilities) and `NexusRpc` (Service contract attributes and handler types). +- Start the dev Server with `temporal server start-dev`. +- Create caller and handler Namespaces with `temporal operator namespace create --namespace `. +- Create the Endpoint with `temporal operator nexus endpoint create --name --target-namespace --target-task-queue `. + +## Define the Service contract + +A Nexus Service contract is an interface annotated with `[NexusService]`. Each operation is a method annotated with `[NexusOperation]`. Inputs and outputs are `record` types (or POCOs); nested types may live inside the interface. + +```csharp +using NexusRpc; + +[NexusService] +public interface IHelloService +{ + static readonly string EndpointName = "nexus-simple-endpoint"; + + [NexusOperation] + EchoOutput Echo(EchoInput input); + + [NexusOperation] + HelloOutput SayHello(HelloInput input); + + public record EchoInput(string Message); + + public record EchoOutput(string Message); + + public record HelloInput(string Name, HelloLanguage Language); + + public record HelloOutput(string Message); + + public enum HelloLanguage { En, Fr, De, Es, Tr } +} +``` + +A common idiom is a `static readonly string EndpointName` on the interface, shared between caller and handler so the Endpoint name is defined once. + +## Implement Operation handlers + +The handler class is annotated with `[NexusServiceHandler(typeof(IService))]`. Each handler method is annotated with `[NexusOperationHandler]` and returns `IOperationHandler` — that is, a factory that returns the operation runner, not the operation result. + +The `Temporalio.Nexus` namespace provides two builder utilities: + +- `NexusOperationExecutionContext.Current.TemporalClient` — get the Temporal Client the Worker was initialized with, for sync handlers backed by Signals, Queries, or Updates. +- `WorkflowRunOperationHandler.FromHandleFactory` — run a Workflow as an asynchronous Nexus Operation. + +### Synchronous handler + +Build a sync handler with `OperationHandler.Sync((ctx, input) => …)` — note the capital `S`. + +```csharp +using NexusRpc.Handlers; + +[NexusServiceHandler(typeof(IHelloService))] +public class HelloService +{ + [NexusOperationHandler] + public IOperationHandler Echo() => + OperationHandler.Sync( + (ctx, input) => new(input.Message)); +} +``` + +### Synchronous handler that calls Temporal + +Use `NexusOperationExecutionContext.Current.TemporalClient` inside a sync handler to Signal, Query, or Update an existing Workflow. All calls must complete within the Nexus request timeout. + +```csharp +[NexusOperationHandler] +public IOperationHandler GetLanguages() => + OperationHandler.Sync( + async (ctx, input) => + { + var client = NexusOperationExecutionContext.Current.TemporalClient; + var handle = client.GetWorkflowHandle(WorkflowIdForUser(input.UserId)); + return await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported)); + }); +``` + +Signal-With-Start and Update-With-Start are also supported from a sync handler. + +### Asynchronous (Workflow-run) handler + +Build an async, Workflow-backed handler with `WorkflowRunOperationHandler.FromHandleFactory`. The factory receives a `WorkflowRunOperationContext` and the operation input; call `context.StartWorkflowAsync(...)` and pass `new() { Id = context.HandlerContext.RequestId }` so retries dedupe to the same Workflow ID. + +```csharp +using NexusRpc.Handlers; +using Temporalio.Nexus; + +[NexusServiceHandler(typeof(IHelloService))] +public class HelloService +{ + [NexusOperationHandler] + public IOperationHandler SayHello() => + WorkflowRunOperationHandler.FromHandleFactory( + (WorkflowRunOperationContext context, IHelloService.HelloInput input) => + context.StartWorkflowAsync( + (HelloHandlerWorkflow wf) => wf.RunAsync(input), + new() { Id = context.HandlerContext.RequestId })); +} +``` + +Prefer business-meaningful Workflow IDs in production — typically derived from the operation input. `context.HandlerContext.RequestId` is shown for examples because it is stable across retries of a single operation. + +### Mapping one Nexus input to multiple Workflow arguments + +A Nexus Operation has a single input parameter. To start a Workflow that takes multiple arguments, deconstruct the Nexus input in the call to `RunAsync`. + +```csharp +[NexusOperationHandler] +public IOperationHandler SayHello() => + WorkflowRunOperationHandler.FromHandleFactory( + (WorkflowRunOperationContext context, IHelloService.HelloInput input) => + context.StartWorkflowAsync( + (HelloHandlerWorkflow wf) => wf.RunAsync(input.Language, input.Name), + new() { Id = context.HandlerContext.RequestId })); +``` + +## Register handlers on a Worker + +Register a Nexus Service handler instance with `.AddNexusService(new MyService())` on `TemporalWorkerOptions`, alongside `.AddWorkflow()` and any activity registrations. The handler Worker's Task Queue must match `--target-task-queue` on the Endpoint. + +```csharp +using var worker = new TemporalWorker( + await ConnectClientAsync("nexus-simple-handler-namespace"), + new TemporalWorkerOptions(taskQueue: "nexus-simple-handler-sample"). + AddNexusService(new HelloService()). + AddWorkflow()); + +await worker.ExecuteAsync(tokenSource.Token); +``` + +`.AddNexusService(...)` takes a concrete handler instance, the same way `.AddActivity(...)` takes an activity instance. + +## Caller Workflow + +Inside a Workflow, create a typed Nexus client with `Workflow.CreateNexusWorkflowClient(endpointName)` and invoke operations through the proxy with `.ExecuteNexusOperationAsync(svc => svc.OperationName(input))`. + +```csharp +using Temporalio.Workflows; + +[Workflow] +public class HelloCallerWorkflow +{ + [WorkflowRun] + public async Task RunAsync(string name, IHelloService.HelloLanguage language) + { + var output = await Workflow.CreateNexusWorkflowClient(IHelloService.EndpointName). + ExecuteNexusOperationAsync(svc => svc.SayHello(new(name, language))); + return output.Message; + } +} +``` + +The caller depends only on the Service interface, not the handler implementation, which is what allows caller and handler to live in different Namespaces or different codebases. + +The caller Worker is registered as usual; it only needs `.AddWorkflow()` and does not register the Nexus Service. + +## Setting Operation timeouts + +Pass a `NexusWorkflowOperationOptions` instance as the second argument to `ExecuteNexusOperationAsync`. Three timeout properties are available: `ScheduleToCloseTimeout`, `ScheduleToStartTimeout`, and `StartToCloseTimeout`. `StartToCloseTimeout` applies only to asynchronous Operations. + +```csharp +var output = await Workflow.CreateNexusWorkflowClient(IHelloService.EndpointName). + ExecuteNexusOperationAsync( + svc => svc.SayHello(new(name, language)), + new NexusWorkflowOperationOptions + { + ScheduleToCloseTimeout = TimeSpan.FromMinutes(10), + ScheduleToStartTimeout = TimeSpan.FromMinutes(2), + StartToCloseTimeout = TimeSpan.FromMinutes(5), + }); +``` + +The Nexus Machinery automatically retries failed requests until `ScheduleToCloseTimeout` is exceeded. If `ScheduleToStartTimeout` or `StartToCloseTimeout` is not set, no such timeout is enforced. + +## Cancellation + +To cancel a Nexus Operation from a caller Workflow, cancel the cancellation token passed to the operation call. Only asynchronous operations can be canceled, since cancellation is delivered via the operation token. The Workflow or other resource backing the operation may ignore the cancellation request. + +Set the cancellation type via `CancellationType` on `NexusWorkflowOperationOptions`. The four values are: + +- `Abandon` — do not request cancellation of the operation. +- `TryCancel` — initiate a cancellation request and immediately report cancellation to the caller. Does not guarantee delivery if the caller exits first. +- `WaitCancellationRequested` — request cancellation and wait for confirmation the request was received; does not wait for actual cancellation. +- `WaitCancellationCompleted` — wait for operation completion. Operation may or may not complete as cancelled. This is the default. + +To ensure cancellations are delivered, wait for all pending operations to finish before exiting the caller Workflow. Once the caller Workflow completes, the caller's Nexus Machinery will not make further cancellation attempts. + +## Observability + +Caller Workflow history events: + +- Synchronous Operations: `NexusOperationScheduled`, `NexusOperationCompleted`. `NexusOperationStarted` is not emitted for sync operations. +- Asynchronous Operations: `NexusOperationScheduled`, `NexusOperationStarted`, `NexusOperationCompleted`. + +Use `temporal workflow describe -w ` to show pending Nexus Operations and attached callbacks; use `temporal workflow show -w ` to see Nexus events in the caller's history. + +## Common mistakes + +1. Writing `OperationHandler.sync` (lowercase). In .NET it is `OperationHandler.Sync(...)` with a capital `S`. +2. Omitting `[NexusServiceHandler(typeof(IService))]` on the handler implementation class. The attribute links the handler class to its Service interface. +3. Returning an `O` (the output type) directly from a `[NexusOperationHandler]` method. The method must return `IOperationHandler` produced by `OperationHandler.Sync(...)` or `WorkflowRunOperationHandler.FromHandleFactory(...)`. +4. Forgetting `.AddNexusService(new MyService())` on `TemporalWorkerOptions`. A Worker only handles incoming Nexus requests when its Nexus Service handlers are registered. +5. Forgetting the endpoint name argument to `Workflow.CreateNexusWorkflowClient(endpointName)`. Standard practice is to expose it as a `static readonly string EndpointName` on the Service interface and pass `IService.EndpointName`. +6. Putting `[NexusOperation]` on handler methods. `[NexusOperation]` belongs on the **interface** methods; the **implementation** uses `[NexusOperationHandler]`. +7. Mixing up the client factory name. It is `Workflow.CreateNexusWorkflowClient(endpointName)` — not `CreateNexusClient`. +8. Using `NexusOperationOptions` instead of `NexusWorkflowOperationOptions` when setting timeouts or `CancellationType` from a caller Workflow. diff --git a/references/go/go.md b/references/go/go.md index 6c42bed..e73df91 100644 --- a/references/go/go.md +++ b/references/go/go.md @@ -252,3 +252,4 @@ See `references/go/testing.md` for info on writing tests. - **`references/go/data-handling.md`** - Data converters, payload codecs, encryption - **`references/go/versioning.md`** - Patching API (`workflow.GetVersion`), Worker Versioning - **`references/go/determinism-protection.md`** - Information on **`workflowcheck`** tool to help statically check for determinism issues. +- **`references/go/nexus.md`** - Go SDK Nexus APIs: `nexus.NewSyncOperation`, `temporalnexus.NewWorkflowRunOperation`, `workflow.NewNexusClient`, timeouts, cancellation diff --git a/references/go/nexus.md b/references/go/nexus.md new file mode 100644 index 0000000..7dd6e94 --- /dev/null +++ b/references/go/nexus.md @@ -0,0 +1,281 @@ +# Go SDK Nexus Reference + +## Overview + +Temporal Go SDK support for Nexus is Generally Available. + +This file documents Go SDK identifiers, APIs, and idioms for Nexus. Concept-level material (Endpoints, Services, Operations, sync vs. async, timeout semantics, error categories) lives in `references/core/nexus.md` and is not redefined here. + +## Prerequisites + +- Temporal CLI v1.3.0 or higher. +- Temporal Go SDK v1.33.0 or higher. +- Run the dev server with `temporal server start-dev` (Nexus enabled). + +Go modules involved: + +- `go.temporal.io/sdk/client` +- `go.temporal.io/sdk/temporalnexus` +- `go.temporal.io/sdk/worker` +- `go.temporal.io/sdk/workflow` +- `github.com/nexus-rpc/sdk-go/nexus` + +Create a Nexus Endpoint to route requests from a caller Namespace to a handler Namespace and Task Queue: + +``` +temporal operator nexus endpoint create \ + --name my-nexus-endpoint-name \ + --target-namespace my-target-namespace \ + --target-task-queue my-handler-task-queue +``` + +## Define the Service contract + +Go has no decorator-based Service contract. Use string constants for the Service and Operation names plus Go types for inputs and outputs, typically in a shared package imported by both caller and handler. + +```go +package service + +const HelloServiceName = "my-hello-service" +const EchoOperationName = "echo" + +type EchoInput struct { + Message string +} + +type EchoOutput EchoInput +``` + +## Implement Operation handlers + +The `temporalnexus` package provides builders for Operation handlers. `NewWorkflowRunOperation` runs a Workflow as an asynchronous Nexus Operation. `GetClient` returns the Temporal Client that the Worker was initialized with for use in synchronous handlers. + +### Synchronous handler + +Use `nexus.NewSyncOperation` for simple RPC handlers. The handler receives `ctx context.Context`, the typed input, and `nexus.StartOperationOptions`, and returns the typed output and an error. + +```go +import ( + "context" + + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/sdk/temporalnexus" +) + +var EchoOperation = nexus.NewSyncOperation( + service.EchoOperationName, + func(ctx context.Context, input service.EchoInput, options nexus.StartOperationOptions) (service.EchoOutput, error) { + return service.EchoOutput(input), nil + }, +) +``` + +The handler `ctx` is set with the Nexus request deadline; pass it directly to Temporal Client calls to propagate the timeout. Handlers should be reliable since the circuit breaker trips after 5 consecutive retryable errors, blocking all Operations from the caller to that Endpoint. + +### Synchronous handler that uses the Temporal Client + +Call `temporalnexus.GetClient(ctx)` inside a sync handler to get the Worker's Temporal Client. Use it to Signal, Query, Update, Signal-With-Start, or Update-With-Start a Workflow. + +```go +var GetLanguagesOperation = nexus.NewSyncOperation( + service.GetLanguagesOperationName, + func(ctx context.Context, input service.GetLanguagesInput, options nexus.StartOperationOptions) (service.GetLanguagesOutput, error) { + c := temporalnexus.GetClient(ctx) + workflowID := GetWorkflowID(input.UserID) + // ... use c to signal/query/update ... + }, +) +``` + +All calls inside a sync handler must complete within the Nexus request timeout. Updates should be short-lived to stay within this deadline. + +### Asynchronous (Workflow-run) handler + +Use `temporalnexus.NewWorkflowRunOperation(name, workflow, optionsFn)` to expose a Workflow as an asynchronous Operation. The `optionsFn` returns `client.StartWorkflowOptions`. Use `options.RequestID` as the Workflow ID for stability across retries of the Operation. + +```go +import ( + "context" + + "github.com/nexus-rpc/sdk-go/nexus" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporalnexus" +) + +var HelloOperation = temporalnexus.NewWorkflowRunOperation( + service.HelloOperationName, + HelloHandlerWorkflow, + func(ctx context.Context, input service.HelloInput, options nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ + ID: options.RequestID, + }, nil + }, +) +``` + +If `TaskQueue` is omitted from `client.StartWorkflowOptions`, the handler Workflow runs on the same Task Queue as the Operation handler. + +### Multi-argument handler Workflows + +A Nexus Operation takes exactly one input. To start a Workflow that takes multiple arguments, use `temporalnexus.NewWorkflowRunOperationWithOptions` or `temporalnexus.MustNewWorkflowRunOperationWithOptions` with `temporalnexus.WorkflowRunOperationOptions[I, O]`, and call `temporalnexus.ExecuteUntypedWorkflow[O](ctx, options, startOpts, workflow, args…)` inside the handler. + +```go +var HelloOperation = temporalnexus.MustNewWorkflowRunOperationWithOptions( + temporalnexus.WorkflowRunOperationOptions[service.HelloInput, service.HelloOutput]{ + Name: service.HelloOperationName, + Handler: func(ctx context.Context, input service.HelloInput, options nexus.StartOperationOptions) (temporalnexus.WorkflowHandle[service.HelloOutput], error) { + return temporalnexus.ExecuteUntypedWorkflow[service.HelloOutput]( + ctx, + options, + client.StartWorkflowOptions{ + ID: options.RequestID, + }, + HelloHandlerWorkflow, + input.Name, + input.Language, + ) + }, + }, +) +``` + +The generic type parameters `[I, O]` on `WorkflowRunOperationOptions` are the Operation input and output types, not the Workflow's argument types. + +## Register the Nexus Service in a Worker + +Build a `nexus.Service` with `nexus.NewService(name)`, register Operation handlers on it with `service.Register(ops…)`, then attach the Service to the Worker with `w.RegisterNexusService(service)`. Also register any handler Workflow with `w.RegisterWorkflow` so the same Worker can execute it. + +```go +c, err := client.Dial(client.Options{ /* ... */ }) +if err != nil { + log.Fatalln("Unable to create client", err) +} +defer c.Close() + +w := worker.New(c, "my-handler-task-queue", worker.Options{}) + +service := nexus.NewService(service.HelloServiceName) +err = service.Register(handler.EchoOperation, handler.HelloOperation) +if err != nil { + log.Fatalln("Unable to register operations", err) +} +w.RegisterNexusService(service) +w.RegisterWorkflow(handler.HelloHandlerWorkflow) + +if err := w.Run(worker.InterruptCh()); err != nil { + log.Fatalln("Unable to start worker", err) +} +``` + +A Worker will only poll for and process incoming Nexus requests if the Nexus Service Handlers are registered. + +## Caller Workflow + +Inside a Workflow function, call `workflow.NewNexusClient(endpointName, serviceName)` to get a client, then `c.ExecuteOperation(ctx, operationName, input, workflow.NexusOperationOptions{})` to schedule the Operation. The return value is a future. + +```go +package caller + +import ( + "github.com/temporalio/samples-go/nexus/service" + "go.temporal.io/sdk/workflow" +) + +const ( + TaskQueue = "my-caller-workflow-task-queue" + endpointName = "my-nexus-endpoint-name" +) + +func EchoCallerWorkflow(ctx workflow.Context, message string) (string, error) { + c := workflow.NewNexusClient(endpointName, service.HelloServiceName) + + fut := c.ExecuteOperation(ctx, service.EchoOperationName, service.EchoInput{Message: message}, workflow.NexusOperationOptions{}) + + var res service.EchoOutput + if err := fut.Get(ctx, &res); err != nil { + return "", err + } + return res.Message, nil +} +``` + +`fut.Get(ctx, &result)` blocks the Workflow until the Operation completes and decodes the result. + +### Wait for Operation start and read the token + +`fut.GetNexusOperationExecution()` returns a secondary future that resolves once the Operation has started. It yields a `workflow.NexusOperationExecution` whose `OperationToken` field is the handle used for additional actions such as cancelation on asynchronous Operations. + +```go +fut := c.ExecuteOperation(ctx, service.HelloOperationName, service.HelloInput{Name: name, Language: language}, workflow.NexusOperationOptions{}) + +var exec workflow.NexusOperationExecution +if err := fut.GetNexusOperationExecution().Get(ctx, &exec); err != nil { + return "", err +} + +var res service.HelloOutput +if err := fut.Get(ctx, &res); err != nil { + return "", err +} +``` + +Register the caller Workflow on its own Worker with `w.RegisterWorkflow(caller.EchoCallerWorkflow)`. The caller Worker runs in the caller Namespace; the handler Worker runs in the target Namespace. + +## Operation timeouts + +Set timeouts on `workflow.NexusOperationOptions` when calling `ExecuteOperation`. See `references/core/nexus.md` for the lifecycle semantics of each. + +```go +fut := c.ExecuteOperation(ctx, opName, input, workflow.NexusOperationOptions{ + ScheduleToCloseTimeout: 10 * time.Minute, + ScheduleToStartTimeout: 2 * time.Minute, + StartToCloseTimeout: 5 * time.Minute, +}) +``` + +- `ScheduleToCloseTimeout` limits the total duration from scheduling to completion. The Nexus Machinery automatically retries failed requests until this timeout is exceeded. +- `ScheduleToStartTimeout` limits how long the caller will wait for the Operation to be started by the handler; if not set, no Schedule-to-Start timeout is enforced. +- `StartToCloseTimeout` limits how long the caller will wait for an asynchronous Operation to complete after it has been started; only applies to asynchronous Operations; if not set, no Start-to-Close timeout is enforced. + +## Cancellation + +To cancel a Nexus Operation from within a Workflow, create a cancellable context with `workflow.WithCancel`. This returns a new context and a function that, when called, cancels the context and any SDK method that was passed it, including the Nexus Operation future. The future is resolved when the Operation finishes, whether it succeeds, fails, times out, or is canceled. + +```go +cancelCtx, cancel := workflow.WithCancel(ctx) +fut := c.ExecuteOperation(cancelCtx, opName, input, workflow.NexusOperationOptions{}) +// ... later ... +cancel() +``` + +Only asynchronous Operations can be canceled in Nexus, as cancelation is sent using an operation token. The Workflow or other resources backing the Operation may choose to ignore the cancelation request; if ignored, the Operation may enter a terminal state. + +Once the caller Workflow completes, the caller's Nexus Machinery will not make any further attempts to cancel Operations that are still running. To ensure cancelations are delivered, wait for all pending Operations to finish before exiting the Workflow. + +## Errors + +`fut.Get(ctx, &result)` returns a non-nil error if the Operation fails, is canceled, or exceeds a configured timeout. See `references/core/nexus.md` for the Nexus error categories and retry semantics. The future returned by `NexusClient.ExecuteOperation` is resolved when the Operation finishes, whether it succeeds, fails, times out, or is canceled. + +Sync handlers feeding the circuit breaker should be reliable: 5 consecutive retryable errors trip the breaker for that Endpoint. + +## Observability + +For asynchronous Nexus Operations the caller's history records `NexusOperationScheduled`, `NexusOperationStarted`, and `NexusOperationCompleted`. For synchronous Operations the caller's history records only `NexusOperationScheduled` and `NexusOperationCompleted` — `NexusOperationStarted` is not reported. + +Use the Temporal CLI to inspect a caller Workflow: + +``` +temporal workflow describe -w +temporal workflow show -w +``` + +## Common mistakes + +1. Calling a non-existent `NewAsyncOperation` instead of `temporalnexus.NewWorkflowRunOperation` for async (Workflow-run) Operations. +2. Building the `nexus.Service` and registering Operations but forgetting `w.RegisterNexusService(service)`, so the Worker does not poll Nexus tasks. +3. Registering the Nexus Service on the handler Worker but forgetting `w.RegisterWorkflow(handler.HelloHandlerWorkflow)` for the Workflow that an async Operation starts. +4. Using `client.Dial` to open a new Client inside a sync handler instead of calling `temporalnexus.GetClient(ctx)` to reuse the Worker's existing Client. +5. Mismatching the generic parameters on `WorkflowRunOperationOptions[I, O]` — `I` and `O` are the Operation input and output types, and the handler must return `temporalnexus.WorkflowHandle[O]`. +6. Calling `c.ExecuteOperation` outside a Workflow function — `workflow.NewNexusClient` must be called with a Workflow `ctx`, and `ExecuteOperation` is a Workflow-side API. +7. Forgetting to pass `options.RequestID` (or another stable, business-meaningful ID) as the Workflow ID in the async handler's options function, defeating dedupe on Operation retries. +8. Letting the caller Workflow exit while async Operations are still running and expecting cancelations to be delivered — they will not be. diff --git a/references/java/integrations/spring-ai.md b/references/java/integrations/spring-ai.md index 5ee0704..ae5154f 100644 --- a/references/java/integrations/spring-ai.md +++ b/references/java/integrations/spring-ai.md @@ -217,7 +217,6 @@ Media image = new Media(MimeTypeUtils.IMAGE_PNG, URI.create("https://cdn.example For anything larger than a small thumbnail, route the bytes to a binary store from an Activity and pass only the URL across the conversation. - ## Vector stores, embeddings, and MCP When the corresponding Spring AI modules (`spring-ai-rag`, `spring-ai-mcp`) are on the classpath, the integration registers Activities for vector stores, embeddings, and MCP tool calls automatically. Inject the matching Spring AI types into your Activities or Workflows and use them as you would in any Spring AI application — each operation executes through a Temporal Activity. diff --git a/references/java/java.md b/references/java/java.md index 05e4f47..0c79fe4 100644 --- a/references/java/java.md +++ b/references/java/java.md @@ -263,6 +263,7 @@ See `references/java/testing.md` for info on writing tests. - **`references/java/advanced-features.md`** - Schedules, worker tuning, and more - **`references/java/data-handling.md`** - Data converters, Jackson, payload encryption - **`references/java/versioning.md`** - Patching API, workflow type versioning, Worker Versioning +- **`references/java/nexus.md`** - Java SDK Nexus APIs: `@Service`/`@ServiceImpl`, `OperationHandler.sync`, `WorkflowRunOperation.fromWorkflowMethod`, `Workflow.newNexusServiceStub`, cancellation ### Java Integrations diff --git a/references/java/nexus.md b/references/java/nexus.md new file mode 100644 index 0000000..e30ce0a --- /dev/null +++ b/references/java/nexus.md @@ -0,0 +1,267 @@ +# Java SDK Nexus Reference + +## Overview + +Concepts (Endpoint, Service contract, Operation, caller, handler) are defined in `references/core/nexus.md`; this file documents the Java-specific identifiers, annotations, and builders. Java SDK Nexus support is Generally Available. + +## Prerequisites + +- Temporal CLI `v1.3.0` or higher. +- Temporal Java SDK `v1.28.0` or higher. +- Same `io.temporal:temporal-sdk` Gradle/Maven coordinate as the rest of the Java SDK. Nexus annotations live in `io.nexusrpc.*`; Nexus runtime utilities live in `io.temporal.nexus.*`. + +Default Data Converter encodes payloads as Null, Byte array, Protobuf JSON, then JSON; this reference uses Java classes serialized to JSON. + +## Define the Service contract + +A Service contract is a Java interface annotated with `@Service`. Each Operation is an interface method annotated with `@Operation` that takes exactly one input parameter and returns one output type. Input and output types should be JSON-serializable; use Jackson `@JsonCreator` / `@JsonProperty` on nested DTO classes. + +```java +import io.nexusrpc.Operation; +import io.nexusrpc.Service; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +@Service +public interface SampleNexusService { + class HelloInput { + private final String name; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public HelloInput(@JsonProperty("name") String name) { + this.name = name; + } + + @JsonProperty("name") + public String getName() { return name; } + } + + class HelloOutput { + private final String message; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public HelloOutput(@JsonProperty("message") String message) { + this.message = message; + } + + @JsonProperty("message") + public String getMessage() { return message; } + } + + @Operation + HelloOutput hello(HelloInput input); +} +``` + +## Implement Operation handlers + +The implementation class is annotated `@ServiceImpl(service = SampleNexusService.class)`. Each method annotated `@OperationImpl` is a **factory** that returns an `OperationHandler` — it does not return the Operation's `Output` directly. + +### Synchronous handler + +Use `OperationHandler.sync` (lowercase `sync`) for simple RPC-style handlers. The lambda receives `(ctx, details, input)` and returns the output value directly. + +```java +import io.nexusrpc.handler.OperationHandler; +import io.nexusrpc.handler.OperationImpl; +import io.nexusrpc.handler.ServiceImpl; + +@ServiceImpl(service = SampleNexusService.class) +public class SampleNexusServiceImpl { + + @OperationImpl + public OperationHandler echo() { + return OperationHandler.sync( + (ctx, details, input) -> new SampleNexusService.EchoOutput(input.getMessage())); + } +} +``` + +### Synchronous handler with Signals, Queries, or Updates + +Inside a sync handler, obtain the Worker's Client via `Nexus.getOperationContext().getWorkflowClient()` and construct stubs for Signal / Query / Update calls. All calls must complete within the Nexus request timeout; Updates should be short-lived. + +```java +import io.temporal.nexus.Nexus; + +private GreetingWorkflow getWorkflowStub(String userId) { + return Nexus.getOperationContext() + .getWorkflowClient() + .newWorkflowStub(GreetingWorkflow.class, getWorkflowId(userId)); +} +``` + +### Asynchronous (Workflow-run) handler + +Use `WorkflowRunOperation.fromWorkflowMethod` to expose a Workflow as an async Operation when the Workflow takes one argument matching the Operation input. The lambda returns a method reference to the Workflow stub (`stub::method`). Default Task Queue is the queue this operation is handled on. Use `details.getRequestId()` for the Workflow Id when no business-meaningful Id is available — the request Id is stable across retries. + +```java +import io.temporal.client.WorkflowOptions; +import io.temporal.nexus.Nexus; +import io.temporal.nexus.WorkflowRunOperation; + +@OperationImpl +public OperationHandler hello() { + return WorkflowRunOperation.fromWorkflowMethod( + (ctx, details, input) -> + Nexus.getOperationContext() + .getWorkflowClient() + .newWorkflowStub( + HelloHandlerWorkflow.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::hello); +} +``` + +### Multi-argument handler Workflows + +When the handler Workflow takes multiple arguments, use `WorkflowRunOperation.fromWorkflowHandle` and `WorkflowHandle.fromWorkflowMethod(stub::method, arg1, arg2, ...)` to map the single Nexus input to multiple Workflow arguments. + +```java +import io.temporal.nexus.WorkflowRunOperation; +import io.temporal.nexus.WorkflowHandle; + +@OperationImpl +public OperationHandler hello() { + return WorkflowRunOperation.fromWorkflowHandle( + (ctx, details, input) -> + WorkflowHandle.fromWorkflowMethod( + Nexus.getOperationContext() + .getWorkflowClient() + .newWorkflowStub( + HelloHandlerWorkflow.class, + WorkflowOptions.newBuilder() + .setWorkflowId(details.getRequestId()) + .build()) + ::hello, + input.getName(), + input.getLanguage())); +} +``` + +## Register handlers on the handler Worker + +Register the Nexus Service implementation alongside any Workflow types it starts. + +```java +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; + +Worker worker = factory.newWorker("my-handler-task-queue"); +worker.registerWorkflowImplementationTypes(HelloHandlerWorkflowImpl.class); +worker.registerNexusServiceImplementation(new SampleNexusServiceImpl()); +factory.start(); +``` + +## Caller Workflow + +Inside a caller Workflow, create a Nexus Service stub via `Workflow.newNexusServiceStub(ServiceClass.class, NexusServiceOptions)`. Calling a method on the stub invokes the Operation and blocks until it completes. + +```java +import io.temporal.workflow.NexusOperationOptions; +import io.temporal.workflow.NexusServiceOptions; +import io.temporal.workflow.Workflow; +import java.time.Duration; + +public class EchoCallerWorkflowImpl implements EchoCallerWorkflow { + SampleNexusService sampleNexusService = + Workflow.newNexusServiceStub( + SampleNexusService.class, + NexusServiceOptions.newBuilder() + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()) + .build()); + + @Override + public String echo(String message) { + return sampleNexusService.echo(new SampleNexusService.EchoInput(message)).getMessage(); + } +} +``` + +For access to the started state or final Promise, use `Workflow.startNexusOperation(stub::method, input)` which returns `NexusOperationHandle`. Call `handle.getExecution().get()` to wait until the Operation is started (the `NexusOperationExecution` carries the Operation token for async Operations), and `handle.getResult().get()` to wait for the final result. + +```java +import io.temporal.workflow.NexusOperationHandle; + +@Override +public String hello(String message, SampleNexusService.Language language) { + NexusOperationHandle handle = + Workflow.startNexusOperation( + sampleNexusService::hello, new SampleNexusService.HelloInput(message, language)); + handle.getExecution().get(); + return handle.getResult().get().getMessage(); +} +``` + +## Bind the Service to an Endpoint on the caller Worker + +The Nexus stub cannot dispatch without an Endpoint binding. On the caller Worker, pass `WorkflowImplementationOptions` that map each Nexus Service class name to a `NexusServiceOptions` carrying the Endpoint name when registering Workflow types. + +```java +import io.temporal.worker.WorkflowImplementationOptions; +import io.temporal.workflow.NexusServiceOptions; +import java.util.Collections; + +worker.registerWorkflowImplementationTypes( + WorkflowImplementationOptions.newBuilder() + .setNexusServiceOptions( + Collections.singletonMap( + "SampleNexusService", + NexusServiceOptions.newBuilder().setEndpoint("my-nexus-endpoint-name").build())) + .build(), + EchoCallerWorkflowImpl.class, + HelloCallerWorkflowImpl.class); +``` + +The key in the map is the **Service class simple name** (e.g. `"SampleNexusService"`). + +## Operation timeouts + +Per-Operation timeouts are set on `NexusOperationOptions.Builder` and attached via `NexusServiceOptions.Builder.setOperationOptions(...)`. + +- `setScheduleToCloseTimeout(Duration)` — total time the caller will wait for the Operation. + +## Cancellation + +Cancellation of a Nexus Operation is scoped through `Workflow.newCancellationScope(Runnable)`. Any SDK methods (including Nexus Operations) started inside that `Runnable` are tied to the returned scope; calling `.cancel()` on the scope cancels them. The Promise returned by `Workflow.startNexusOperation` resolves when the Operation finishes — success, failure, timeout, or cancellation. + +Only asynchronous Operations can be canceled (cancellation is delivered via the Operation token); the handler Workflow may ignore the cancellation request. + +Cancellation types, set via `.setCancellationType(...)` on `NexusServiceOptions.Builder`: + +- `ABANDON` — do not request cancellation. +- `TRY_CANCEL` — send the cancellation request and report cancellation immediately to the caller. +- `WAIT_REQUESTED` — wait for confirmation that the cancellation request was received, not for actual cancellation. +- `WAIT_COMPLETED` — wait for the Operation to complete (it may or may not complete as canceled). Default. + +Once the caller Workflow completes, the caller stops attempting to cancel still-running Operations and lets them finish. To guarantee delivery, wait for pending cancellation requests before exiting the caller Workflow. + +## Observability + +Caller Workflow history events: + +- Synchronous Operations: `NexusOperationScheduled`, `NexusOperationCompleted`. +- Asynchronous Operations: `NexusOperationScheduled`, `NexusOperationStarted`, `NexusOperationCompleted`. + +`NexusOperationStarted` is not reported for synchronous Operations. + +Inspect pending Nexus Operations and attached handler callbacks with `temporal workflow describe -w `; full event history with `temporal workflow show -w `. + +## Reliability constraint + +The Nexus circuit breaker trips after 5 consecutive retryable errors and blocks all Operations from the caller to that Endpoint, so handlers must be reliable. + +## Common mistakes + +1. Writing `OperationHandler.Sync(...)` (capital S) — the Java factory is `OperationHandler.sync(...)`. +2. Omitting `@ServiceImpl(service = MyService.class)` on the implementation class — the impl is not discoverable as a Nexus Service without it. +3. Returning the Operation output directly from an `@OperationImpl` method. The method is a factory; it must return `OperationHandler`. +4. Calling `Workflow.newWorkflowStub(...)` inside an async handler to start the handler Workflow — start it through `WorkflowRunOperation.fromWorkflowMethod` or `WorkflowRunOperation.fromWorkflowHandle` so Nexus owns the Workflow start. +5. Forgetting to bind the Endpoint on the caller Worker via `WorkflowImplementationOptions.setNexusServiceOptions(...)` — the stub has no Endpoint and Operations cannot dispatch. +6. Confusing `@Operation` (on the Service interface method) with `@OperationImpl` (on the implementation factory method). +7. Using a non-stable Workflow Id for the handler Workflow. Use `details.getRequestId()` (stable across Operation retries) when no business-meaningful Id is available. diff --git a/references/python/nexus.md b/references/python/nexus.md new file mode 100644 index 0000000..c4a32d3 --- /dev/null +++ b/references/python/nexus.md @@ -0,0 +1,252 @@ +# Python SDK Nexus Reference + +## Overview + +This file documents the Python SDK APIs for Temporal Nexus: decorators, classes, methods, and Worker/caller wiring. For Nexus concepts (Endpoints, Services, Operations, lifecycle, cross-Namespace routing), see `references/core/nexus.md`. + +Temporal Python SDK support for Nexus is Generally Available. + +## Prerequisites + +- Temporal CLI `v1.3.0` or higher. +- Temporal Python SDK `v1.14.1` or higher. +- The `temporalio` package (provides `temporalio.nexus`, `temporalio.workflow`, `temporalio.exceptions.NexusOperationError`). +- The `nexusrpc` package (provides `@nexusrpc.service`, `nexusrpc.Operation`, `nexusrpc.handler.*`, `nexusrpc.OperationError`, `nexusrpc.HandlerError`). + +## Define the Service contract + +Declare the Service as a class decorated with `@nexusrpc.service`. Each Operation is a class attribute annotated with `nexusrpc.Operation[InputType, OutputType]`. + +```python +from dataclasses import dataclass +import nexusrpc + +@dataclass +class MyInput: + name: str + +@dataclass +class MyOutput: + message: str + +@nexusrpc.service +class MyNexusService: + my_sync_operation: nexusrpc.Operation[MyInput, MyOutput] + my_workflow_run_operation: nexusrpc.Operation[MyInput, MyOutput] +``` + +- A Nexus Operation takes exactly one input parameter; map multiple Workflow arguments via the input dataclass and unpack with `args=[...]` on the handler side. +- The default Data Converter encodes payloads as Null, Byte array, Protobuf JSON, then JSON; dataclasses serialize to JSON. + +## Implement Operation handlers + +A handler class is decorated with `@nexusrpc.handler.service_handler(service=MyNexusService)` — the `service=` kwarg binds the handler to its contract. + +Two utility decorators define Operations: + +- `nexusrpc.handler.sync_operation` — synchronous handler. +- `nexus.workflow_run_operation` — asynchronous handler that starts a Workflow. + +### Synchronous Operation handler + +Decorate an `async def` method whose signature is `(self, ctx: nexusrpc.handler.StartOperationContext, input: I) -> O`. + +```python +import nexusrpc + +@nexusrpc.handler.service_handler(service=MyNexusService) +class MyNexusServiceHandler: + @nexusrpc.handler.sync_operation + async def my_sync_operation( + self, ctx: nexusrpc.handler.StartOperationContext, input: MyInput + ) -> MyOutput: + return MyOutput(message=f"Hello {input.name} from sync operation!") +``` + +- A sync handler must return in under `10s`; for longer work use `@nexus.workflow_run_operation` below. +- Handlers should be reliable: 5 consecutive retryable errors trip the circuit breaker and block all Operations from the caller to that Endpoint. + +### Sync handler accessing Temporal primitives + +From inside a sync handler, get the Worker's Temporal Client via `nexus.client()` to Signal, Query, or Update an existing Workflow (or Signal-With-Start / Update-With-Start). + +```python +from temporalio import nexus + +@nexusrpc.handler.service_handler(service=NexusGreetingService) +class NexusGreetingServiceHandler: + def _get_workflow_handle(self, user_id: str): + return nexus.client().get_workflow_handle_for( + GreetingWorkflow.run, get_workflow_id(user_id) + ) +``` + +- All calls inside a sync handler must complete within the Nexus request timeout; keep Updates short-lived. +- Use `nexus.info()` to access information about the currently-executing Operation, including its Task Queue. + +### Async (Workflow-run) Operation handler + +Decorate an `async def` whose signature is `(self, ctx: nexus.WorkflowRunOperationContext, input: I) -> nexus.WorkflowHandle[O]`, and return the handle from `await ctx.start_workflow(...)`. + +```python +import uuid +import nexusrpc +from temporalio import nexus + +@nexusrpc.handler.service_handler(service=MyNexusService) +class MyNexusServiceHandler: + @nexus.workflow_run_operation + async def my_workflow_run_operation( + self, ctx: nexus.WorkflowRunOperationContext, input: MyInput + ) -> nexus.WorkflowHandle[MyOutput]: + return await ctx.start_workflow( + WorkflowStartedByNexusOperation.run, + input, + id=str(uuid.uuid4()), + ) +``` + +- Start the underlying Workflow with `await ctx.start_workflow(...)` — do **not** use a `Client` from inside an async handler. +- Use a business-meaningful Workflow `id` (passed in via the Operation input where possible); the ID dedupes Workflow starts. +- The Workflow's Task Queue defaults to the Task Queue the Operation is handled on. +- Attach multiple Nexus callers to one handler Workflow with Conflict-Policy `Use-Existing`. + +#### Multi-argument Workflows + +A Nexus Operation has one input, but the started Workflow can take many. Unpack the input via `args=[...]`: + +```python +@nexus.workflow_run_operation +async def hello( + self, ctx: nexus.WorkflowRunOperationContext, input: HelloInput +) -> nexus.WorkflowHandle[HelloOutput]: + return await ctx.start_workflow( + HelloHandlerWorkflow.run, + args=[input.name, input.language], + id=f"hello-multi-args-{input.name}-{input.language}", + ) +``` + +## Register handlers on a Worker + +Pass instantiated handlers to the Worker via the `nexus_service_handlers=[...]` kwarg. Constructor arguments needed by your handler go into its `__init__`. + +```python +from temporalio.client import Client +from temporalio.worker import Worker + +async def main(): + client = await Client.connect("localhost:7233", namespace=NAMESPACE) + worker = Worker( + client, + task_queue=TASK_QUEUE, + workflows=[WorkflowStartedByNexusOperation], + nexus_service_handlers=[MyNexusServiceHandler()], + ) + await worker.run() +``` + +- A Worker only polls for Nexus tasks when handlers are registered via `nexus_service_handlers`. +- Operation handlers typically live in the same Worker as the underlying Temporal primitives they abstract. + +## Caller Workflow + +Import the Service contract through the determinism sandbox, then build a Nexus client via `workflow.create_nexus_client(service=..., endpoint=...)`. + +```python +from temporalio import workflow + +with workflow.unsafe.imports_passed_through(): + from hello_nexus.service import MyInput, MyNexusService, MyOutput + +@workflow.defn +class CallerWorkflow: + @workflow.run + async def run(self, name: str) -> tuple[MyOutput, MyOutput]: + nexus_client = workflow.create_nexus_client( + service=MyNexusService, + endpoint=NEXUS_ENDPOINT, + ) + # One-shot: start and wait in a single await. + wf_result = await nexus_client.execute_operation( + MyNexusService.my_workflow_run_operation, + MyInput(name), + ) + # Two-phase: obtain a handle, then await it for the result. + sync_operation_handle = await nexus_client.start_operation( + MyNexusService.my_sync_operation, + MyInput(name), + ) + sync_result = await sync_operation_handle + return sync_result, wf_result +``` + +- `execute_operation(op, input)` starts the Operation and waits for the result in one `await`. +- `start_operation(op, input)` returns the started state (including the operation token for async Operations); `await` the handle to obtain the result. +- Register and start the caller Workflow exactly like any other Workflow (`client.start_workflow` / `client.execute_workflow`). + +## Setting Operation timeouts + +Pass timeout kwargs on `execute_operation` (or `start_operation`). The Python docs show `schedule_to_close_timeout`: + +```python +return await nexus_client.execute_operation( + SayHelloNexusService.say_hello, + MyInput(name=name), + schedule_to_close_timeout=timedelta(seconds=10), +) +``` + +## Cancellation + +Only asynchronous Operations can be canceled; cancellation flows via the operation token. Call `handle.cancel()` on the Operation handle returned by `start_operation`. + +- The Workflow or resources backing the Operation may ignore the cancellation request, in which case the Operation can still enter a terminal state. + +Set the `cancellation_type` kwarg when starting/executing an Operation. The four values: + +- `ABANDON` — do not request cancellation of the Operation. +- `TRY_CANCEL` — initiate the cancellation request and immediately report cancellation to the caller; delivery is not guaranteed if the caller exits first. +- `WAIT_REQUESTED` — request cancellation and wait for confirmation the request was received; does not wait for actual cancellation. +- `WAIT_COMPLETED` — wait for Operation completion (may or may not complete as cancelled). + +Default is `WAIT_COMPLETED`. + +Once the caller Workflow completes, the caller's Nexus Machinery makes no further attempts to cancel running Operations; to ensure delivery, await all pending Operations before exiting. + +## Exceptions + +Three Nexus-specific exception classes: + +- `nexusrpc.OperationError` — raise from inside an Operation handler to indicate the Operation failed per its own application logic and should not be retried. +- `nexusrpc.HandlerError` — raise with a specific `nexusrpc.HandlerErrorType`; retryability follows the type per the Nexus spec. +- `temporalio.exceptions.NexusOperationError` — raised inside the caller Workflow when a Nexus Operation fails for any reason; walk the cause chain via the `__cause__` attribute. + +`HandlerErrorType` categories: + +- Non-retryable: `BAD_REQUEST`, `UNAUTHENTICATED`, `UNAUTHORIZED`, `NOT_FOUND`, `NOT_IMPLEMENTED`. +- Retryable: `RESOURCE_EXHAUSTED`, `INTERNAL`, `UNAVAILABLE`, `UPSTREAM_TIMEOUT`. + +## Observability + +Workflow history events surfaced in the caller's history: + +- Async Operations: `NexusOperationScheduled`, `NexusOperationStarted`, `NexusOperationCompleted`. +- Sync Operations: `NexusOperationScheduled`, `NexusOperationCompleted` (no `NexusOperationStarted`). + +Use `temporal workflow describe -w ` to see pending Nexus Operations and attached callbacks; `temporal workflow show -w ` to see the full history. + +## Common mistakes + +1. Using `@nexus.sync_operation` (wrong) — the sync decorator is `@nexusrpc.handler.sync_operation`. +2. Using `@nexusrpc.handler.workflow_run_operation` (wrong) — the Workflow-run decorator is `@nexus.workflow_run_operation` from `temporalio.nexus`. +3. Omitting the `service=` kwarg from `@nexusrpc.handler.service_handler(service=MyNexusService)`. +4. Omitting `service=` or `endpoint=` from `workflow.create_nexus_client(...)`. +5. Running long work in a sync handler — the deadline is `10s`; use `@nexus.workflow_run_operation` for long-running work. +6. Calling `Client.start_workflow` from inside an async handler — use `await ctx.start_workflow(...)` instead. +7. Forgetting `args=[...]` for multi-arg Workflows when mapping a single Operation input. +8. Importing the Service module inside a Workflow without `with workflow.unsafe.imports_passed_through():`. +9. Omitting `nexus_service_handlers=[...]` on `Worker(...)` — without it the Worker will not poll for Nexus tasks. +10. Confusing `nexusrpc.OperationError` (Operation-level, non-retryable application failure) with `nexusrpc.HandlerError` (handler-level failure whose retryability is determined by `HandlerErrorType`). +11. Trying to cancel a synchronous Operation — only async Operations can be canceled. +12. Exiting the caller Workflow with pending Operations expecting cancels to be delivered — the Nexus Machinery stops delivering cancels once the caller completes. diff --git a/references/python/python.md b/references/python/python.md index 5493387..40a17f9 100644 --- a/references/python/python.md +++ b/references/python/python.md @@ -182,6 +182,7 @@ See `references/python/testing.md` for info on writing tests. - **`references/python/versioning.md`** - Patching API, workflow type versioning, Worker Versioning - **`references/python/determinism-protection.md`** - Python sandbox specifics, forbidden operations, pass-through imports - **`references/python/ai-patterns.md`** - LLM integration, Pydantic data converter, AI workflow patterns +- **`references/python/nexus.md`** - Python SDK Nexus APIs: Service contracts, sync/async Operation handlers, caller-side Nexus client, exception classes, cancellation ### Python Integrations diff --git a/references/typescript/nexus.md b/references/typescript/nexus.md new file mode 100644 index 0000000..d5f2057 --- /dev/null +++ b/references/typescript/nexus.md @@ -0,0 +1,217 @@ +# Temporal TypeScript SDK Nexus Reference + +TypeScript SDK identifiers, APIs, and idioms for building Nexus Services, Operation handlers, and caller Workflows. See `references/core/nexus.md` for concepts (Endpoint, Service, Operation, caller/handler roles). + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Prerequisites + +- Temporal CLI `v1.3.0` or higher. +- Temporal TypeScript SDK `v1.12.3` or higher. +- Packages: `nexus-rpc` (Service contract + handler), `@temporalio/nexus` (Workflow-run helper, `getClient`, `startWorkflow`), `@temporalio/worker` (registration), `@temporalio/workflow` (caller-side client), `@temporalio/client` (start the caller Workflow). +- Run a dev Server with `temporal server start-dev` and create the Endpoint with `temporal operator nexus endpoint create --name --target-namespace --target-task-queue `. + +## Define the Service contract + +Put the Service contract in a shared module so the caller and handler import the same definition. + +```ts +import * as nexus from 'nexus-rpc'; + +export const helloService = nexus.service('hello', { + echo: nexus.operation(), + hello: nexus.operation(), +}); + +export interface EchoInput { message: string } +export interface EchoOutput { message: string } +export interface HelloInput { name: string; language: LanguageCode } +export interface HelloOutput { message: string } +export type LanguageCode = 'en' | 'fr' | 'de' | 'es' | 'tr'; +``` + +- Service contract: `nexus.service('', { … })` from `nexus-rpc`. +- Operation declaration: `nexus.operation()` — input and output are the only type parameters. +- The default Data Converter encodes payloads as Null, byte array, or JSON; use plain TypeScript objects (serialized to JSON) for polyglot interop. + +## Implement Operation handlers + +A Service handler implements every Operation declared by the Service contract using `nexus.serviceHandler(service, { … })` from `nexus-rpc`. + +### Synchronous handler (plain async function) + +A synchronous Operation handler is a plain async function on the service handler object — there is no special wrapper. + +```ts +import * as nexus from 'nexus-rpc'; +import { helloService, EchoInput, EchoOutput } from '../api'; + +export const helloServiceHandler = nexus.serviceHandler(helloService, { + echo: async (ctx, input: EchoInput): Promise => { + return input; + }, + // ... +}); +``` + +- Signature: `async (ctx, input: I): Promise => { … }`. +- Use sync handlers for short RPC-style work (lookups, computations, Signals/Queries/Updates). Handlers must be reliable: the circuit breaker trips after 5 consecutive retryable errors. + +### Sync handler that calls Temporal (Signal / Query / Update) + +```ts +import * as nexus from 'nexus-rpc'; +import * as temporalNexus from '@temporalio/nexus'; + +export const nexusGreetingServiceHandler = nexus.serviceHandler(nexusGreetingService, { + getLanguages: async (ctx, input: GetLanguagesInput) => { + const client = temporalNexus.getClient(); + const handle = client.workflow.getHandle(workflowIdForUser(input.userId)); + return await handle.query(getLanguagesQuery); + }, +}); +``` + +- `temporalNexus.getClient()` returns a Temporal Client connected via the same `NativeConnection` as the host Worker. +- All work inside a sync handler must complete within the Nexus request timeout. +- `ctx.abortSignal` is an `AbortSignal` that fires when the deadline is exceeded — pass it to Temporal Client calls so they are canceled on timeout. +- `ctx.requestDeadline` is an optional `Date` for the current request's deadline; use it to decide whether to start work or to set downstream timeouts. +- Keep Updates inside sync handlers short-lived to stay within the request deadline. + +### Asynchronous (Workflow-run) handler + +To back an Operation with a Workflow, use `new WorkflowRunOperationHandler(fn)` from `@temporalio/nexus` and start the Workflow with `temporalNexus.startWorkflow(ctx, workflow, options)`. + +```ts +import { randomUUID } from 'crypto'; +import * as nexus from 'nexus-rpc'; +import * as temporalNexus from '@temporalio/nexus'; +import { helloService, HelloInput, HelloOutput } from '../api'; +import { helloWorkflow } from './workflows'; + +export const helloServiceHandler = nexus.serviceHandler(helloService, { + hello: new temporalNexus.WorkflowRunOperationHandler( + async (ctx, input: HelloInput) => { + return await temporalNexus.startWorkflow(ctx, helloWorkflow, { + args: [input], + workflowId: ctx.requestId ?? randomUUID(), + // Task queue defaults to the task queue this Operation is handled on. + }); + }, + ), +}); +``` + +- The delegate `fn` receives `(ctx, input)` and is the place to validate/transform input and customize Workflow start options. +- `ctx.requestId` is allocated by Temporal when the caller schedules the Operation and is stable across retries — use it as the `workflowId` to dedupe starts. +- Workflow IDs should be business-meaningful and are used to dedupe Workflow starts; pass the ID through the Operation input as part of the Service contract when possible. +- The Task Queue defaults to the queue the Operation handler runs on — set it explicitly only when targeting a different Worker fleet. + +#### Multi-argument Workflows + +A Nexus Operation takes a single input, so wrap multiple Workflow arguments in one input object and spread them into the `args` array on `startWorkflow`. + +```ts +return await temporalNexus.startWorkflow(ctx, myWorkflow, { + args: [input.x, input.y], + workflowId: ctx.requestId ?? randomUUID(), +}); +``` + +## Register handlers on a Worker + +Pass `nexusServices: [...]` to `Worker.create({ … })`. + +```ts +import { Worker, NativeConnection } from '@temporalio/worker'; +import { helloServiceHandler } from './handler'; + +const connection = await NativeConnection.connect({ address: 'localhost:7233' }); +const worker = await Worker.create({ + connection, + namespace: 'my-target-namespace', + taskQueue: 'my-handler-task-queue', + workflowsPath: require.resolve('./workflows'), + nexusServices: [helloServiceHandler], +}); +``` + +- A Worker only polls for Nexus tasks if at least one handler is in `nexusServices`. +- Service handlers are typically hosted on the same Worker as the Workflows and Activities they wrap. + +## Caller Workflow + +Inside a Workflow, build a Nexus client with `createNexusServiceClient` from `@temporalio/workflow` and invoke `executeOperation(opName, input, options)`. + +```ts +import * as wf from '@temporalio/workflow'; +import { helloService, LanguageCode } from '../service/api'; + +const HELLO_SERVICE_ENDPOINT = 'hello-service-endpoint-name'; + +export async function helloCallerWorkflow(name: string, language: LanguageCode): Promise { + const nexusClient = wf.createNexusServiceClient({ + service: helloService, + endpoint: HELLO_SERVICE_ENDPOINT, + }); + + const helloResult = await nexusClient.executeOperation( + 'hello', + { name, language }, + { scheduleToCloseTimeout: '10s' }, + ); + + return helloResult.message; +} +``` + +- `createNexusServiceClient({ service, endpoint })` — the `service` is the imported Service contract and `endpoint` is the Nexus Endpoint name. +- `executeOperation` starts the Operation and awaits its result. +- The caller depends only on the Service contract, not on handler code — keep the contract in a shared module. + +## Operation timeouts + +Set timeouts in the third argument of `executeOperation`. The documented option in TypeScript is `scheduleToCloseTimeout`. + +```ts +await nexusClient.executeOperation('hello', input, { scheduleToCloseTimeout: '10s' }); +``` + +## Cancellation + +Nexus Operations execute inside Cancellation Scopes provided by `@temporalio/workflow`. + +- Cancelling a Cancellation Scope cancels every cancellable operation owned by it, including Nexus Operations. +- The Workflow itself is the root Cancellation Scope: cancelling the caller Workflow propagates cancellation to all Nexus Operations it started. +- For per-Operation control, create a new Cancellation Scope and start the Operation inside it. +- Only asynchronous Operations can be canceled — cancellation is delivered via the operation token. Synchronous Operations cannot be canceled. +- The Workflow or resource backing the Operation may ignore the cancellation request. +- Once the caller Workflow completes, the Nexus Machinery makes no further attempts to cancel still-running Operations — `await` pending Operations before returning if you need cancellation to be delivered. + +## Exceptions + +TypeScript surfaces three Nexus-specific exception types. + +- `OperationError` (from `nexus-rpc`) — throw inside a handler to signal application-level failure that should not be retried. +- `HandlerError` (from `nexus-rpc`) — throw with a `HandlerErrorType`. Non-retryable types: `BAD_REQUEST`, `UNAUTHENTICATED`, `UNAUTHORIZED`, `NOT_FOUND`, `NOT_IMPLEMENTED`. Retryable types: `RESOURCE_EXHAUSTED`, `INTERNAL`, `UNAVAILABLE`, `UPSTREAM_TIMEOUT`. +- `NexusOperationFailure` (from `@temporalio/nexus`) — thrown inside the caller Workflow when an Operation fails for any reason; inspect `cause` to walk the cause chain. + +## Observability + +Caller Workflow history for synchronous Operations contains `NexusOperationScheduled` and `NexusOperationCompleted`. For asynchronous Operations it also contains `NexusOperationStarted`. + +For tracing, register `OpenTelemetryPlugin` from `@temporalio/interceptors-opentelemetry` via the Worker's `plugins` option — it auto-registers Nexus, Activity, and Workflow interceptors. + +## Common mistakes + +1. Forgetting `nexusServices: [...]` on `Worker.create` — the Worker silently never polls for Nexus tasks. +2. Using a wrapper type for sync handlers — there is no wrapper; a sync handler is a plain `async (ctx, input) => …` property on the `serviceHandler` object. +3. Building the Temporal Client manually inside a sync handler — use `temporalNexus.getClient()` so the call uses the Worker's existing `NativeConnection`. +4. Ignoring `ctx.abortSignal` for downstream calls — long Client calls will run past the Nexus request deadline if you don't pass it through. +5. Putting long-running work in a sync handler — sync handlers must complete inside the Nexus request timeout; back long work with a Workflow via `WorkflowRunOperationHandler`. +6. Generating a random `workflowId` for every retry of a Workflow-run Operation — use `ctx.requestId ?? randomUUID()` so retries dedupe to the same Workflow. +7. Passing multiple positional Workflow arguments through a Nexus Operation — Operations take a single input; wrap arguments in one object and spread into `args: [...]`. +8. Expecting to cancel a synchronous Operation — only asynchronous Operations support cancellation. +9. Letting the caller Workflow return while Operations are still in flight — pending cancellations are not delivered once the caller completes; `await` outstanding Operations first. +10. Mismatching the Endpoint name between the caller's `createNexusServiceClient({ endpoint })` and the Endpoint created via `temporal operator nexus endpoint create --name` — calls fail to route. diff --git a/references/typescript/typescript.md b/references/typescript/typescript.md index 96fc089..1fc1515 100644 --- a/references/typescript/typescript.md +++ b/references/typescript/typescript.md @@ -182,3 +182,4 @@ See `references/typescript/testing.md` for info on writing tests. - **`references/typescript/data-handling.md`** - Data converters, payload encryption, etc. - **`references/typescript/versioning.md`** - Patching API, workflow type versioning, Worker Versioning - **`references/typescript/determinism-protection.md`** - V8 sandbox and bundling +- **`references/typescript/nexus.md`** - TypeScript SDK Nexus APIs (Public Preview): Service contracts, sync/async Operation handlers, caller-side Nexus client, deadline propagation, cancellation