From ce433231550fca73cadc9d26bf42a0471df2b6b6 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 6 Aug 2026 14:38:48 -0700 Subject: [PATCH 01/16] Adding documentation for Nexus Client library code generation --- .../nexus/nexus-client-code-generator.mdx | 400 ++++++++++++++++++ sidebars.js | 1 + 2 files changed, 401 insertions(+) create mode 100644 docs/encyclopedia/nexus/nexus-client-code-generator.mdx diff --git a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx new file mode 100644 index 0000000000..b3784fa3ff --- /dev/null +++ b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx @@ -0,0 +1,400 @@ +--- +id: nexus-client-code-generator +title: Nexus Client Code Generator +sidebar_label: Nexus Client Code Generator +description: The Nexus Client Code Generator turns one schema into typed models, runtime validators, and Nexus Service definitions for Go, Java, Python, and TypeScript. +toc_max_heading_level: 4 +slug: /nexus/client-code-generator +keywords: + - nexus client code generator + - nexus code generation + - nexus service definition + - service contract + - json schema + - schema validation + - generated models +tags: + - Nexus + - Concepts +--- + +A [Nexus Service](/nexus/services) is a contract meant to be shared across team boundaries. +Those teams often work in different languages, so the same request and response types get hand-written once per SDK. +Hand-written copies drift: a field is required on one side and optional on the other, a bound is enforced by the caller but not the handler. + +The **Nexus Client Code Generator** removes those copies. +You describe your types and [Nexus Operations](/nexus/operations) once in a definition file, and the generator emits the equivalent library code for Go, Java, Python, and TypeScript. +The generator is a command-line tool named `nexgen`, distributed from the [temporalio/nex-gen](https://github.com/temporalio/nex-gen) repository. + +:::caution + +`nexgen` is pre-release software, currently at version 0.2.1. +The supported schema subset, command-line options, and emitted code may change incompatibly before a stable release. +It is not yet published to any package registry, so you build it from source as described in [Install the generator](#install-the-generator). + +::: + +## What the generator produces + +For every type in your definition file, the generator emits three things per language. + +- **A typed model.** An idiomatic struct, class, interface, or dataclass, with doc comments carried over from the schema. +- **A shared runtime validator.** One validator per type, used when a value is parsed off the wire and again when it is serialized onto the wire, so a payload cannot enter or leave your service in a shape the contract forbids. +- **A [Nexus Service Contract](/glossary#nexus-service-contract) definition.** The generated Service and Operation declarations you register on a Worker and call from a caller Workflow. + +Constraint failures do not surface one at a time. +They aggregate into a single native error listing every violation, each naming the offending field and the bound it broke. +A handler maps that error to a `BAD_REQUEST` [Nexus error](/nexus/error-handling), so a malformed request tells the caller everything that was wrong with it in one response. + +The supported schema subset is deliberately strict. +Anything ambiguous, or anything that cannot be expressed identically in all four languages, is rejected when you run the generator, with a diagnostic explaining how to express it instead. +The generator prefers to fail loudly at generation time over emitting code that behaves differently in one language than another. + +## Supported languages + +`nexgen` generates Go, Java, Python, and TypeScript. + +## Definition files + +Types are modeled with [JSON Schema 2020-12](https://json-schema.org/draft/2020-12). +A definition file comes in two flavors. + +**Pure JSON Schema.** The root of the document is itself a type, and reusable types live under `$defs`. +Use this when you only need data models shared across languages, with no Service or Operation declarations. + +**Nexus document.** Add a root `nexusrpc: "1.0.0"` marker to enable a `services` section. +The root becomes an envelope: Services and their Operations sit at the top level, and your types live under `$defs`. + +The examples on this page use `samples/schemas/chat.nexusrpc.yaml` from the repository, abbreviated here: + +```yaml +nexusrpc: '1.0.0' +$schema: https://json-schema.org/draft/2020-12/schema +services: + ChatService: + fqn: example.chat.v1.ChatService + description: Send messages and look up rooms. + operations: + sendMessage: + description: Post a message to a room. + input: { $ref: '#/$defs/SendMessageInput' } + output: { $ref: '#/$defs/SendMessageOutput' } + getRoom: + description: Look up a room by id. + input: + type: object + additionalProperties: false + properties: + roomId: { type: string } + required: [roomId] + output: { $ref: '#/$defs/Room' } + ping: + description: Liveness probe. +$defs: + SendMessageInput: + type: object + additionalProperties: false + properties: + roomId: { type: string } + message: { $ref: '#/$defs/Message' } + required: [roomId, message] + SendMessageOutput: + type: object + additionalProperties: false + properties: + messageId: { type: string } + required: [messageId] +``` + +`fqn` is the wire name of the Service, the name callers reference when executing an Operation. + +An Operation's `input` and `output` are each optional. +The `ping` Operation above declares neither, which generates an Operation that takes and returns nothing. +When present, each must be an object type, so that a field can be added later without breaking the wire format. + +The repository holds four example definitions under [`samples/schemas/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas): +[`chat.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/chat.nexusrpc.yaml), +the feature-diverse [`showcase.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/showcase.nexusrpc.yaml), +the pure-schema [`temporal.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/temporal.yaml), +and a multi-file closure under [`kb/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb) showing how types split across files resolve through `$ref`. +The `kb/` closure starts at [`kb.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/kb/kb.nexusrpc.yaml) and pulls in types from its [`content/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb/content) and [`tree/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb/tree) subdirectories. + +## Install the generator + +Build the `nexgen` binary from source with a Rust toolchain: + +```bash +git clone https://github.com/temporalio/nex-gen.git +cd nex-gen +cargo build --release +``` + +The binary lands at `target/release/nexgen`. +Confirm it works and check which targets your build supports: + +```bash +./target/release/nexgen --version +./target/release/nexgen --help +``` + +## Generate code + +Every language uses the same shape: `nexgen ... --output `. +Inputs are positional and may be files or directories, so you can pass a whole multi-file schema closure. Some languages have extra flags. + +:::note + +The output directory name becomes the generated package or module name. +Name it after your domain, such as `chat`, not after the language. +Pointing `--output` at a directory named `go` produces `package go`, which is not valid Go. + +::: + +### Go + +```bash +nexgen go samples/schemas/chat.nexusrpc.yaml --output ./chat +``` + +Place the output directory inside your Go module. +The package name is the directory name, so the example above generates `package chat` in `./chat/chat.go` alongside `./chat/definitions.go`. + +### Java + +Java requires `--package-name`, and its last dot-separated segment must match the `--output` directory name: + +```bash +nexgen java samples/schemas/chat.nexusrpc.yaml \ + --output ./src/main/java/com/example/chat \ + --package-name com.example.chat +``` + +If the two disagree, generation stops and tells you how to reconcile them: + +``` +`--package-name com.example.wrong` must end with the output directory name `chat`, +but its last segment is `wrong`; point `--output` at a directory named `wrong` or +change the package's last segment to `chat` +``` + +### Python + +```bash +nexgen python samples/schemas/chat.nexusrpc.yaml --output ./chat +``` + +This writes an importable package: `models.py`, `services.py`, and an `__init__.py` that re-exports both. +Generated models are [Pydantic](https://docs.pydantic.dev/) models, so your Worker and Client must use the Pydantic Data Converter described in [Use Pydantic models](/develop/python/data-handling/data-conversion#use-pydantic-models). + +### TypeScript + +```bash +nexgen ts samples/schemas/chat.nexusrpc.yaml --output ./chat +``` + +TypeScript accepts `--date-time-types` to choose how temporal `format` fields are represented in memory: + +```bash +nexgen ts samples/schemas/temporal.yaml --output ./chat --date-time-types temporal +``` + +- `string` (the default) keeps every temporal field as the RFC 3339 string that appears on the wire. + It has no runtime dependency and round-trips losslessly, but you parse and compare the strings yourself. +- `date` maps `date-time` fields to a JavaScript `Date`. + This is lossy: a `Date` is a UTC instant, so the original offset is folded away and precision is capped at milliseconds. +- `temporal` maps to the TC39 Temporal API, preserving offset and sub-second precision, and requires the `Temporal` global or a polyfill. + +## Use the generated code + +The generated Service definition is a normal Nexus Service definition. +You register it on a Worker and call it from a caller Workflow exactly as described in your SDK's Nexus guide. +What differs per language is how the validator gets invoked. + +| SDK | How validation reaches the wire | Extra step | +| ---------- | ----------------------------------------------------------- | ---------- | +| Go | Generated `MarshalJSON` and `UnmarshalJSON` on each model | None | +| Java | Generated Jackson serializer and deserializer on each model | None | +| Python | Pydantic model validation | [Use the Pydantic data converter](/develop/python/data-handling/data-conversion#use-pydantic-models) | +| TypeScript | Generated mapper classes | Call the mapper yourself | + +In Go, Java, and Python the validator sits in the serialization hook the Temporal data converter already calls, so validation happens on its own once the models are in use. +TypeScript requires an explicit call, covered in [Validate payloads in TypeScript](#validate-payloads-in-typescript). + +### Go + +The generated `ChatService` value carries the Service name and one typed Operation reference per Operation. +Register handlers on a Worker: + +```go +service := nexus.NewService(chat.ChatService.ServiceName) + +sendMessage := nexus.NewSyncOperation(chat.ChatService.SendMessage.Name(), + func(ctx context.Context, input chat.SendMessageInput, _ nexus.StartOperationOptions) (chat.SendMessageOutput, error) { + return chat.SendMessageOutput{MessageId: store(input)}, nil + }) + +if err := service.Register(sendMessage); err != nil { + return err +} +w.RegisterNexusService(service) +``` + +Call it from a caller Workflow, passing the generated Operation reference so the SDK type-checks the request and response: + +```go +client := workflow.NewNexusClient("chat-endpoint", chat.ChatService.ServiceName) + +var output chat.SendMessageOutput +err := client.ExecuteOperation( + ctx, + chat.ChatService.SendMessage, + chat.SendMessageInput{RoomId: "r1", Message: chat.Message{Kind: "text", Body: "hi"}}, + workflow.NexusOperationOptions{}, +).Get(ctx, &output) +``` + +### Java + +The generator emits `ChatService` as an interface annotated with `@Service`, with one `@Operation` method per Operation. +On the handler side, write a separate implementation class that points at the generated interface with `@ServiceImpl`, and return an `OperationHandler` from each `@OperationImpl` method: + +```java +@ServiceImpl(service = ChatService.class) +public final class ChatServiceImpl { + @OperationImpl + public OperationHandler sendMessage() { + return OperationHandler.sync((ctx, details, input) -> new SendMessageOutput(store(input))); + } +} +``` + +Register it on a Worker with `worker.registerNexusServiceImplementation(new ChatServiceImpl())`. + +On the caller side, the same interface works directly as a Workflow stub: + +```java +ChatService chat = Workflow.newNexusServiceStub( + ChatService.class, + NexusServiceOptions.newBuilder() + .setEndpoint("chat-endpoint") + .setOperationOptions(NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()) + .build()); + +SendMessageOutput output = chat.sendMessage(new SendMessageInput("r1", message)); +``` + +### Python + +The generator emits `ChatService` as a `@service`-decorated class whose attributes are typed `Operation` declarations. +Bind a handler to it: + +```python +@service_handler(service=ChatService) +class ChatServiceHandler: + @sync_operation + async def send_message( + self, ctx: StartOperationContext, input: SendMessageInput + ) -> SendMessageOutput: + return SendMessageOutput(messageId=store(input)) +``` + +Pass the handler to your Worker as `nexus_service_handlers=[ChatServiceHandler()]`, then call it from a caller Workflow: + +```python +client = workflow.create_nexus_client(service=ChatService, endpoint="chat-endpoint") + +output = await client.execute_operation( + ChatService.send_message, + SendMessageInput(roomId="r1", message=Message(kind="text", body="hi")), +) +``` + +Generated Python fields are snake_case with the wire name as an alias. +Construct models with either name, and read them with the snake_case attribute: `SendMessageInput(roomId="r1", ...)` constructs, and `output.message_id` reads. + +### TypeScript + +The generator emits a `chatService` Service definition plus, for each type, an interface and a companion `Mapper` class: + +```typescript +export const chatService = nexus.service('example.chat.v1.ChatService', { + sendMessage: nexus.operation({ name: 'SendMessage' }), + getRoom: nexus.operation({ name: 'GetRoom' }), + ping: nexus.operation({ name: 'Ping' }), +}); +``` + +Register a handler against that definition with `nexus.serviceHandler(chatService, { ... })`, and create a caller with `workflow.createNexusServiceClient({ service: chatService, endpoint: 'chat-endpoint' })`. + +#### Validate payloads in TypeScript + +:::caution + +In TypeScript the generated validator only runs when you call the mapper. +No generated payload converter exists, so nothing calls it for you. + +::: + +Each generated type comes with a mapper exposing two methods. +`fromIntermediate` validates an untrusted plain value and returns the typed model. +`toIntermediate` validates a model and returns its plain wire form. +Call them at both edges of every Operation, on the handler side and the caller side: + +```typescript +const handler = nexus.serviceHandler(chatService, { + async sendMessage(_ctx, input) { + const request = new SendMessageInputMapper().fromIntermediate(input); + const output = { messageId: await store(request) }; + return new SendMessageOutputMapper().toIntermediate(output) as SendMessageOutput; + }, +}); +``` + +The cast on the return value is expected: `toIntermediate` returns `unknown`, because its result is a plain wire value rather than the model type the Operation declares. + +Skipping the mapper is the failure to watch for, because nothing reports it. +The value handed to your handler is typed as the model, since `nexus.operation` declares it that way, but at runtime it is only whatever was deserialized. +A handler that ignores the mapper compiles, type-checks, and returns correct results for valid payloads, while enforcing none of the constraints in your schema. + +When a payload does violate the contract, `fromIntermediate` throws a `ValidationError` carrying every violation at once: + +``` +ValidationError: 2 validation error(s): roomId: required; message.body: expected string +``` + +The error also exposes a `violations` array of `{ path, reason }` objects, so a handler can convert it into a `BAD_REQUEST` Nexus error with the full list intact. + +## Schema defaults + +A `default` in your schema is applied when reading, and is never written back to the wire. +The field stays optional in the generated model, and each language exposes the default differently. + +- **Go** and **Java** generate an accessor: `PriorityOrDefault()` and `getPriorityOrDefault()`. +- **Python** applies the default through Pydantic, so reading the attribute returns it. +- **TypeScript** exports a module-level constant, such as `DEFAULT_PRIORITY`, that you apply yourself with `value.priority ?? DEFAULT_PRIORITY`. + +## Supported schema features + +The generator implements a curated subset of JSON Schema 2020-12 chosen so that every accepted construct lowers identically into all four languages. + +Fully supported: `properties`, `required`, `default`, `minProperties` and `maxProperties`, `dependentRequired`, string and numeric bounds, `items`, `minItems` and `maxItems`, `minContains` and `maxContains`, `allOf`, the recognized nullable pattern `oneOf: [{type: T}, {type: "null"}]`, and the `title`, `description`, and `deprecated` annotations. + +Partially supported: `type` (single-string form only), `additionalProperties`, `propertyNames`, `const` and `enum` (scalars only), `format`, `pattern` (a portable RE2-safe subset), `multipleOf`, `contentEncoding`, `uniqueItems`, `contains`, `oneOf` (branches must be separable by a decidable selector), and `$ref` with `$defs` (local files only). + +Deliberately rejected, because they have no coherent typed lowering across all four languages: `anyOf`, `not`, `if`/`then`/`else`, `dependentSchemas`, `prefixItems`, `unevaluatedProperties`, `unevaluatedItems`, `contentMediaType`, and `contentSchema`. + +For the current per-keyword support table, see the [nex-gen README](https://github.com/temporalio/nex-gen#supported-json-schema-features). + +:::tip RESOURCES + +- [temporalio/nex-gen](https://github.com/temporalio/nex-gen) for the generator, its README, and the example schemas. +- [Nexus Services](/nexus/services) for the Service contract concept. +- Nexus feature guides for registering Services and calling Operations: + [Go](/develop/go/nexus/feature-guide) | + [Java](/develop/java/nexus/feature-guide) | + [Python](/develop/python/nexus/feature-guide) | + [TypeScript](/develop/typescript/nexus/feature-guide) + +::: diff --git a/sidebars.js b/sidebars.js index cbcb3f767e..d0f8f20b1d 100644 --- a/sidebars.js +++ b/sidebars.js @@ -2069,6 +2069,7 @@ module.exports = { }, items: [ 'encyclopedia/nexus/nexus-services', + 'encyclopedia/nexus/nexus-client-code-generator', 'encyclopedia/nexus/nexus-operations', 'encyclopedia/nexus/standalone-nexus-operation', 'encyclopedia/nexus/nexus-endpoints', From b65227a494651bb227df287738a7f1cde47f76d8 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Fri, 7 Aug 2026 15:12:52 -0700 Subject: [PATCH 02/16] Some proposed documentation. Do not merge, this is just for review. --- .../add-a-standalone-activity.mdx | 72 +++ .../development-walkthrough/add-messaging.mdx | 84 +++ .../call-the-service.mdx | 63 +++ .../call-the-standalone-activity.mdx | 63 +++ .../choose-backing-implementation.mdx | 70 +++ .../debugging-and-tips.mdx | 112 ++++ .../define-the-data-contract.mdx | 70 +++ .../development-walkthrough/generate-code.mdx | 53 ++ .../implement-the-service.mdx | 78 +++ .../nexus/development-walkthrough/index.mdx | 89 ++++ .../publish-in-nexus.mdx | 69 +++ .../development-walkthrough/send-messages.mdx | 69 +++ docs/encyclopedia/nexus/nexus-sdk-v2.mdx | 502 ++++++++++++++++++ .../nexus/nexus-standalone-activity.mdx | 254 +++++++++ sidebars.js | 24 + 15 files changed, 1672 insertions(+) create mode 100644 docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/add-messaging.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/call-the-service.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/generate-code.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/index.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx create mode 100644 docs/develop/java/nexus/development-walkthrough/send-messages.mdx create mode 100644 docs/encyclopedia/nexus/nexus-sdk-v2.mdx create mode 100644 docs/encyclopedia/nexus/nexus-standalone-activity.mdx diff --git a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx new file mode 100644 index 0000000000..cad4003dbe --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx @@ -0,0 +1,72 @@ +--- +id: add-a-standalone-activity +title: Step 9 - Add a Standalone Activity +sidebar_label: 9. Add a Standalone Activity +description: Back the notification Nexus Operation with a Standalone Activity instead of a Workflow, with no wrapper Workflow required. +toc_max_heading_level: 4 +keywords: + - nexus standalone activity + - activity backed operation + - start activity + - notification activity +tags: + - Nexus + - Java SDK +--- + +The last Operation is `notifyRequester`, which tells the requester their approval was `APPROVED` or `DENIED`. + +This one is not a Workflow. It is a single outbound notification with no state, nothing to wait for, and nothing to orchestrate — the [Standalone Activity](/nexus/standalone-activity) shape chosen in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation). + +## Write the Activity + +The Activity is an ordinary Activity. In this walkthrough it is a placeholder that does nothing — no email is sent. Real logic would call an email provider, push to a notification service, or write to an outbox. + +Nothing about it is Nexus-specific. The same Activity could be called from a Workflow. + +`{sample code will be here}` + +## Back the Operation with it + +Use `TemporalOperationHandler` as with every other Operation, but call `startActivity` on the Nexus-aware Client instead of `startWorkflow`. The Operation starts an Activity Execution with no parent Workflow and completes when the Activity returns. + +Before Activity-backed Operations, this Operation would have needed a Workflow whose only job was to call this one Activity — a wrapper with its own Event History and Workflow Id, providing nothing. + +`{sample code will be here}` + +### Options an Activity-backed Operation requires + +`StartActivityOptions` needs two things that a Workflow-called Activity does not, because there is no parent Workflow to supply them: + +- **An Activity Id**, unique within the Namespace. +- **A Task Queue.** It does not have to be the Endpoint's target Task Queue, so notifications can run on their own Worker fleet. + +Derive the Activity Id from the Nexus request Id to make the start idempotent. The server retries a Nexus start request with the same request Id, so each retry targets the same Activity Id instead of sending a second notification. + +That last point matters here more than usual. A duplicate Workflow start is usually harmless; a duplicate notification is a second email to a real person. + +## Register the Activity on the Worker + +Add the Activity implementation to the same Worker that hosts the Nexus Service. An Activity-backed Operation needs no Workflow implementation registered for it. + +`{sample code will be here}` + +## Cancellation needs heartbeating + +For this notification the point is moot — it finishes immediately. But it is worth knowing before you write a longer Activity-backed Operation, because the behavior differs from a Workflow-backed one. + +A Workflow is interrupted by a cancellation request. An Activity is not: the server records the request, and the Worker only finds out on its next heartbeat. An Activity that never heartbeats runs until it completes or hits its start-to-close timeout, however many cancellation requests arrive. + +Making a long-running Activity-backed Operation cancellable takes three settings together — heartbeating from the Activity, a heartbeat timeout, and maximum attempts of 1 so a cancelled attempt is not retried. See [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating). + +## Next + +[Call the Standalone Activity](/develop/java/nexus/development-walkthrough/call-the-standalone-activity). + +:::tip RESOURCES + +- [Nexus Standalone Activity](/nexus/standalone-activity) for the full concept and options. +- [Standalone Activity](/standalone-activity) for Activity Executions outside a Workflow. +- [Java: Standalone Activities](/develop/java/activities/standalone-activities) for the SDK API. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx new file mode 100644 index 0000000000..303d58a192 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -0,0 +1,84 @@ +--- +id: add-messaging +title: Step 7 - Add messaging +sidebar_label: 7. Add messaging +description: Expose Signal, Query, and Update on the approval Workflow as Nexus Operations using the Nexus-aware Client. +toc_max_heading_level: 4 +keywords: + - nexus signal + - nexus query + - nexus update + - workflow message passing + - temporal operation handler +tags: + - Nexus + - Java SDK +--- + +The approval blocks waiting for a decision. Now give callers a way to interact with it while it waits. + +Three Operations get added, one per [message](/sending-messages) type. Which message type to use is determined by what the caller needs back, not by preference. + +| Operation | Message type | Why this type | +| --- | --- | --- | +| `remindApprover` | Signal | Fire-and-forget. The caller does not need a response, only for the nudge to happen. | +| `getApprovalStatus` | Query | Reads state without changing it. Never blocks, never writes. | +| `submitDecision` | Update | Changes state *and* returns a result the caller needs — confirmation the decision was recorded. | + +## Add the handlers to the Workflow + +On the Workflow, add a Signal handler that increments the reminder count, a Query handler that returns the current progress, and an Update handler that records the decision and unblocks the wait. + +The Update is what ends the approval. It records `APPROVED` or `DENIED`, which satisfies the condition the Workflow is blocked on, and the Workflow then returns that decision as its result. + +`{sample code will be here}` + +Two constraints apply to the Query handler. It must not block, and it must not mutate Workflow state — a Query is served by replaying history, so anything it changes is invisible and anything it waits on stalls the Query. Return only what is already in memory. + +## Expose them as Nexus Operations + +All three use `TemporalOperationHandler`, but they divide along the line described in [Nexus SDK V2](/nexus/sdk-v2#the-nexus-aware-client): Signal and Query are **sync side effects**, and Update is an **async backing**. + +### Signal and Query + +Reach these through `client.getWorkflowClient()` on the Nexus-aware Client, then return `TemporalOperationResult.sync(...)`. The Operation completes immediately, during the handler call. + +You can perform as many sync side effects as you want in one handler. Using the injected Client rather than your own is what gets the message linked back to the caller. + +`{sample code will be here}` + +### Update + +Use `client.startWorkflowUpdate(...)`, which is an async backing: the Operation completes when the Update completes, and its result is delivered through the Nexus completion callback. If the Update happens to come back already complete — a retried request, or one that failed validation — the result returns synchronously instead. + +Because it is an async backing, there is at most one per Operation invocation. A handler can still combine it with sync side effects. + +`{sample code will be here}` + +:::caution Query linking is not complete + +Query works through `client.getWorkflowClient()`, but [bidirectional linking](/nexus/execution-debugging#bi-directional-linking) for Query is still in progress and is not yet available in any SDK. A Query sent from a handler is not connected to the caller in the UI the way a Signal is. + +The Operation behaves correctly; only the observability link is missing. See the [per-SDK support table](/nexus/sdk-v2#per-sdk-support) for current status. + +::: + +## Keep the responsibilities separate + +It is worth restating why there are three Operations rather than one flexible one, because collapsing them is a common mistake. + +`getApprovalStatus` reports **in-flight progress only** — whether a decision is still pending, and how many reminders have gone out. It does not return the final decision. The decision is the result of `requestApproval`, which the caller is already awaiting from [step 6](/develop/java/nexus/development-walkthrough/call-the-service). + +Using a Query to fetch the outcome would mean polling for something that is already being pushed, and it would break once the [Retention Period](/temporal-service/temporal-server#retention-period) expires and the history the Query replays is gone. + +## Next + +[Send messages](/develop/java/nexus/development-walkthrough/send-messages) from the caller. + +:::tip RESOURCES + +- [Workflow message passing](/encyclopedia/workflow-message-passing) for Signals, Queries, and Updates. +- [Handling messages](/handling-messages) for handler constraints, including Query restrictions. +- [Nexus SDK V2](/nexus/sdk-v2) for the sync side effect and async backing distinction. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx new file mode 100644 index 0000000000..ea8a4f2ab8 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx @@ -0,0 +1,63 @@ +--- +id: call-the-service +title: Step 6 - Call the Service +sidebar_label: 6. Call the Service +description: Call the approval Nexus Service from a caller Workflow in another Namespace using the generated Service interface. +toc_max_heading_level: 4 +keywords: + - nexus caller workflow + - call nexus operation + - cross namespace + - nexus service stub +tags: + - Nexus + - Java SDK +--- + +Call `requestApproval` from a Workflow in the caller Namespace. The caller knows the Endpoint name and the contract, and nothing else about the handler. + +## Use the generated interface as a stub + +In Java, the generated Service interface works directly as a Nexus Service stub. Create it inside the caller Workflow with the Endpoint name and Operation options, then call its methods as if they were local. + +Because the stub is the generated interface, the call is type-checked against the contract at compile time. A field the contract does not have will not compile, and a payload the contract forbids is rejected by the generated validator before it reaches the wire. + +`{sample code will be here}` + +## Await the decision + +`requestApproval` returns `APPROVED` or `DENIED`. That value is the approval Workflow's return value, delivered to the caller through the Nexus completion callback when the Workflow finishes. + +The caller does not poll. It awaits the Operation, and the wait is durable — the caller Workflow can be evicted, the Worker can restart, and the result still arrives. + +Set a schedule-to-close timeout that reflects how long an approval can legitimately take. A human approval measured in days needs a timeout in days; the default is not going to be right. See [Nexus Operations](/nexus/operations) for the timeout model. + +## Callers in other languages + +The caller here is Java, but nothing about the handler requires that. + +:::note For reviewers + +Every sample in this walkthrough is generated from the contract written in [step 1](/develop/java/nexus/development-walkthrough/define-the-data-contract), so a caller in any supported language talks to this same Java handler without changes on either side. Readers working in Go, Python, or TypeScript will be able to find a caller for their language in that language's sample repository rather than porting this one by hand. + +To generate a caller for another language from this contract, see [Generate code](/nexus/client-code-generator#generate-code) in the Nexus Client Code Generator documentation. + +::: + +## Calling without a caller Workflow + +A caller Workflow is the usual pattern and the one this walkthrough uses, because a Workflow gives the call durability and lets you orchestrate around it. + +If you only need to run one Operation and have nothing to orchestrate, a Client can start an Operation directly with no caller Workflow at all. That is a [Standalone Nexus Operation](/standalone-nexus-operation), and it uses the same Service contract, the same handler, and the same Endpoint — only the caller side differs. See [Java: Standalone Operations](/develop/java/nexus/standalone-operations). + +## Next + +The Service can start an approval and return a decision. [Add messaging](/develop/java/nexus/development-walkthrough/add-messaging) so callers can interact with an approval while it is pending. + +:::tip RESOURCES + +- [Nexus Operations](/nexus/operations) for the Operation lifecycle and timeouts. +- [Java Nexus feature guide](/develop/java/nexus/feature-guide) for the caller API. +- [Nexus Client Code Generator](/nexus/client-code-generator) for generating callers in other languages. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx new file mode 100644 index 0000000000..30924d1b7c --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -0,0 +1,63 @@ +--- +id: call-the-standalone-activity +title: Step 10 - Call the Standalone Activity +sidebar_label: 10. Call the Standalone Activity +description: Call the Activity-backed notification Operation from a caller Workflow and complete the approval flow end to end. +toc_max_heading_level: 4 +keywords: + - call nexus operation + - activity backed operation + - nexus caller workflow +tags: + - Nexus + - Java SDK +--- + +Call `notifyRequester` once the decision is final. From the caller's side there is nothing new to learn, which is the point of this step. + +## The caller cannot tell the difference + +`notifyRequester` is called exactly like `requestApproval`: through the same generated Service stub, with the same Endpoint, the same type checking, and the same error handling. + +Nothing in the caller reveals that this Operation is backed by an Activity and the other by a Workflow. That is the contract doing its job. The handler team could later replace the notification Activity with a Workflow that retries across providers and escalates on failure, and no caller would change. + +`{sample code will be here}` + +## Complete the flow + +With all ten steps in place, the caller Workflow runs the whole approval: + +1. Call `requestApproval` and await it. The Operation starts the approval Workflow in the handler Namespace. +2. While it is pending, other systems call `remindApprover` to nudge and `getApprovalStatus` to report progress. +3. Someone calls `submitDecision` with `APPROVED` or `DENIED`. The Update records it, confirms to that caller, and unblocks the approval Workflow. +4. The approval Workflow returns the decision, which resolves the `requestApproval` Operation the original caller has been awaiting. +5. The caller calls `notifyRequester` with the decision, backed by the notification Activity. + +`{sample code will be here}` + +Every step crossed a Namespace boundary, and the caller never learned a Workflow Id, a Task Queue, or which primitive backed any Operation. + +## Trace it end to end + +Open the caller Workflow in the UI and follow the links. Because the handlers used the Nexus-aware Client, each Operation is connected to the Execution it started or messaged in the handler Namespace, and you can move between the two Namespaces in one view. + +The exception is Query: [linking for Query is still in progress](/nexus/sdk-v2#per-sdk-support), so `getApprovalStatus` will not show a link yet. + +## Where to go next + +The Service is complete but minimal. Natural extensions: + +- **Timeouts and escalation.** Give the approval a deadline and escalate or auto-deny when it passes. +- **Split the Workers.** Run the Nexus Service, the approval Workflow, and the notification Activity on separate Worker fleets. See [Nexus patterns](/nexus/patterns). +- **Callers in other languages.** Generate a caller from the same contract in Go, Python, or TypeScript. See [Nexus Client Code Generator](/nexus/client-code-generator). +- **Standalone invocation.** Call an Operation from a Client with no caller Workflow. See [Standalone Nexus Operation](/standalone-nexus-operation). + +Before running this against anything real, read [Debugging, common pitfalls, and tips](/develop/java/nexus/development-walkthrough/debugging-and-tips). + +:::tip RESOURCES + +- [Nexus execution debugging](/nexus/execution-debugging) for tracing across Namespaces. +- [Nexus patterns](/nexus/patterns) for Worker and Service topology. +- [Nexus Standalone Activity](/nexus/standalone-activity) for Activity-backed Operations. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx new file mode 100644 index 0000000000..3c46bb7b51 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx @@ -0,0 +1,70 @@ +--- +id: choose-backing-implementation +title: Step 3 - Choose the backing implementation +sidebar_label: 3. Choose the backing implementation +description: Decide whether each Nexus Operation is backed by a Standalone Activity, a Workflow, or an Entity Workflow. +toc_max_heading_level: 4 +keywords: + - nexus operation backing + - entity workflow + - standalone activity + - workflow backed operation +tags: + - Nexus + - Java SDK +--- + +The contract says nothing about what runs behind an Operation. That is deliberate — it is the handler's private decision, and it can change later without touching callers. + +There are three shapes to choose from, and picking the wrong one is the most common source of trouble later. + +## Standalone Activity + +One step, no waiting, no state. Call an external API, run a computation, send a notification. + +The Operation starts an Activity Execution with no parent Workflow, and completes when the Activity returns. You get retries and a durable record without a wrapper Workflow that exists only to call one Activity. + +The tradeoff is that an Activity has no Workflow's ability to receive messages or hold state, and cancellation only works if the Activity heartbeats. See [Nexus Standalone Activity](/nexus/standalone-activity). + +## Workflow + +More than one step, or any need for durable intermediate state. + +The Operation starts a Workflow and completes when that Workflow returns, so the Workflow's return value is the Operation's result. Use this when the work orchestrates several Activities, needs a timer, or needs to survive a Worker restart partway through. + +## Entity Workflow + +A Workflow that represents one long-lived thing and stays available for interaction while it runs. + +The distinguishing feature is that it accepts [messages](/sending-messages) — Signals, Queries, and Updates — against a stable Workflow Id derived from the entity it represents. It is still a Workflow-backed Operation; "entity" describes how you use it, not a separate mechanism. + +## The choice for the approval Service + +The approval problem needs both. + +| Operation | Backing | Why | +| --- | --- | --- | +| `requestApproval` | Entity Workflow | Blocks for a human decision, holds the reminder count, and must accept messages while pending | +| `notifyRequester` | Standalone Activity | One outbound notification, no state, nothing to wait for | + +An approval is a textbook entity: it exists for a while, it has identity, and other systems interact with it during its lifetime. Backing it with an Activity would not work at all — an Activity cannot block for a human and cannot receive a Signal. + +The notification is the opposite. It is a single side effect with nothing to orchestrate, so a Workflow would add an Event History and a Workflow Id for no benefit. + +## Give the entity a stable Id + +An Entity Workflow needs a Workflow Id derived from the entity, not a random one, so that later messages can find it. Deriving the approval's Workflow Id from the approval id means a caller that knows the approval id can reach the right Execution. + +This also makes the start idempotent. A retried Nexus start request targets the same Workflow Id rather than starting a second approval. + +## Next + +[Implement the Service](/develop/java/nexus/development-walkthrough/implement-the-service) with a Workflow-backed Operation. + +:::tip RESOURCES + +- [Nexus Standalone Activity](/nexus/standalone-activity) for Activity-backed Operations. +- [Workflow message passing](/encyclopedia/workflow-message-passing) for what makes a Workflow interactive. +- [Nexus patterns](/nexus/patterns) for Service and Worker topology choices. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx new file mode 100644 index 0000000000..aeea78e903 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -0,0 +1,112 @@ +--- +id: debugging-and-tips +title: Debugging, common pitfalls, and tips +sidebar_label: Debugging and tips +description: Diagnose the most common Nexus failures, from Endpoint authorization to Query constraints, and avoid the pitfalls that are easy to miss. +toc_max_heading_level: 4 +keywords: + - nexus debugging + - nexus troubleshooting + - nexus pitfalls + - nexus operation hangs +tags: + - Nexus + - Java SDK +--- + +Most Nexus problems are wiring problems, and they produce a small number of recognizable symptoms. Work from the symptom. + +## The call hangs and nothing happens + +Three causes, in the order worth checking. + +**No Worker is polling the target Task Queue.** The request was accepted and queued, and nothing is serving it. Check that your handler Worker is running and shows as a poller on the Endpoint's target Task Queue. + +**The Task Queue does not match.** The Endpoint's target Task Queue and the Task Queue your Worker registered are two separate strings that have to be identical. A typo produces exactly this symptom, because the request is queued somewhere nobody is listening. + +**The timeout is longer than your patience.** A human approval with a multi-day schedule-to-close timeout is supposed to sit there. Confirm the Operation is actually pending rather than stuck by looking at it in the UI. + +## The call fails as unauthorized + +The caller Namespace is almost certainly not on the Endpoint's allowed caller list. + +Creating an Endpoint does not authorize anyone to call it. Endpoints reject callers that are not explicitly allowed, and in Temporal Cloud the Namespace name includes an Account suffix that is easy to omit. See [Nexus security](/nexus/security). + +## The caller and handler are not linked in the UI + +The handler used its own Temporal Client instead of the one `TemporalOperationHandler` injects. + +Fetching a Client yourself works, and the Operation behaves correctly, but you lose the [bidirectional links](/nexus/execution-debugging#bi-directional-linking) that connect the two Executions. Use the injected Client for anything that starts or messages an Execution. + +The one current exception is Query, where [linking is still in progress](/nexus/sdk-v2#per-sdk-support) and no SDK produces a link yet. + +## A Query returns stale data, hangs, or throws + +Query handlers have two hard constraints, and violating either fails in confusing ways. + +**A Query must not block.** It is served synchronously by replaying history. Waiting on anything stalls the Query rather than delaying it. + +**A Query must not mutate state.** Changes made during a Query are not recorded in Event History, so they are invisible and will not survive. Return only what is already in memory. + +If a Query against a completed approval fails, the [Retention Period](/temporal-service/temporal-server#retention-period) has probably expired and the history it needs to replay is gone. + +## Pitfalls that are easy to miss + +### Using a Query to get the final result + +The single most common design mistake in this shape. + +The approval's decision is the result of `requestApproval` — the Workflow's return value, pushed to the caller when the Workflow completes. Querying for it instead means polling for something already being delivered, it requires the Workflow code to stay deployed and replay-compatible, and it stops working when history ages out. + +Use the Operation result for outcomes. Use a Query for in-flight progress. + +### Expecting to re-attach to a running Operation + +There is no Operation that attaches to an already-running Execution and waits for its result. Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2#per-sdk-support). + +Whoever starts the Operation is who receives the result. If other systems need it, distribute it from the caller or notify them from the handler. + +### An Activity-backed Operation that will not cancel + +An Activity is not interrupted by cancellation the way a Workflow is. Without heartbeating, a heartbeat timeout, and maximum attempts of 1, a cancellation request has no effect and the Operation runs to its timeout. All three settings are needed together. See [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating). + +### Duplicate side effects on retry + +The server retries Nexus start requests. If the backing Execution's Id is not derived from something stable, a retry starts a second one. + +Derive the Workflow Id or Activity Id from the Nexus request Id, or from the Operation input when several Operations should share one Execution. This matters most for Operations with external side effects — a duplicate notification is a second message to a real person. + +### Sending a Signal to a Workflow that may not exist + +A Signal to a missing Workflow fails. Use Signal-with-Start when the target may not be running yet; it starts the Workflow if needed and delivers the Signal either way. + +### More than one async backing per handler invocation + +A handler can perform unlimited sync side effects but at most one async backing. Calling `startWorkflow` and `startWorkflowUpdate` in the same invocation is not a valid Operation. Compose sync side effects freely; pick one thing for the caller to await. + +### Hand-editing generated code + +Generated files are marked as generated and are overwritten on the next run. When a generated name is wrong, fix it with a per-language naming override in the contract. See the [Nexus Client Code Generator](/nexus/client-code-generator). + +### Letting the contract drift + +Callers and handlers deploy independently, so both sides run different contract versions simultaneously. Adding an optional field is safe. Making a field required, removing one, or changing a type is not — it breaks whichever side deploys second. + +## Tips + +**Verify the wiring before writing a caller.** Confirm the Endpoint exists, targets the right Namespace and Task Queue, and that a Worker is polling it. This eliminates most of the symptoms above before any caller code exists. + +**Set timeouts to match reality.** A human approval measured in days needs a schedule-to-close timeout in days. Defaults are not tuned for human latency. + +**Let contract violations be `BAD_REQUEST`.** The generated validators aggregate every violation into one error, so the caller learns everything that was wrong in one response instead of fixing fields one at a time. See [Nexus error handling](/nexus/error-handling). + +**Use `TemporalOperationHandler` even when the Operation is trivial.** An Operation that starts synchronous can later gain an async backing or a Signal without changing shape. + +:::tip RESOURCES + +- [Nexus execution debugging](/nexus/execution-debugging) for tracing Operations across Namespaces. +- [Nexus error handling](/nexus/error-handling) for the error model and retry behavior. +- [Nexus security](/nexus/security) for Endpoint authorization. +- [Nexus SDK V2](/nexus/sdk-v2) for current per-SDK capability status. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx new file mode 100644 index 0000000000..d8c5dadb24 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx @@ -0,0 +1,70 @@ +--- +id: define-the-data-contract +title: Step 1 - Define the data contract +sidebar_label: 1. Define the data contract +description: Plan an approval Nexus Service and write its data contract before any implementation, so every language shares one definition. +toc_max_heading_level: 4 +keywords: + - nexus data contract + - nexus service contract + - json schema + - api contract first +tags: + - Nexus + - Java SDK +--- + +Start with the contract, not the code. + +The contract is the only thing a caller and a handler share. Everything else — which language each side is written in, whether an Operation is backed by a Workflow or an Activity, which Task Queue the Worker polls — is private to one side and can change without the other side knowing. + +## Why the contract comes first + +Writing the contract first is what makes the Service polyglot. + +**Every sample in this walkthrough, in every language, is generated from this one contract.** A Go caller can call the Java handler built here. The Java caller built here can call a Python handler. Neither side hand-writes the request and response types, so neither side can drift from the other. + +The alternative — code-first, where you expose an existing Workflow and derive the contract from its signature — ties the contract to one implementation's shape. It also gives you no way to review the API before building it. Temporal does not currently have a good path from code back to a generated contract, so the contract-first order is the one to follow. + +## Plan the Operations + +Work backwards from what callers need, not from what your Workflow happens to do. + +For the approval problem, callers need to start an approval and learn the outcome, nudge a pending approval, check on progress, submit a decision, and be notified when it is final. That produces five Operations: + +| Operation | Input | Output | Added in | +| --- | --- | --- | --- | +| `requestApproval` | Item id, requester, amount | `APPROVED` or `DENIED` | Step 4 | +| `remindApprover` | Approval id | Nothing | Step 7 | +| `getApprovalStatus` | Approval id | Pending or decided, reminders sent | Step 7 | +| `submitDecision` | Approval id, decision | Confirmation of the recorded decision | Step 7 | +| `notifyRequester` | Requester, decision | Nothing | Step 9 | + +Two decisions in that table are worth explaining, because they are easy to get wrong. + +**`requestApproval` returns the final decision.** It does not return an approval id for the caller to poll. The Operation is backed by a Workflow, so the Operation completes when that Workflow returns, and the Workflow's return value *is* the Operation's result. The caller awaits the Operation and receives `APPROVED` or `DENIED`. + +**`getApprovalStatus` reports in-flight progress only.** It is tempting to use it to fetch the final decision too, but that is the wrong tool. The decision already arrives as the result of `requestApproval`. A Query is served by replaying history in a Worker, which means the Workflow code must still be deployed and replay-compatible, and it stops working once the [Retention Period](/temporal-service/temporal-server#retention-period) expires. Use the Operation result for the outcome, and the Query for what is happening while the approval is still open. + +## Shape the types + +Two constraints apply when writing the contract. + +An Operation's input and output are each optional, but when present each must be an **object type**. A bare string works today and then cannot grow a field tomorrow without breaking the wire format. `remindApprover` returns nothing at all, which is fine. + +Keep the types **forward-compatible**. Callers and handlers deploy independently and will run different versions of the contract at the same time. Adding an optional field is safe; making an existing field required, or removing one, is not. + +Types are modeled with JSON Schema 2020-12. See [Definition files](/nexus/client-code-generator#definition-files) for the two file flavors and the supported subset. + +`{sample code will be here}` + +## Next + +With the contract written, [generate code from it](/develop/java/nexus/development-walkthrough/generate-code) for the handler and the caller. + +:::tip RESOURCES + +- [Nexus Client Code Generator](/nexus/client-code-generator) for the contract format and the supported JSON Schema subset. +- [Nexus Services](/nexus/services) for what a Service contract is and how it is shared. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx new file mode 100644 index 0000000000..13d0ec5269 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx @@ -0,0 +1,53 @@ +--- +id: generate-code +title: Step 2 - Generate code from the contract +sidebar_label: 2. Generate code +description: Use the Nexus Client Code Generator to turn the approval contract into typed models, validators, and Service definitions for the handler and the caller. +toc_max_heading_level: 4 +keywords: + - nexus code generation + - nexgen + - generated models + - nexus service definition +tags: + - Nexus + - Java SDK +--- + +Generate the library code before writing any implementation. Both sides of the Service use it: the handler implements against the generated Service definition, and the caller invokes against the same one. + +## What generation produces + +For each type in the contract, the [Nexus Client Code Generator](/nexus/client-code-generator) emits a typed model, a runtime validator, and — for a contract that declares Services — a Nexus Service definition. + +In Java the Service definition is an interface annotated with `@Service`, carrying one `@Operation` method per Operation. That interface is used on both sides and in two different ways: + +- The **handler** provides an implementation for it, which the Worker registers. +- The **caller** uses the interface directly as a Workflow stub, so calls are type-checked against the contract. + +The generated validators run when a payload is parsed and again when it is serialized, so a request that violates the contract is rejected at the boundary rather than reaching your Workflow. Violations aggregate into one error naming every field that failed, which a handler maps to `BAD_REQUEST`. + +## Generate for Java + +Java generation requires a package name whose last segment matches the output directory name. See [Generate code](/nexus/client-code-generator#generate-code) for the full command shape and the per-language flags. + +`{sample code will be here}` + +Commit the generated code, and regenerate whenever the contract changes. Do not hand-edit it — the files are marked as generated, and your edits are lost on the next run. If a generated name is wrong for Java, fix it in the contract with a per-language naming override rather than editing the output. See the [Nexus Client Code Generator](/nexus/client-code-generator). + +## Generate for other languages + +The same contract produces a caller in any supported language. Generating a Go, Python, or TypeScript client from this contract is one command each, and the result talks to the Java handler built in this walkthrough without any coordination beyond the contract. + +This is the step where the contract-first ordering pays off. Nothing about the handler needs to know which languages its callers use. + +## Next + +With the types in hand, [choose what backs each Operation](/develop/java/nexus/development-walkthrough/choose-backing-implementation). + +:::tip RESOURCES + +- [Nexus Client Code Generator](/nexus/client-code-generator) for installation, per-language commands, and the supported JSON Schema subset. +- [Use the generated code](/nexus/client-code-generator#use-the-generated-code) for how validation is wired in each language. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx new file mode 100644 index 0000000000..3b69ff8ec3 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -0,0 +1,78 @@ +--- +id: implement-the-service +title: Step 4 - Implement the Service +sidebar_label: 4. Implement the Service +description: Implement the approval Nexus Service in Java using TemporalOperationHandler, backed by a Workflow, and run a Worker that hosts it. +toc_max_heading_level: 4 +keywords: + - temporal operation handler + - nexus service implementation + - nexus worker + - approval workflow +tags: + - Nexus + - Java SDK +--- + +Implement the generated Service interface, back `requestApproval` with the approval Workflow, and run a Worker that hosts both. + +## Write the approval Workflow + +The Workflow is an ordinary Temporal Workflow. Nothing in it is Nexus-specific, and it could be started directly by a Client instead. + +For the approval, it needs to: + +1. Run an Activity that evaluates whether the request can be auto-decided. In this walkthrough it is a placeholder that does nothing — real logic would apply policy, check limits, or call a risk service. +2. Run an Activity that tells a human the request is waiting. Also a placeholder. +3. Block until a decision arrives. +4. Return `APPROVED` or `DENIED`. + +The blocking step is the reason this is a Workflow. It may wait weeks, across Worker restarts and deployments, and the wait costs nothing while it is idle. + +`{sample code will be here}` + +## Implement the Operation with TemporalOperationHandler + +Use `TemporalOperationHandler` for every Temporal-backed Operation, including simple ones. It is the entry point in [Nexus SDK V2](/nexus/sdk-v2), and starting with it means an Operation can later gain a Signal or change its backing without changing shape. + +`TemporalOperationHandler.create(...)` gives your start handler a context, a Nexus-aware Client, and the Operation input. Call `startWorkflow` on that Client and return its result. The Operation then completes when the Workflow returns, delivering the Workflow's return value to the caller. + +The Client is not an ordinary Temporal Client. It propagates bidirectional links and request Ids automatically, so the caller's Execution and the approval Workflow are connected in the UI. Fetching your own Client inside a handler works but gives up that linking. + +`{sample code will be here}` + +Set the Workflow Id from the approval id, as decided in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#give-the-entity-a-stable-id). + +:::note + +You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Compare the old and new handlers](/nexus/sdk-v2#compare-the-old-and-new-handlers). + +::: + +## Run the Worker + +One Worker hosts the Nexus Service implementation, the Workflow implementation, and the Activity implementations. Its Task Queue has to match the Task Queue the Nexus Endpoint targets, which you create in the next step. + +`{sample code will be here}` + +A Worker registering a Nexus Service does not need to be the same Worker that runs the backing Workflow. Splitting them is a normal choice for larger deployments — see [Nexus patterns](/nexus/patterns). + +## Handle failures + +Two failure categories behave differently, and callers can tell them apart. + +A **contract violation** — a payload the generated validator rejects — should surface as `BAD_REQUEST`. It is the caller's fault and retrying will not help. The generated validators aggregate every violation into one error, so the caller learns everything that was wrong in a single response. + +An **application failure** — the approval cannot proceed for a business reason — is a failed Operation. Whether it retries depends on the error type you raise. See [Nexus error handling](/nexus/error-handling). + +## Next + +The Service runs but nothing can reach it yet. [Publish it in Nexus](/develop/java/nexus/development-walkthrough/publish-in-nexus). + +:::tip RESOURCES + +- [Nexus SDK V2](/nexus/sdk-v2) for `TemporalOperationHandler` and the Nexus-aware Client. +- [Java Nexus feature guide](/develop/java/nexus/feature-guide) for the full handler and Worker API. +- [Nexus error handling](/nexus/error-handling) for mapping failures to Nexus errors. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/index.mdx b/docs/develop/java/nexus/development-walkthrough/index.mdx new file mode 100644 index 0000000000..35ec50d1bc --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/index.mdx @@ -0,0 +1,89 @@ +--- +id: index +title: Nexus Development Walkthrough - Java SDK +sidebar_label: Development Walkthrough +description: Build a Nexus Service end to end in Java, starting from a data contract and adding one Nexus capability at a time to solve an approval problem. +toc_max_heading_level: 4 +keywords: + - nexus walkthrough + - nexus java + - approval workflow + - data contract + - nexus service + - temporal operation handler +tags: + - Nexus + - Java SDK + - Temporal SDKs +--- + +:::caution + +This walkthrough covers [Nexus SDK V2](/nexus/sdk-v2), which is pre-release. +APIs are experimental and may change in backwards-incompatible ways. + +::: + +This walkthrough builds one Nexus Service from nothing to a complete API, adding a single Nexus capability at each step. + +## Nexus Introduction + +A [Nexus Service](/nexus/services) is a contract that one team publishes and other teams call, across [Namespace](/namespaces) boundaries, without sharing code or a deployment. +Three things follow from that. + +**Durable microservices.** A Nexus Service turns Workflows and Activities into an API. Callers see Operations with typed inputs and outputs; they do not see your Workflow Ids, Task Queues, or retry policies. You keep the freedom to change what runs behind an Operation — swap an Activity for a Workflow, split one Workflow into several — as long as the contract holds. The reliability guarantees come along for free: an Operation backed by a Workflow is as durable as that Workflow. + +**A durable orchestration layer for AI agents and tools.** Agent systems call tools that are slow, flaky, and occasionally expensive. Exposing each tool as a Nexus Operation gives every call durable execution, automatic retries, and a record in [Event History](/encyclopedia/event-history) — and lets the agent and the tools live in different Namespaces, owned by different teams, written in different languages. See [Build AI applications with Temporal](/with-ai) for the wider picture. + +**A shared facade for extensibility.** A Nexus Service can front something that is not a Temporal Workflow at all — an existing internal API, a legacy job queue, a third-party endpoint. Write the wrapper once, run it as one Worker fleet, and every team calls the same Operations instead of each writing its own integration. Because callers only depend on the contract, the team behind it can modify or update the service without breaking anyone. + +## The sample problem + +A purchase request needs approval before it can proceed. + +Approval is slow and human-driven: someone has to look at the request and decide. The system needs to survive that wait, which may be minutes or weeks. While a request is pending, other systems need to nudge the approver and check on progress. Eventually a decision arrives, and the requesting system needs the outcome. + +Concretely, the Service needs to: + +- Start an approval and, eventually, return `APPROVED` or `DENIED` +- Accept a nudge that asks the approver again, and count how many have been sent +- Report progress while the approval is still pending +- Accept a decision from the caller and confirm it was recorded +- Send a notification when the decision is final + +Each of those maps onto a different Nexus capability, which is what makes it a useful walkthrough. By the end, the Service exercises a Workflow-backed Operation, a Signal, a Query, an Update, and an Activity-backed Operation. + +## One contract, every language + +The walkthrough begins with the data contract, before any implementation, and that ordering is the point. + +**The equivalent sample for each language is written against the same contract.** Because the contract is the only thing the two sides share, any caller can call any handler: the Go sample walkthrough caller can drive this Java handler for example, and the Java caller here can drive the handler from each other language's Nexus Development Walkthrough. Handler and caller do not need to agree on a language, only on the contract. + + +:::note + +The idea is that we write a sample repo for each language that implements this project. Then we should be able to run the client from any sample project against the handler from any sample project. + +::: +The [Nexus Client Code Generator](/nexus/client-code-generator) makes this easy. It takes the contract and emits typed models, runtime validators, and Service definitions for Go, Java, Python, and TypeScript, so neither side hand-writes the types and neither side can drift from the contract. + +## Steps + +1. [Define the data contract](/develop/java/nexus/development-walkthrough/define-the-data-contract) +2. [Generate code from the contract](/develop/java/nexus/development-walkthrough/generate-code) +3. [Choose the backing implementation](/develop/java/nexus/development-walkthrough/choose-backing-implementation) +4. [Implement the Service](/develop/java/nexus/development-walkthrough/implement-the-service) +5. [Publish in Nexus](/develop/java/nexus/development-walkthrough/publish-in-nexus) +6. [Call the Service](/develop/java/nexus/development-walkthrough/call-the-service) +7. [Add messaging](/develop/java/nexus/development-walkthrough/add-messaging) +8. [Send messages](/develop/java/nexus/development-walkthrough/send-messages) +9. [Add a Standalone Activity](/develop/java/nexus/development-walkthrough/add-a-standalone-activity) +10. [Call the Standalone Activity](/develop/java/nexus/development-walkthrough/call-the-standalone-activity) + +Then: [Debugging, common pitfalls, and tips](/develop/java/nexus/development-walkthrough/debugging-and-tips). + +## Before you start + +You need two Namespaces, one for the handler and one for the caller, so the walkthrough crosses a real Namespace boundary. A [local development server](/develop/run-a-development-server) with two Namespaces is enough for steps 1 through 4; step 5 covers both the development server and Temporal Cloud. + +If you have not used Nexus before, read [Nexus Services](/nexus/services) and [Nexus Operations](/nexus/operations) first, or work through the shorter [Nexus quickstart](/develop/java/nexus/quickstart). diff --git a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx new file mode 100644 index 0000000000..1283bf7a86 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx @@ -0,0 +1,69 @@ +--- +id: publish-in-nexus +title: Step 5 - Publish in Nexus +sidebar_label: 5. Publish in Nexus +description: Create a Nexus Endpoint for the approval Service, allow caller Namespaces to reach it, and set up credentials for Temporal Cloud. +toc_max_heading_level: 4 +keywords: + - nexus endpoint + - nexus registry + - allowed caller namespaces + - api key + - temporal cloud nexus +tags: + - Nexus + - Java SDK +--- + +The Service is implemented and a Worker is polling, but no caller can reach it. A [Nexus Endpoint](/nexus/endpoints) is what makes it reachable, and the [Nexus Registry](/nexus/registry) is where Endpoints live. + +An Endpoint routes incoming Operation requests to a target Namespace and Task Queue. Callers address the Endpoint by name and never learn the Namespace or Task Queue behind it, which is what lets you move the handler later without changing caller code. + +## Create the Endpoint + +An Endpoint needs three things: a unique name, the target Namespace where the handler runs, and the target Task Queue the handler's Worker polls. The Task Queue must match what your Worker registered in [step 4](/develop/java/nexus/development-walkthrough/implement-the-service#run-the-worker), or requests arrive and nothing picks them up. + +On a development server, create it with the CLI: + +`{sample code will be here}` + +In Temporal Cloud, create it in the UI under Nexus, or with `tcld`. See [Create a Nexus Endpoint](/nexus/registry#create-a-nexus-endpoint). + +Endpoint names are unique within the Registry. In Temporal Cloud the Registry is global across your whole Account and spans every Namespace; in a self-hosted deployment it is scoped to the Cluster. + +## Allow caller Namespaces + +This is the step people miss, because the failure looks like a routing problem rather than a permissions one. + +An Endpoint **rejects callers that are not on its allowed list**. Creating the Endpoint is not enough — you have to name the Namespaces permitted to call it. The caller Namespace in this walkthrough is separate from the handler Namespace, so it has to be added explicitly. + +In Temporal Cloud, set the allowed caller Namespaces when you create or edit the Endpoint in the UI, or with `tcld`. Add the caller Namespace, including its Account suffix. + +If a call fails as unauthorized and the Endpoint clearly exists, check this list first. + +## Set up credentials + +On a development server there is nothing to configure. Both Namespaces are local and unauthenticated. + +For Temporal Cloud, the caller and handler connect as separate clients, each to its own Namespace. Generate an API key with access to both Namespaces, or use mTLS certificates. The SDK's [environment configuration](/develop/environment-configuration) support lets you keep one profile per Namespace and select between them with an environment variable, which is cleaner than passing connection options in code. + +`{sample code will be here}` + +## Verify it is reachable + +Before writing a caller, confirm the wiring independently. Check that the Endpoint exists in the Registry and targets the right Namespace and Task Queue, and that your handler Worker shows as polling that Task Queue. + +A Worker that is not polling is the other common cause of a call that appears to hang: the request is accepted and queued, and nothing serves it. + +## Next + +[Call the Service](/develop/java/nexus/development-walkthrough/call-the-service) from the caller Namespace. + +:::tip RESOURCES + +- [Nexus Endpoints](/nexus/endpoints) and [Nexus Registry](/nexus/registry) for the concepts and management surfaces. +- [Nexus security](/nexus/security) for the Endpoint authorization model. +- [Temporal Cloud Nexus](/cloud/nexus) for Cloud-specific setup and limits. +- [Environment configuration](/develop/environment-configuration) for managing two Namespace profiles. + +::: diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx new file mode 100644 index 0000000000..8352d9b613 --- /dev/null +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -0,0 +1,69 @@ +--- +id: send-messages +title: Step 8 - Send messages +sidebar_label: 8. Send messages +description: Call the Signal, Query, and Update Operations from a caller Workflow, and start an approval with Signal-with-Start. +toc_max_heading_level: 4 +keywords: + - send nexus signal + - nexus query caller + - nexus update caller + - signal with start +tags: + - Nexus + - Java SDK +--- + +From the caller's side, the three messaging Operations are just Operations. They are called through the same generated Service stub as `requestApproval`, with the same type checking. + +The caller does not know that one is a Signal, one is a Query, and one is an Update. That is the handler's implementation detail, and it can change without breaking callers. + +## Nudge, check, and decide + +`{sample code will be here}` + +What differs between them is what you get back and how long it takes. + +`remindApprover` returns nothing and completes as soon as the Signal is accepted. Accepted is not the same as handled — the Signal is durably recorded and the Workflow will process it, but the Operation does not wait for that. If the caller needs confirmation that the nudge took effect, it needs an Update, not a Signal. + +`getApprovalStatus` returns the current progress immediately. Call it when something needs to report on a pending approval; do not call it in a loop waiting for the decision. + +`submitDecision` returns confirmation that the decision was recorded. This is the point of using an Update: the caller learns the outcome of its own message. Once it succeeds, the approval Workflow unblocks and completes, which resolves the `requestApproval` Operation that the original caller is still awaiting. + +## Start and Signal in one call + +Sometimes a caller wants to start an approval and immediately attach information to it, without a race between the two calls. + +Signal-with-Start does both atomically: if the target Workflow is not running it is started, and either way the Signal is delivered. It is a sync side effect on the Nexus-aware Client, reached through `client.getWorkflowClient()`, so a handler Operation can offer it directly. + +This is also how you make a Signal safe to send to an approval that may not exist yet. A plain Signal to a missing Workflow fails; Signal-with-Start creates it. + +`{sample code will be here}` + +:::caution Update-with-Start is not yet available + +Update-with-Start — starting a Workflow and running an Update against it atomically — is not available in any SDK yet. See the [per-SDK support table](/nexus/sdk-v2#per-sdk-support). + +Until it lands, an Operation that needs both must start the Workflow and then submit the Update as a separate call, which is not atomic. Signal-with-Start is available and covers the case where the caller does not need a response. + +::: + +## You cannot re-attach to get a result + +A caller that did not start an approval cannot ask Nexus for its final decision. + +`requestApproval` delivers the decision to whoever awaited it. There is no Operation that attaches to an already-running approval and waits for its result, because Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2#per-sdk-support) in any SDK. + +If several systems need the outcome, either have the caller that started the approval distribute it, or have the handler notify them — which is what the notification in the next step does. + +## Next + +[Add a Standalone Activity](/develop/java/nexus/development-walkthrough/add-a-standalone-activity) to notify the requester once a decision is final. + +:::tip RESOURCES + +- [Sending messages](/sending-messages) for Signal, Query, and Update semantics. +- [Nexus SDK V2](/nexus/sdk-v2) for which capabilities are available per SDK. +- [Java Nexus feature guide](/develop/java/nexus/feature-guide) for the caller API. + +::: diff --git a/docs/encyclopedia/nexus/nexus-sdk-v2.mdx b/docs/encyclopedia/nexus/nexus-sdk-v2.mdx new file mode 100644 index 0000000000..db64f6c765 --- /dev/null +++ b/docs/encyclopedia/nexus/nexus-sdk-v2.mdx @@ -0,0 +1,502 @@ +--- +id: nexus-sdk-v2 +title: Nexus SDK V2 +sidebar_label: Nexus SDK V2 +description: Nexus SDK V2 replaces the per-primitive Nexus Operation helpers with a single Temporal Operation Handler that supports every Temporal primitive and propagates bidirectional links automatically. +toc_max_heading_level: 4 +slug: /nexus/sdk-v2 +keywords: + - nexus sdk v2 + - temporal operation handler + - nexus sdk ergonomics + - nexus signal + - nexus update + - nexus query + - bidirectional linking +tags: + - Nexus + - Concepts +--- + +import { SdkTabs } from '@site/src/components'; + +:::caution + +Nexus SDK V2 is pre-release. +`TemporalOperationHandler` is marked experimental in the SDKs and may change in backwards-incompatible ways. +"SDK V2" is a working title used while the feature is in pre-release. + +::: + +Nexus SDK V2 changes how you implement a [Nexus Service](/nexus/services) contract. +Instead of one helper type per Temporal primitive, there is a single handler type — `TemporalOperationHandler` — that can back an Operation with any Temporal primitive and that carries [bidirectional links](/nexus/execution-debugging#bi-directional-linking) across the Namespace boundary for you. + +Nothing on the wire changes, and no existing Operation stops working. +The Service contract, [Nexus Endpoint](/nexus/endpoints) setup, and Worker registration are the same as before. +What changes is the handler you write. + +## Why it changed + +Before SDK V2, only one pattern had first-class support: start a Workflow and return its result. +Everything else meant reaching for a lower-level API. + +- **Only one primitive was ergonomic.** `WorkflowRunOperation` covered "start a Workflow and wait." Signal, Update, and Query had no equivalent, so teams wrote synchronous handlers that reached for a Temporal Client by hand. +- **Nexus calls were hard to find.** The synchronous handler API lives in the separate `nexus-rpc` SDK rather than the Temporal SDK, so developers looking through Temporal's own API surface did not find it. +- **Hand-wired handlers lost observability.** A synchronous handler that grabbed a Client itself did not produce bidirectional links, so the caller-side and handler-side Executions were not connected in the UI. Bidirectional linking is quite useful but wasn't always present. + +SDK V2 addresses all three by making one handler type the entry point for every Temporal-backed Operation, and by injecting a Nexus-aware Client that does the linking. + +## The Nexus-aware Client + +`TemporalOperationHandler.create(...)` gives your start handler three things: a context, a Client, and the Operation input. + +The Client propagates bidirectional links and request IDs automatically, so every Execution it starts or messages is connected back to the caller in the UI and in [Event History](/encyclopedia/event-history). +Reaching for your own Client inside a handler still works, but it gives up that linking. + +The Client exposes two kinds of call, and the distinction matters. + +**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. + +- `client.startWorkflow(...)` — the Operation completes when the Workflow returns +- `client.startWorkflowUpdate(...)` — the Operation completes when the Update completes +- `client.startActivity(...)` — the Operation completes when the Activity returns; see [Nexus Standalone Activity](/nexus/standalone-activity) + +**Sync messaging — as many as you need.** Reach these through `client.getWorkflowClient()`. +They take effect during the handler call, still get link propagation, and do not require an async backing. + +- Signal, Signal-with-Start, Query, Cancel, and Terminate + +A single handler can combine both: perform a sync Signal to unblock something, then return an async backing whose result the caller waits on. +A handler that only performs sync side effects returns `TemporalOperationResult.sync(...)` and the Operation completes immediately. + +## Updated handler methods + +The following examples use a Nexus Service with a `startGreeting` Operation backed by a Workflow and a `greet` Operation that completes inline. Click the language tabs to see example code in each language - Go, Java, .Net, Python, and Typescript. + +:::note +This is still a rough draft for feedback. Not all languages are filled in yet. + +::: + +### Back an Operation with a Workflow + +Before, each SDK had a dedicated Workflow-run helper. It reached the Temporal Client through the Operation context rather than being handed one, and it returned a Workflow handle or method reference rather than an Operation result: + + + + +```go +op := temporalnexus.NewWorkflowRunOperation( + "startGreeting", + GreetingWorkflow, + func(ctx context.Context, input GreetingInput, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ + ID: "greeting-" + input.Name, + }, nil + }) +``` + + + + +```java +@OperationImpl +public OperationHandler startGreeting() { + return WorkflowRunOperation.fromWorkflowMethod( + (ctx, details, input) -> + Nexus.getOperationContext() + .getWorkflowClient() + .newWorkflowStub( + GreetingWorkflow.class, + WorkflowOptions.newBuilder() + .setWorkflowId("greeting-" + input.getName()) + .build()) + ::greet); +} +``` + + + + +```python +@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}" + ) +``` + + + + +```typescript +const startGreeting = new temporalnexus.WorkflowRunOperationHandler( + async (ctx, input: GreetingInput) => + await temporalnexus.startWorkflow(ctx, greetingWorkflow, { + args: [input], + workflowId: `greeting-${input.name}`, + }), +); +``` + + + + +```csharp +WorkflowRunOperationHandler.FromHandleFactory( + async (context, input) => + await context.StartWorkflowAsync( + (GreetingWorkflow wf) => wf.RunAsync(input), + new() { Id = $"greeting-{input.Name}" })); +``` + + + + +Now the Client is handed to your start handler, and you call its start method directly. The return value is a `TemporalOperationResult`, which is what lets the same handler shape also return a synchronous result or an Activity-backed one: + + + + +```go +op := temporalnexus.MustNewTemporalOperation( + temporalnexus.TemporalOperationOptions[GreetingInput, GreetingOutput]{ + Name: "startGreeting", + Start: func( + ctx context.Context, + nc temporalnexus.NexusClient, + input GreetingInput, + _ temporalnexus.StartTemporalOperationOptions, + ) (temporalnexus.TemporalOperationResult[GreetingOutput], error) { + return temporalnexus.StartWorkflow(ctx, nc, + client.StartWorkflowOptions{ID: "greeting-" + input.Name}, + GreetingWorkflow, input) + }, + }) +``` + + + + +```java +@OperationImpl +public OperationHandler startGreeting() { + return TemporalOperationHandler.create( + (context, client, input) -> + client.startWorkflow( + GreetingWorkflow.class, + GreetingWorkflow::greet, + input, + WorkflowOptions.newBuilder() + .setWorkflowId("greeting-" + input.getName()) + .build())); +} +``` + + + + +```python +@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}" + ) +``` + + + + +```typescript +const startGreeting = new temporalnexus.TemporalOperationHandler({ + async start(ctx, client, input) { + return await client.startWorkflow(greetingWorkflow, { + args: [input], + workflowId: `greeting-${input.name}`, + }); + }, +}); +``` + + + + +```csharp +TemporalOperationHandler.FromHandleFactory( + async (context, client, input) => + await client.StartWorkflowAsync( + (GreetingWorkflow wf) => wf.RunAsync(input), + new() { Id = $"greeting-{input.Name}" })); +``` + + + + +Go exposes the start calls as package-level functions taking the Client, rather than as methods on it, because Go does not allow generic methods on a non-generic struct. + +### Send a Signal from an Operation + +Before, a Signal-sending Operation was a synchronous handler that fetched its own Client. +This is the pattern that produced no bidirectional links: + + + + +```go +// nexus.NewSyncOperation comes from the separate nexus-rpc SDK, not from temporalnexus. +op := nexus.NewSyncOperation("cancelOrder", + func(ctx context.Context, input CancelOrderInput, o nexus.StartOperationOptions) (nexus.NoValue, error) { + c := temporalnexus.GetClient(ctx) + return nil, c.SignalWorkflow(ctx, "order-"+input.OrderID, "", "requestCancellation", input) + }) +``` + + + + +```java +@OperationImpl +public OperationHandler cancelOrder() { + return OperationHandler.sync( + (ctx, details, input) -> { + Nexus.getOperationContext() + .getWorkflowClient() + .newUntypedWorkflowStub("order-" + input.getOrderId()) + .signal("requestCancellation", input); + return null; + }); +} +``` + + + + + ```csharp + ``` + + + + +```python +@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) +``` + + + + + ```typescript +``` + + + + +Now the same Operation uses the injected Client, so the Signal is linked. It returns a synchronous result rather than a bare value, because the handler type is the same one used for async backings: + + + + +```go +op := temporalnexus.MustNewTemporalOperation( + temporalnexus.TemporalOperationOptions[CancelOrderInput, nexus.NoValue]{ + Name: "cancelOrder", + Start: func( + ctx context.Context, + nc temporalnexus.NexusClient, + input CancelOrderInput, + _ temporalnexus.StartTemporalOperationOptions, + ) (temporalnexus.TemporalOperationResult[nexus.NoValue], error) { + err := nc.GetWorkflowClient().SignalWorkflow( + ctx, "order-"+input.OrderID, "", "requestCancellation", input) + if err != nil { + return temporalnexus.TemporalOperationResult[nexus.NoValue]{}, err + } + return temporalnexus.NewSyncResult[nexus.NoValue](nil), nil + }, + }) +``` + + + + +```java +@OperationImpl +public OperationHandler cancelOrder() { + return TemporalOperationHandler.create( + (context, client, input) -> { + client.getWorkflowClient() + .newUntypedWorkflowStub("order-" + input.getOrderId()) + .signal("requestCancellation", input); + return TemporalOperationResult.sync(null); + }); +} +``` + + + + +```python +@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) +``` + + + + +```typescript +const cancelOrder = new temporalnexus.TemporalOperationHandler({ + async start(ctx, client, input) { + await client.getWorkflowHandle(`order-${input.orderId}`).signal(requestCancellation, input); + return temporalnexus.TemporalOperationResult.sync(undefined); + }, +}); +``` + + + + +```csharp +TemporalOperationHandler.FromHandleFactory( + async (context, client, input) => + { + await client.TemporalClient + .GetWorkflowHandle($"order-{input.OrderId}") + .SignalAsync("requestCancellation", new object?[] { input }); + return TemporalOperationResult.SyncResult(default); + }); +``` + + + + +The same Client also offers Signal-with-Start, Cancel, and Terminate as sync messaging, and a handler may perform several before returning. + +### Back an Operation with an Activity + +There was no previous equivalent. +Exposing an Activity through Nexus meant wrapping it in a Workflow that did nothing but call it, so there is no "before" to compare against. + +Activity options require an Activity Id and a Task Queue here, because there is no parent Workflow to supply them. See [Nexus Standalone Activity](/nexus/standalone-activity). + + + + +```go +op := temporalnexus.MustNewTemporalOperation( + temporalnexus.TemporalOperationOptions[GreetingInput, GreetingOutput]{ + Name: "greet", + Start: func( + ctx context.Context, + nc temporalnexus.NexusClient, + input GreetingInput, + _ temporalnexus.StartTemporalOperationOptions, + ) (temporalnexus.TemporalOperationResult[GreetingOutput], error) { + return temporalnexus.StartActivity(ctx, nc, client.StartActivityOptions{ + ID: "greet-" + input.Name, + TaskQueue: TaskQueueName, + StartToCloseTimeout: 10 * time.Second, + }, GreetingActivities.Greet, input) + }, + }) +``` + + + + +```java +@OperationImpl +public OperationHandler greet() { + return TemporalOperationHandler.create( + (context, client, input) -> + client.startActivity( + GreetingActivities.class, + GreetingActivities::greet, + input, + StartActivityOptions.newBuilder() + .setId("greet-" + context.getRequestId()) + .setTaskQueue(HandlerWorker.TASK_QUEUE_NAME) + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build())); +} +``` + + + + +```csharp +TemporalOperationHandler.FromHandleFactory( + async (context, client, input) => + await client.StartActivityAsync( + () => GreetingActivities.GreetAsync(input), + new() + { + Id = $"greet-{input.Name}", + TaskQueue = TaskQueueName, + ScheduleToCloseTimeout = TimeSpan.FromMinutes(1), + })); +``` + + + + +```python + +``` + + + + +```typescript + +``` + + + + + +## What this replaces + +`WorkflowRunOperation` and the synchronous `OperationHandler` are **de-emphasized, not removed**. +Existing handlers keep working and there is no forced migration. + +Prefer `TemporalOperationHandler` for new work, including simple cases. +Using one type everywhere means a handler that starts out synchronous can grow an async backing, or pick up a Signal, without changing shape. + +Beyond the handler, [parent-close policy](/nexus/operations) parity with Child Workflows — deciding what happens to the handler Workflow when the caller completes, fails, or is cancelled — is still outstanding in every SDK. +Today, only cancellation propagates. + +:::tip RESOURCES + +- [Nexus Services](/nexus/services) and [Nexus Operations](/nexus/operations) for the underlying concepts. +- [Nexus Client Code Generator](/nexus/client-code-generator) to generate Service contracts and typed models from one schema. +- [Nexus Standalone Activity](/nexus/standalone-activity) for Activity-backed Operations. +- [Bidirectional linking](/nexus/execution-debugging#bi-directional-linking) for what the Nexus-aware Client gives you. +- [Development Walkthrough](/develop/java/nexus/development-walkthrough) builds a Nexus Service end to end using SDK V2. +- Nexus feature guides: + [Go](/develop/go/nexus/feature-guide) | + [Java](/develop/java/nexus/feature-guide) | + [Python](/develop/python/nexus/feature-guide) | + [TypeScript](/develop/typescript/nexus/feature-guide) + +::: diff --git a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx new file mode 100644 index 0000000000..eb4d837f38 --- /dev/null +++ b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx @@ -0,0 +1,254 @@ +--- +id: nexus-standalone-activity +title: Nexus Standalone Activity +sidebar_label: Nexus Standalone Activity +description: Back a Nexus Operation with a Standalone Activity instead of a Workflow, so exposing an Activity through Nexus needs no wrapper Workflow. +toc_max_heading_level: 4 +slug: /nexus/standalone-activity +keywords: + - nexus standalone activity + - activity backed nexus operation + - standalone activity + - start activity + - temporal operation handler +tags: + - Nexus + - Concepts +--- + +import { SdkTabs } from '@site/src/components'; + +:::caution + +Activity-backed Nexus Operations are pre-release and build on [Nexus SDK V2](/nexus/sdk-v2). +`TemporalOperationHandler` is marked experimental in the SDKs and may change in backwards-incompatible ways. + +::: + +A Nexus Operation can be backed by a [Standalone Activity](/standalone-activity) instead of a Workflow. +Starting the Operation starts an Activity Execution that has no parent Workflow, and the Operation completes when that Activity returns. + +This is the right shape when the work behind an Operation is a single step with no orchestration: call an external API, run a computation, send a notification. +Before Activity-backed Operations, exposing an Activity through Nexus meant writing a Workflow whose only job was to call that one Activity — a wrapper with its own Event History, its own Task Queue considerations, and no value of its own. + +These compose. A Standalone Nexus Operation can be backed by a Standalone Activity, which means neither side has a Workflow. +They are also independent: choosing an Activity-backed Operation says nothing about how callers invoke it. + +## How it works + +Use `TemporalOperationHandler` and call `startActivity` on the injected Nexus-aware Client. +The handler returns an async result carrying an activity-execution Operation token, and the server delivers the Activity's result to the caller through the Nexus completion callback when the Activity finishes. + +Click the language tabs to see example code in each language - Go, Java, .Net, Python, and Typescript. + +:::note +This is still a rough draft for feedback. Not all languages are filled in yet. + +::: + + + + +Code coming in next draft + + + + +```java +@ServiceImpl(service = GreetingNexusService.class) +public class GreetingNexusServiceImpl { + + @OperationImpl + public OperationHandler greet() { + return TemporalOperationHandler.create( + (context, client, input) -> + client.startActivity( + GreetingActivities.class, + GreetingActivities::greet, + input, + StartActivityOptions.newBuilder() + .setId("greet-" + context.getRequestId()) + .setTaskQueue(HandlerWorker.TASK_QUEUE_NAME) + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build())); + } +} +``` + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +The Activities themselves are ordinary Activities. +Nothing about them is Nexus-specific, and the same implementations can be called from a Workflow. +What makes them standalone is how they are started. + + + + +Code coming in next draft + + + + +```java +@ActivityInterface +public interface GreetingActivities { + @ActivityMethod + GreetingOutput greet(GreetingInput input); +} +``` + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +### Required options + +`StartActivityOptions` requires two values that a Workflow-called Activity does not need. + +- **An Activity ID**, unique within the Namespace. There is no parent Workflow to scope it. +- **A Task Queue.** It does not have to be the Task Queue the Nexus Endpoint targets, so the Activity can run on its own Worker fleet. + +Deriving the ID from the Nexus request ID makes the start idempotent. +The server retries a Nexus start request using the same request ID, so each retry targets the same Activity ID rather than starting a second Activity. + +Setting `setIdConflictPolicy(ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING)` attaches to an already-running Activity with that ID instead of failing. +Combined with an ID derived from the Operation *input* rather than the request ID, this lets several Nexus Operations share one Activity Execution and all receive its result. + +### Register the Worker + +Register the Activity implementations and the Nexus Service implementation on a Worker polling the Endpoint's target Task Queue. +There is no Workflow implementation to register. + + + + +Code coming in next draft + + + + +```java +Worker worker = factory.newWorker(TASK_QUEUE_NAME); +worker.registerActivitiesImplementations(new GreetingActivitiesImpl()); +worker.registerNexusServiceImplementation(new GreetingNexusServiceImpl()); +``` + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +## Cancellation requires heartbeating + +This is the biggest behavioral difference from a Workflow-backed Operation, and the easiest thing to get wrong. + +A Workflow is interrupted by a cancellation request: a blocking call throws and, if the failure propagates, the Workflow and its Operation both end as cancelled. +An Activity is not interrupted. +The server records the cancellation request, and the Worker only learns about it on the next heartbeat. + +So an Activity that never heartbeats runs until it completes or hits its start-to-close timeout, no matter how many cancellation requests the caller sends. +For a long-running Activity-backed Operation to be cancellable at all: + +- Heartbeat from the Activity, and let the resulting completion exception propagate. +- Set a heartbeat timeout so the server notices a Worker that has stopped heartbeating. +- Set maximum attempts to 1, or a cancelled attempt is retried and the Operation stays running instead of ending as cancelled. + + + + +Code coming in next draft + + + + +```java +StartActivityOptions.newBuilder() + .setId("greeting-" + context.getRequestId()) + .setTaskQueue(HandlerWorker.TASK_QUEUE_NAME) + .setStartToCloseTimeout(Duration.ofMinutes(10)) + .setHeartbeatTimeout(Duration.ofSeconds(5)) + .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).build()) + .build(); +``` + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +Code coming in next draft + + + + +For a short Activity that finishes well inside its timeout, none of this applies. + +## Choose between an Activity and a Workflow + +Back an Operation with an **Activity** when the work is a single step: one external call, one computation, one notification. +You get no Event History for orchestration you are not doing, and no wrapper Workflow to maintain. + +Back an Operation with a **Workflow** when the work has more than one step, needs to wait for something, needs to receive [messages](/sending-messages), or needs durable intermediate state. +For example, an approval that blocks for a human decision is a Workflow, not an Activity. + +Sample code: `{code not yet live}` + +:::tip RESOURCES + +- [Nexus SDK V2](/nexus/sdk-v2) for `TemporalOperationHandler` and the Nexus-aware Client. +- [Standalone Activity](/standalone-activity) for the underlying concept, and [Java: Standalone Activities](/develop/java/activities/standalone-activities) for the SDK API. +- [Standalone Nexus Operation](/standalone-nexus-operation) for starting Operations without a caller Workflow. +- [Development Walkthrough](/develop/java/nexus/development-walkthrough) uses an Activity-backed Operation in context. + +::: diff --git a/sidebars.js b/sidebars.js index d0f8f20b1d..232ae8219e 100644 --- a/sidebars.js +++ b/sidebars.js @@ -416,6 +416,28 @@ const developJavaCategory = { 'develop/java/nexus/quickstart', 'develop/java/nexus/feature-guide', 'develop/java/nexus/standalone-operations', + { + type: 'category', + label: 'Development Walkthrough', + collapsed: true, + link: { + type: 'doc', + id: 'develop/java/nexus/development-walkthrough/index', + }, + items: [ + 'develop/java/nexus/development-walkthrough/define-the-data-contract', + 'develop/java/nexus/development-walkthrough/generate-code', + 'develop/java/nexus/development-walkthrough/choose-backing-implementation', + 'develop/java/nexus/development-walkthrough/implement-the-service', + 'develop/java/nexus/development-walkthrough/publish-in-nexus', + 'develop/java/nexus/development-walkthrough/call-the-service', + 'develop/java/nexus/development-walkthrough/add-messaging', + 'develop/java/nexus/development-walkthrough/send-messages', + 'develop/java/nexus/development-walkthrough/add-a-standalone-activity', + 'develop/java/nexus/development-walkthrough/call-the-standalone-activity', + 'develop/java/nexus/development-walkthrough/debugging-and-tips', + ], + }, ], }, { @@ -2069,7 +2091,9 @@ module.exports = { }, items: [ 'encyclopedia/nexus/nexus-services', + 'encyclopedia/nexus/nexus-sdk-v2', 'encyclopedia/nexus/nexus-client-code-generator', + 'encyclopedia/nexus/nexus-standalone-activity', 'encyclopedia/nexus/nexus-operations', 'encyclopedia/nexus/standalone-nexus-operation', 'encyclopedia/nexus/nexus-endpoints', From 1223ad1eae6f85e5280a244bab86caf224a35257 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Fri, 7 Aug 2026 15:23:56 -0700 Subject: [PATCH 03/16] Updating links --- .../java/nexus/development-walkthrough/add-messaging.mdx | 2 +- .../development-walkthrough/call-the-standalone-activity.mdx | 2 +- .../java/nexus/development-walkthrough/debugging-and-tips.mdx | 4 ++-- .../nexus/development-walkthrough/implement-the-service.mdx | 2 +- .../java/nexus/development-walkthrough/send-messages.mdx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx index 303d58a192..09e82bfc22 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -59,7 +59,7 @@ Because it is an async backing, there is at most one per Operation invocation. A Query works through `client.getWorkflowClient()`, but [bidirectional linking](/nexus/execution-debugging#bi-directional-linking) for Query is still in progress and is not yet available in any SDK. A Query sent from a handler is not connected to the caller in the UI the way a Signal is. -The Operation behaves correctly; only the observability link is missing. See the [per-SDK support table](/nexus/sdk-v2#per-sdk-support) for current status. +The Operation behaves correctly; only the observability link is missing. See [Nexus SDK V2](/nexus/sdk-v2) for current status. ::: diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx index 30924d1b7c..5a432672e1 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -41,7 +41,7 @@ Every step crossed a Namespace boundary, and the caller never learned a Workflow Open the caller Workflow in the UI and follow the links. Because the handlers used the Nexus-aware Client, each Operation is connected to the Execution it started or messaged in the handler Namespace, and you can move between the two Namespaces in one view. -The exception is Query: [linking for Query is still in progress](/nexus/sdk-v2#per-sdk-support), so `getApprovalStatus` will not show a link yet. +The exception is Query: [linking for Query is still in progress](/nexus/sdk-v2), so `getApprovalStatus` will not show a link yet. ## Where to go next diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx index aeea78e903..2b1336e10a 100644 --- a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -38,7 +38,7 @@ The handler used its own Temporal Client instead of the one `TemporalOperationHa Fetching a Client yourself works, and the Operation behaves correctly, but you lose the [bidirectional links](/nexus/execution-debugging#bi-directional-linking) that connect the two Executions. Use the injected Client for anything that starts or messages an Execution. -The one current exception is Query, where [linking is still in progress](/nexus/sdk-v2#per-sdk-support) and no SDK produces a link yet. +The one current exception is Query, where [linking is still in progress](/nexus/sdk-v2) and no SDK produces a link yet. ## A Query returns stale data, hangs, or throws @@ -62,7 +62,7 @@ Use the Operation result for outcomes. Use a Query for in-flight progress. ### Expecting to re-attach to a running Operation -There is no Operation that attaches to an already-running Execution and waits for its result. Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2#per-sdk-support). +There is no Operation that attaches to an already-running Execution and waits for its result. Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2). Whoever starts the Operation is who receives the result. If other systems need it, distribute it from the caller or notify them from the handler. diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx index 3b69ff8ec3..47d596a3bf 100644 --- a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -45,7 +45,7 @@ Set the Workflow Id from the approval id, as decided in [step 3](/develop/java/n :::note -You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Compare the old and new handlers](/nexus/sdk-v2#compare-the-old-and-new-handlers). +You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Updated handler methods](/nexus/sdk-v2#updated-handler-methods). ::: diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx index 8352d9b613..96fe8a550b 100644 --- a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -42,7 +42,7 @@ This is also how you make a Signal safe to send to an approval that may not exis :::caution Update-with-Start is not yet available -Update-with-Start — starting a Workflow and running an Update against it atomically — is not available in any SDK yet. See the [per-SDK support table](/nexus/sdk-v2#per-sdk-support). +Update-with-Start — starting a Workflow and running an Update against it atomically — is not available in any SDK yet. See [Nexus SDK V2](/nexus/sdk-v2). Until it lands, an Operation that needs both must start the Workflow and then submit the Update as a separate call, which is not atomic. Signal-with-Start is available and covers the case where the caller does not need a response. @@ -52,7 +52,7 @@ Until it lands, an Operation that needs both must start the Workflow and then su A caller that did not start an approval cannot ask Nexus for its final decision. -`requestApproval` delivers the decision to whoever awaited it. There is no Operation that attaches to an already-running approval and waits for its result, because Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2#per-sdk-support) in any SDK. +`requestApproval` delivers the decision to whoever awaited it. There is no Operation that attaches to an already-running approval and waits for its result, because Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2) in any SDK. If several systems need the outcome, either have the caller that started the approval distribute it, or have the handler notify them — which is what the notification in the next step does. From 9d5c281979c8292b60df6dcbbf449bdf64d43952 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 10 Aug 2026 17:26:39 -0700 Subject: [PATCH 04/16] Updating docs --- .../development-walkthrough/add-messaging.mdx | 14 +- .../call-the-standalone-activity.mdx | 2 +- .../debugging-and-tips.mdx | 2 +- .../implement-the-service.mdx | 2 +- .../nexus/development-walkthrough/index.mdx | 17 +- .../development-walkthrough/send-messages.mdx | 2 +- docs/encyclopedia/nexus/nexus-sdk-v2.mdx | 350 +++++++++--------- .../nexus/nexus-standalone-activity.mdx | 82 +++- 8 files changed, 265 insertions(+), 206 deletions(-) diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx index 09e82bfc22..999191d3a8 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -37,13 +37,13 @@ Two constraints apply to the Query handler. It must not block, and it must not m ## Expose them as Nexus Operations -All three use `TemporalOperationHandler`, but they divide along the line described in [Nexus SDK V2](/nexus/sdk-v2#the-nexus-aware-client): Signal and Query are **sync side effects**, and Update is an **async backing**. +These use `TemporalOperationHandler`, and they divide along the line described in [Nexus SDK V2](/nexus/sdk-v2#the-nexus-aware-client): Signal is **sync messaging**, and Update is an **async backing**. -### Signal and Query +### Signal -Reach these through `client.getWorkflowClient()` on the Nexus-aware Client, then return `TemporalOperationResult.sync(...)`. The Operation completes immediately, during the handler call. +Reach it through `client.getWorkflowClient()` on the Nexus-aware Client, then return `TemporalOperationResult.sync(...)`. The Operation completes immediately, during the handler call. -You can perform as many sync side effects as you want in one handler. Using the injected Client rather than your own is what gets the message linked back to the caller. +You can send as many messages as you want in one handler. Using the injected Client rather than your own is what gets the message linked back to the caller. `{sample code will be here}` @@ -55,11 +55,11 @@ Because it is an async backing, there is at most one per Operation invocation. A `{sample code will be here}` -:::caution Query linking is not complete +:::caution Query is not in the pre-release -Query works through `client.getWorkflowClient()`, but [bidirectional linking](/nexus/execution-debugging#bi-directional-linking) for Query is still in progress and is not yet available in any SDK. A Query sent from a handler is not connected to the caller in the UI the way a Signal is. +`getApprovalStatus` belongs in the contract, but Query is not available as sync messaging on the Nexus-aware Client in the pre-release, so this Operation cannot be implemented yet. See [Nexus SDK V2](/nexus/sdk-v2). -The Operation behaves correctly; only the observability link is missing. See [Nexus SDK V2](/nexus/sdk-v2) for current status. +Adding the Query handler to the Workflow is still worth doing — it costs nothing and the Operation can be wired up once Query lands. Until then, a caller that needs in-flight progress has to obtain it outside this Service. ::: diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx index 5a432672e1..faa5dea9a1 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -41,7 +41,7 @@ Every step crossed a Namespace boundary, and the caller never learned a Workflow Open the caller Workflow in the UI and follow the links. Because the handlers used the Nexus-aware Client, each Operation is connected to the Execution it started or messaged in the handler Namespace, and you can move between the two Namespaces in one view. -The exception is Query: [linking for Query is still in progress](/nexus/sdk-v2), so `getApprovalStatus` will not show a link yet. +The exception is `getApprovalStatus`, which is [not implementable in the pre-release](/nexus/sdk-v2) because Query is not yet available as sync messaging. ## Where to go next diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx index 2b1336e10a..71bc9cfd4d 100644 --- a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -38,7 +38,7 @@ The handler used its own Temporal Client instead of the one `TemporalOperationHa Fetching a Client yourself works, and the Operation behaves correctly, but you lose the [bidirectional links](/nexus/execution-debugging#bi-directional-linking) that connect the two Executions. Use the injected Client for anything that starts or messages an Execution. -The one current exception is Query, where [linking is still in progress](/nexus/sdk-v2) and no SDK produces a link yet. +Note that Query, Cancel, and Terminate are [not available as sync messaging](/nexus/sdk-v2) in the pre-release. You can still send them with a Client of your own, but nothing links those messages back to the caller. ## A Query returns stale data, hangs, or throws diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx index 47d596a3bf..df68d2751b 100644 --- a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -45,7 +45,7 @@ Set the Workflow Id from the approval id, as decided in [step 3](/develop/java/n :::note -You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Updated handler methods](/nexus/sdk-v2#updated-handler-methods). +You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Coming from the earlier handler APIs](/nexus/sdk-v2#coming-from-the-earlier-handler-apis). ::: diff --git a/docs/develop/java/nexus/development-walkthrough/index.mdx b/docs/develop/java/nexus/development-walkthrough/index.mdx index 35ec50d1bc..33653dc0e4 100644 --- a/docs/develop/java/nexus/development-walkthrough/index.mdx +++ b/docs/develop/java/nexus/development-walkthrough/index.mdx @@ -22,22 +22,17 @@ tags: This walkthrough covers [Nexus SDK V2](/nexus/sdk-v2), which is pre-release. APIs are experimental and may change in backwards-incompatible ways. +Please do NOT review the documents under this page past the high level structure. Once we agree on the structure and form I will be working on the docs to match. For now those subpage contents should be considered placeholder text. + ::: This walkthrough builds one Nexus Service from nothing to a complete API, adding a single Nexus capability at each step. -## Nexus Introduction - -A [Nexus Service](/nexus/services) is a contract that one team publishes and other teams call, across [Namespace](/namespaces) boundaries, without sharing code or a deployment. -Three things follow from that. - -**Durable microservices.** A Nexus Service turns Workflows and Activities into an API. Callers see Operations with typed inputs and outputs; they do not see your Workflow Ids, Task Queues, or retry policies. You keep the freedom to change what runs behind an Operation — swap an Activity for a Workflow, split one Workflow into several — as long as the contract holds. The reliability guarantees come along for free: an Operation backed by a Workflow is as durable as that Workflow. - -**A durable orchestration layer for AI agents and tools.** Agent systems call tools that are slow, flaky, and occasionally expensive. Exposing each tool as a Nexus Operation gives every call durable execution, automatic retries, and a record in [Event History](/encyclopedia/event-history) — and lets the agent and the tools live in different Namespaces, owned by different teams, written in different languages. See [Build AI applications with Temporal](/with-ai) for the wider picture. +## Nexus -**A shared facade for extensibility.** A Nexus Service can front something that is not a Temporal Workflow at all — an existing internal API, a legacy job queue, a third-party endpoint. Write the wrapper once, run it as one Worker fleet, and every team calls the same Operations instead of each writing its own integration. Because callers only depend on the contract, the team behind it can modify or update the service without breaking anyone. +A [Nexus Service](/evaluate/nexus) is a contract that one team publishes and other teams call, across [Namespace](/namespaces) boundaries, without sharing code or a deployment. -## The sample problem +## A sample problem A purchase request needs approval before it can proceed. @@ -51,7 +46,7 @@ Concretely, the Service needs to: - Accept a decision from the caller and confirm it was recorded - Send a notification when the decision is final -Each of those maps onto a different Nexus capability, which is what makes it a useful walkthrough. By the end, the Service exercises a Workflow-backed Operation, a Signal, a Query, an Update, and an Activity-backed Operation. +Each of those maps onto a different Nexus capability, which is what makes it a useful walkthrough. By the end, this sample service will exercise a Workflow-backed Operation, a Signal, a Query, an Update, and an Activity-backed Operation. ## One contract, every language diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx index 96fe8a550b..cf94a99c92 100644 --- a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -26,7 +26,7 @@ What differs between them is what you get back and how long it takes. `remindApprover` returns nothing and completes as soon as the Signal is accepted. Accepted is not the same as handled — the Signal is durably recorded and the Workflow will process it, but the Operation does not wait for that. If the caller needs confirmation that the nudge took effect, it needs an Update, not a Signal. -`getApprovalStatus` returns the current progress immediately. Call it when something needs to report on a pending approval; do not call it in a loop waiting for the decision. +`getApprovalStatus` returns the current progress immediately. Call it when something needs to report on a pending approval; do not call it in a loop waiting for the decision. Note that this Operation is [not implementable in the pre-release](/develop/java/nexus/development-walkthrough/add-messaging#expose-them-as-nexus-operations), because Query is not yet available as sync messaging. `submitDecision` returns confirmation that the decision was recorded. This is the point of using an Update: the caller learns the outcome of its own message. Once it succeeds, the approval Workflow unblocks and completes, which resolves the `requestApproval` Operation that the original caller is still awaiting. diff --git a/docs/encyclopedia/nexus/nexus-sdk-v2.mdx b/docs/encyclopedia/nexus/nexus-sdk-v2.mdx index db64f6c765..0f8f2476d9 100644 --- a/docs/encyclopedia/nexus/nexus-sdk-v2.mdx +++ b/docs/encyclopedia/nexus/nexus-sdk-v2.mdx @@ -2,7 +2,7 @@ id: nexus-sdk-v2 title: Nexus SDK V2 sidebar_label: Nexus SDK V2 -description: Nexus SDK V2 replaces the per-primitive Nexus Operation helpers with a single Temporal Operation Handler that supports every Temporal primitive and propagates bidirectional links automatically. +description: Implement Nexus Operations with the Temporal Operation Handler - back an Operation with a Workflow, an Update, or an Activity, and get bidirectional linking across the Namespace boundary. toc_max_heading_level: 4 slug: /nexus/sdk-v2 keywords: @@ -11,7 +11,6 @@ keywords: - nexus sdk ergonomics - nexus signal - nexus update - - nexus query - bidirectional linking tags: - Nexus @@ -28,32 +27,34 @@ Nexus SDK V2 is pre-release. ::: -Nexus SDK V2 changes how you implement a [Nexus Service](/nexus/services) contract. -Instead of one helper type per Temporal primitive, there is a single handler type — `TemporalOperationHandler` — that can back an Operation with any Temporal primitive and that carries [bidirectional links](/nexus/execution-debugging#bi-directional-linking) across the Namespace boundary for you. +A [Nexus Service](/nexus/services) publishes Operations that other teams call across [Namespace](/namespaces) boundaries. +`TemporalOperationHandler` is how you implement those Operations. -Nothing on the wire changes, and no existing Operation stops working. -The Service contract, [Nexus Endpoint](/nexus/endpoints) setup, and Worker registration are the same as before. -What changes is the handler you write. +It is a single handler type that can back an Operation with any Temporal primitive — start a Workflow, run an Update, start an Activity — or complete the Operation inline with no backing Execution at all. +Whichever you choose, the handler is the same shape, and every Execution it touches is connected back to the caller automatically. -## Why it changed +## What you can do with it -Before SDK V2, only one pattern had first-class support: start a Workflow and return its result. -Everything else meant reaching for a lower-level API. +**Back an Operation with whichever primitive fits the work.** A multi-step process is a Workflow. A single durable step is an [Activity](/nexus/standalone-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. -- **Only one primitive was ergonomic.** `WorkflowRunOperation` covered "start a Workflow and wait." Signal, Update, and Query had no equivalent, so teams wrote synchronous handlers that reached for a Temporal Client by hand. -- **Nexus calls were hard to find.** The synchronous handler API lives in the separate `nexus-rpc` SDK rather than the Temporal SDK, so developers looking through Temporal's own API surface did not find it. -- **Hand-wired handlers lost observability.** A synchronous handler that grabbed a Client itself did not produce bidirectional links, so the caller-side and handler-side Executions were not connected in the UI. Bidirectional linking is quite useful but wasn't always present. +**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. -SDK V2 addresses all three by making one handler type the entry point for every Temporal-backed Operation, and by injecting a Nexus-aware Client that does the linking. +**Get observability across the Namespace boundary without wiring it.** The Client handed to your handler propagates [bidirectional links](/nexus/execution-debugging#bi-directional-linking) and request Ids on every call it makes. The caller-side and handler-side Executions are connected in the UI and in [Event History](/encyclopedia/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 pick up a Signal, without changing handler type or breaking its contract. ## The Nexus-aware Client `TemporalOperationHandler.create(...)` gives your start handler three things: a context, a Client, and the Operation input. -The Client propagates bidirectional links and request IDs automatically, so every Execution it starts or messages is connected back to the caller in the UI and in [Event History](/encyclopedia/event-history). -Reaching for your own Client inside a handler still works, but it gives up that linking. +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 matters. +The Client exposes two kinds of call, and the distinction shapes how you write the handler. **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. @@ -64,14 +65,16 @@ The Client exposes two kinds of call, and the distinction matters. **Sync messaging — as many as you need.** Reach these through `client.getWorkflowClient()`. They take effect during the handler call, still get link propagation, and do not require an async backing. -- Signal, Signal-with-Start, Query, Cancel, and Terminate +- Signal and Signal-with-Start -A single handler can combine both: perform a sync Signal to unblock something, then return an async backing whose result the caller waits on. -A handler that only performs sync side effects returns `TemporalOperationResult.sync(...)` and the Operation completes immediately. +Query, Cancel, and Terminate are not part of the pre-release. +You can still reach them through a Temporal Client of your own, but a message sent that way is not linked back to the caller. -## Updated handler methods +A handler that only sends messages returns `TemporalOperationResult.sync(...)`, and the Operation completes immediately. -The following examples use a Nexus Service with a `startGreeting` Operation backed by a Workflow and a `greet` Operation that completes inline. Click the language tabs to see example code in each language - Go, Java, .Net, Python, and Typescript. +## Write an Operation handler + +The examples below use a Nexus Service with a `startGreeting` Operation backed by a Workflow, a `cancelOrder` Operation that sends a Signal, and a `greet` Operation backed by an Activity. Click the language tabs to see example code in each language - Go, Java, .Net, Python, and Typescript. :::note This is still a rough draft for feedback. Not all languages are filled in yet. @@ -80,82 +83,7 @@ This is still a rough draft for feedback. Not all languages are filled in yet. ### Back an Operation with a Workflow -Before, each SDK had a dedicated Workflow-run helper. It reached the Temporal Client through the Operation context rather than being handed one, and it returned a Workflow handle or method reference rather than an Operation result: - - - - -```go -op := temporalnexus.NewWorkflowRunOperation( - "startGreeting", - GreetingWorkflow, - func(ctx context.Context, input GreetingInput, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { - return client.StartWorkflowOptions{ - ID: "greeting-" + input.Name, - }, nil - }) -``` - - - - -```java -@OperationImpl -public OperationHandler startGreeting() { - return WorkflowRunOperation.fromWorkflowMethod( - (ctx, details, input) -> - Nexus.getOperationContext() - .getWorkflowClient() - .newWorkflowStub( - GreetingWorkflow.class, - WorkflowOptions.newBuilder() - .setWorkflowId("greeting-" + input.getName()) - .build()) - ::greet); -} -``` - - - - -```python -@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}" - ) -``` - - - - -```typescript -const startGreeting = new temporalnexus.WorkflowRunOperationHandler( - async (ctx, input: GreetingInput) => - await temporalnexus.startWorkflow(ctx, greetingWorkflow, { - args: [input], - workflowId: `greeting-${input.name}`, - }), -); -``` - - - - -```csharp -WorkflowRunOperationHandler.FromHandleFactory( - async (context, input) => - await context.StartWorkflowAsync( - (GreetingWorkflow wf) => wf.RunAsync(input), - new() { Id = $"greeting-{input.Name}" })); -``` - - - - -Now the Client is handed to your start handler, and you call its start method directly. The return value is a `TemporalOperationResult`, which is what lets the same handler shape also return a synchronous result or an Activity-backed one: +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. @@ -243,67 +171,7 @@ Go exposes the start calls as package-level functions taking the Client, rather ### Send a Signal from an Operation -Before, a Signal-sending Operation was a synchronous handler that fetched its own Client. -This is the pattern that produced no bidirectional links: - - - - -```go -// nexus.NewSyncOperation comes from the separate nexus-rpc SDK, not from temporalnexus. -op := nexus.NewSyncOperation("cancelOrder", - func(ctx context.Context, input CancelOrderInput, o nexus.StartOperationOptions) (nexus.NoValue, error) { - c := temporalnexus.GetClient(ctx) - return nil, c.SignalWorkflow(ctx, "order-"+input.OrderID, "", "requestCancellation", input) - }) -``` - - - - -```java -@OperationImpl -public OperationHandler cancelOrder() { - return OperationHandler.sync( - (ctx, details, input) -> { - Nexus.getOperationContext() - .getWorkflowClient() - .newUntypedWorkflowStub("order-" + input.getOrderId()) - .signal("requestCancellation", input); - return null; - }); -} -``` - - - - - ```csharp - ``` - - - - -```python -@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) -``` - - - - - ```typescript -``` - - - - -Now the same Operation uses the injected Client, so the Signal is linked. It returns a synchronous result rather than a bare value, because the handler type is the same one used for async backings: +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. @@ -390,14 +258,11 @@ TemporalOperationHandler.FromHandleFactory( -The same Client also offers Signal-with-Start, Cancel, and Terminate as sync messaging, and a handler may perform several before returning. +The same Client also offers Signal-with-Start, and a handler may send several messages before returning. ### Back an Operation with an Activity -There was no previous equivalent. -Exposing an Activity through Nexus meant wrapping it in a Workflow that did nothing but call it, so there is no "before" to compare against. - -Activity options require an Activity Id and a Task Queue here, because there is no parent Workflow to supply them. See [Nexus Standalone Activity](/nexus/standalone-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 a Task Queue. See [Nexus Standalone Activity](/nexus/standalone-activity). @@ -460,31 +325,170 @@ TemporalOperationHandler.FromHandleFactory( -```python +Code coming in next draft + + + + +Code coming in next draft + + + + +## 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. + +| If you used | Use instead | +| --- | --- | +| The Workflow-run helper (`WorkflowRunOperation`, `NewWorkflowRunOperation`, `@workflow_run_operation`) | `TemporalOperationHandler` with `startWorkflow` | +| The synchronous handler (`OperationHandler.sync`, `nexus.NewSyncOperation`, `@sync_operation`) | `TemporalOperationHandler` returning a sync result | +| A Workflow wrapping a single Activity | `TemporalOperationHandler` with `startActivity` | +| 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](/nexus/execution-debugging#bi-directional-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: + + + + +```go +op := temporalnexus.NewWorkflowRunOperation( + "startGreeting", + GreetingWorkflow, + func(ctx context.Context, input GreetingInput, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ + ID: "greeting-" + input.Name, + }, nil + }) +``` + + + +```java +@OperationImpl +public OperationHandler startGreeting() { + return WorkflowRunOperation.fromWorkflowMethod( + (ctx, details, input) -> + Nexus.getOperationContext() + .getWorkflowClient() + .newWorkflowStub( + GreetingWorkflow.class, + WorkflowOptions.newBuilder() + .setWorkflowId("greeting-" + input.getName()) + .build()) + ::greet); +} +``` + + + + +```python +@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}" + ) ``` ```typescript - +const startGreeting = new temporalnexus.WorkflowRunOperationHandler( + async (ctx, input: GreetingInput) => + await temporalnexus.startWorkflow(ctx, greetingWorkflow, { + args: [input], + workflowId: `greeting-${input.name}`, + }), +); ``` + +```csharp +WorkflowRunOperationHandler.FromHandleFactory( + async (context, input) => + await context.StartWorkflowAsync( + (GreetingWorkflow wf) => wf.RunAsync(input), + new() { Id = $"greeting-{input.Name}" })); +``` + + -## What this replaces +Replace it with [Back an Operation with a Workflow](#back-an-operation-with-a-workflow). + +### Migrating a synchronous Operation -`WorkflowRunOperation` and the synchronous `OperationHandler` are **de-emphasized, not removed**. -Existing handlers keep working and there is no forced migration. +A messaging Operation used to be a synchronous handler that fetched its own Client, which is why those messages produced no links: -Prefer `TemporalOperationHandler` for new work, including simple cases. -Using one type everywhere means a handler that starts out synchronous can grow an async backing, or pick up a Signal, without changing shape. + + + +```go +// nexus.NewSyncOperation comes from the separate nexus-rpc SDK, not from temporalnexus. +op := nexus.NewSyncOperation("cancelOrder", + func(ctx context.Context, input CancelOrderInput, o nexus.StartOperationOptions) (nexus.NoValue, error) { + c := temporalnexus.GetClient(ctx) + return nil, c.SignalWorkflow(ctx, "order-"+input.OrderID, "", "requestCancellation", input) + }) +``` + + + + +```java +@OperationImpl +public OperationHandler cancelOrder() { + return OperationHandler.sync( + (ctx, details, input) -> { + Nexus.getOperationContext() + .getWorkflowClient() + .newUntypedWorkflowStub("order-" + input.getOrderId()) + .signal("requestCancellation", input); + return null; + }); +} +``` + + + + +```python +@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) +``` + + + + +Code coming in next draft + + + + +Code coming in next draft + + + -Beyond the handler, [parent-close policy](/nexus/operations) parity with Child Workflows — deciding what happens to the handler Workflow when the caller completes, fails, or is cancelled — is still outstanding in every SDK. -Today, only cancellation propagates. +Replace it with [Send a Signal from an Operation](#send-a-signal-from-an-operation). :::tip RESOURCES diff --git a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx index eb4d837f38..4d3ef086ab 100644 --- a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx +++ b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx @@ -2,7 +2,7 @@ id: nexus-standalone-activity title: Nexus Standalone Activity sidebar_label: Nexus Standalone Activity -description: Back a Nexus Operation with a Standalone Activity instead of a Workflow, so exposing an Activity through Nexus needs no wrapper Workflow. +description: Back a Nexus Operation with a Standalone Activity. toc_max_heading_level: 4 slug: /nexus/standalone-activity keywords: @@ -20,19 +20,79 @@ import { SdkTabs } from '@site/src/components'; :::caution -Activity-backed Nexus Operations are pre-release and build on [Nexus SDK V2](/nexus/sdk-v2). +Activity-backed Nexus Operations are pre-release and built on [Nexus SDK V2](/nexus/sdk-v2). `TemporalOperationHandler` is marked experimental in the SDKs and may change in backwards-incompatible ways. ::: -A Nexus Operation can be backed by a [Standalone Activity](/standalone-activity) instead of a Workflow. +A Nexus Operation can be backed by a [Standalone Activity](/standalone-activity) as well as a Workflow. Starting the Operation starts an Activity Execution that has no parent Workflow, and the Operation completes when that Activity returns. -This is the right shape when the work behind an Operation is a single step with no orchestration: call an external API, run a computation, send a notification. -Before Activity-backed Operations, exposing an Activity through Nexus meant writing a Workflow whose only job was to call that one Activity — a wrapper with its own Event History, its own Task Queue considerations, and no value of its own. +Reach for this shape when the work behind an Operation is a single durable step rather than a process: call an external API, run a computation, write to another system. -These compose. A Standalone Nexus Operation can be backed by a Standalone Activity, which means neither side has a Workflow. -They are also independent: choosing an Activity-backed Operation says nothing about how callers invoke it. +Two things combine here, and it is worth separating them. +An [Activity](/activities) gives that step automatic retries, timeouts, and a durable record of what happened. +Exposing it as a Nexus Operation puts a typed contract and a [Namespace](/namespaces) boundary in front of it, so another team can call it without sharing your code, your deployment, or write access to your Namespace. +Because the Activity carries the durability, the Operation needs no Workflow behind it, and uses fewer [Billable Actions](/cloud/actions-usage#actions-in-workflows) in Temporal Cloud than running the same single step through one. + +## When to use an Activity-backed Operation + +The shape fits work that is one durable step sitting behind a team boundary. +You get all of the following without building any of it yourself: + +- **Durability per call.** Retries on the policy you set, timeouts you control, and a record of every attempt including the last error. +- **Deduplicated starts.** A stable Activity Id means a retried or redelivered request targets the same Activity Execution instead of starting a second one, so you do not need a separate store of processed Ids. +- **Protection from a failing dependency.** Repeated retryable failures trip the [circuit breaker](/nexus/operations#circuit-breaking) rather than piling retries onto a system that is already struggling. +- **A boundary between teams.** Callers get scoped access to named Operations, not write access to your Namespace, and they never see your Task Queues, your Workers, or your implementation. +- **Room to change your mind.** As long as the contract holds you can rewrite what runs behind an Operation, including replacing the Activity with a Workflow later, and no caller changes. + +Callers are free either way: a caller Workflow can invoke the Operation as one step of a larger process, or a Client can start it directly as a [Standalone Nexus Operation](/standalone-nexus-operation) with no Workflow on either side. + +Here are some example use cases. + +### Durable webhook and event processing without running a queue + +A service ingests webhooks from third-party providers — payment events, repository pushes, delivery receipts, CRM change notifications. +Each one needs to trigger a single durable task: update a downstream system, kick off a notification, index into search, sync into a warehouse. + +Providers retry aggressively if you do not return `200` within seconds, so the receiver has to accept fast and do the work elsewhere. +The usual way to get there is to assemble it yourself: a queue, a consumer fleet, hand-written retry and dead-letter handling, and a separate store holding processed event Ids for deduplication. +That is a lot of infrastructure whose only job is to run one function reliably. + +An Activity-backed Operation replaces the whole assembly. +The receiver starts the Operation and returns `200` immediately; Temporal owns delivery from that point. +The provider's own event Id becomes the Activity Id, so a redelivered webhook targets the same Activity Execution instead of starting a second one, and the deduplication store disappears. +Retries, backoff, and the record of every attempt come from the Activity. +When a downstream dependency starts failing, the [circuit breaker](/nexus/operations#circuit-breaking) trips rather than letting retries pile up against it. + +The team that owns the receiver is usually not the team that owns the processing, and the Nexus Endpoint is where that boundary lands: scoped access to specific Operations rather than write access to a Namespace. + +### Sandboxing a tool call + +An agent needs to call a tool — an MCP tool, an internal API, something that executes untrusted input. +You want that call to run somewhere with its own credentials, its own network reach, and its own blast radius, not inside the process orchestrating the agent. + +An Activity-backed Operation puts a Namespace boundary between the two. +The tool runs on Workers in the Namespace that owns it, under that Namespace's credentials, and the caller only ever sees the Operation contract. +The call is still durable and retried, and it is still traceable end to end, but the caller cannot reach past the contract into the environment the tool runs in. +See [Build AI applications with Temporal](/with-ai) for how this fits alongside the rest of the agent stack. + +### A durable front door to another system + +Any system you call can fail, time out, or be down when you need it — a third-party API, a legacy internal service, an unreliable piece of infrastructure. +Wrapping that call in an Activity-backed Operation makes every call against it durable: retried on your policy, bounded by your timeouts, and recorded whether it succeeded or not. + +Written once and published as a Nexus Service, it becomes a connector that every team calls instead of each writing its own integration. +The team that owns it can fix a bug or change the implementation behind the contract, without a coordinated rollout across every consumer. + +### Related patterns + +The same shape fits anything that is an external trigger, one durable step, and a team boundary. + +- **Asynchronous user actions from a backend-for-frontend.** A user clicks "export my data" or "revoke my sessions"; the BFF starts the Operation and hands the client a handle to poll. +- **Consumer offload.** A Kafka or event-stream consumer starts an Operation, commits its offset, and lets Temporal own durability from there. The event Id is the deduplication key. +- **Platform actions triggered by CI/CD.** A pipeline step requests a compliance scan, a canary step, or a credential rotation. The platform team owns the handler; consumer teams get scoped Endpoint access. +- **Scheduled platform tasks.** A scheduler fires an Operation and a shared platform team's Workers run the task. ## How it works @@ -93,9 +153,9 @@ Code coming in next draft -The Activities themselves are ordinary Activities. -Nothing about them is Nexus-specific, and the same implementations can be called from a Workflow. -What makes them standalone is how they are started. +You write the Activity the same way whichever side calls it. +The same Activity Function can be executed by a Workflow and started behind a Nexus Operation with no code changes — nothing in its definition is Nexus-specific. +What differs is how it is started, not what it is. @@ -237,7 +297,7 @@ For a short Activity that finishes well inside its timeout, none of this applies ## Choose between an Activity and a Workflow Back an Operation with an **Activity** when the work is a single step: one external call, one computation, one notification. -You get no Event History for orchestration you are not doing, and no wrapper Workflow to maintain. +You get retries, timeouts, and a durable record of the step, without an Event History tracking orchestration you are not doing. Back an Operation with a **Workflow** when the work has more than one step, needs to wait for something, needs to receive [messages](/sending-messages), or needs durable intermediate state. For example, an approval that blocks for a human decision is a Workflow, not an Activity. From bd55d8d14751bd4f5ab0fadfedea9b061ec4d4f2 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Tue, 11 Aug 2026 16:23:47 -0700 Subject: [PATCH 05/16] Unlisted the developer experience files --- .../add-a-standalone-activity.mdx | 1 + .../development-walkthrough/add-messaging.mdx | 1 + .../call-the-service.mdx | 1 + .../call-the-standalone-activity.mdx | 1 + .../choose-backing-implementation.mdx | 1 + .../debugging-and-tips.mdx | 1 + .../define-the-data-contract.mdx | 1 + .../development-walkthrough/generate-code.mdx | 1 + .../implement-the-service.mdx | 1 + .../nexus/development-walkthrough/index.mdx | 1 + .../publish-in-nexus.mdx | 1 + .../development-walkthrough/send-messages.mdx | 1 + docs/encyclopedia/nexus/nexus.mdx | 11 ++++++++++ sidebars.js | 22 ------------------- 14 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx index cad4003dbe..9dfe304203 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx @@ -4,6 +4,7 @@ title: Step 9 - Add a Standalone Activity sidebar_label: 9. Add a Standalone Activity description: Back the notification Nexus Operation with a Standalone Activity instead of a Workflow, with no wrapper Workflow required. toc_max_heading_level: 4 +unlisted: true keywords: - nexus standalone activity - activity backed operation diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx index 999191d3a8..cef82db8b9 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -4,6 +4,7 @@ title: Step 7 - Add messaging sidebar_label: 7. Add messaging description: Expose Signal, Query, and Update on the approval Workflow as Nexus Operations using the Nexus-aware Client. toc_max_heading_level: 4 +unlisted: true keywords: - nexus signal - nexus query diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx index ea8a4f2ab8..d7c181719c 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx @@ -4,6 +4,7 @@ title: Step 6 - Call the Service sidebar_label: 6. Call the Service description: Call the approval Nexus Service from a caller Workflow in another Namespace using the generated Service interface. toc_max_heading_level: 4 +unlisted: true keywords: - nexus caller workflow - call nexus operation diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx index faa5dea9a1..f9212f0a96 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -4,6 +4,7 @@ title: Step 10 - Call the Standalone Activity sidebar_label: 10. Call the Standalone Activity description: Call the Activity-backed notification Operation from a caller Workflow and complete the approval flow end to end. toc_max_heading_level: 4 +unlisted: true keywords: - call nexus operation - activity backed operation diff --git a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx index 3c46bb7b51..63536f4510 100644 --- a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx +++ b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx @@ -4,6 +4,7 @@ title: Step 3 - Choose the backing implementation sidebar_label: 3. Choose the backing implementation description: Decide whether each Nexus Operation is backed by a Standalone Activity, a Workflow, or an Entity Workflow. toc_max_heading_level: 4 +unlisted: true keywords: - nexus operation backing - entity workflow diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx index 71bc9cfd4d..b6899bb277 100644 --- a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -4,6 +4,7 @@ title: Debugging, common pitfalls, and tips sidebar_label: Debugging and tips description: Diagnose the most common Nexus failures, from Endpoint authorization to Query constraints, and avoid the pitfalls that are easy to miss. toc_max_heading_level: 4 +unlisted: true keywords: - nexus debugging - nexus troubleshooting diff --git a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx index d8c5dadb24..c8f92359fc 100644 --- a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx +++ b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx @@ -4,6 +4,7 @@ title: Step 1 - Define the data contract sidebar_label: 1. Define the data contract description: Plan an approval Nexus Service and write its data contract before any implementation, so every language shares one definition. toc_max_heading_level: 4 +unlisted: true keywords: - nexus data contract - nexus service contract diff --git a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx index 13d0ec5269..768c887599 100644 --- a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx +++ b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx @@ -4,6 +4,7 @@ title: Step 2 - Generate code from the contract sidebar_label: 2. Generate code description: Use the Nexus Client Code Generator to turn the approval contract into typed models, validators, and Service definitions for the handler and the caller. toc_max_heading_level: 4 +unlisted: true keywords: - nexus code generation - nexgen diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx index df68d2751b..c511e01016 100644 --- a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -4,6 +4,7 @@ title: Step 4 - Implement the Service sidebar_label: 4. Implement the Service description: Implement the approval Nexus Service in Java using TemporalOperationHandler, backed by a Workflow, and run a Worker that hosts it. toc_max_heading_level: 4 +unlisted: true keywords: - temporal operation handler - nexus service implementation diff --git a/docs/develop/java/nexus/development-walkthrough/index.mdx b/docs/develop/java/nexus/development-walkthrough/index.mdx index 33653dc0e4..ca9626008c 100644 --- a/docs/develop/java/nexus/development-walkthrough/index.mdx +++ b/docs/develop/java/nexus/development-walkthrough/index.mdx @@ -4,6 +4,7 @@ title: Nexus Development Walkthrough - Java SDK sidebar_label: Development Walkthrough description: Build a Nexus Service end to end in Java, starting from a data contract and adding one Nexus capability at a time to solve an approval problem. toc_max_heading_level: 4 +unlisted: true keywords: - nexus walkthrough - nexus java diff --git a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx index 1283bf7a86..cdab27070a 100644 --- a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx +++ b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx @@ -4,6 +4,7 @@ title: Step 5 - Publish in Nexus sidebar_label: 5. Publish in Nexus description: Create a Nexus Endpoint for the approval Service, allow caller Namespaces to reach it, and set up credentials for Temporal Cloud. toc_max_heading_level: 4 +unlisted: true keywords: - nexus endpoint - nexus registry diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx index cf94a99c92..ab60816392 100644 --- a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -4,6 +4,7 @@ title: Step 8 - Send messages sidebar_label: 8. Send messages description: Call the Signal, Query, and Update Operations from a caller Workflow, and start an approval with Signal-with-Start. toc_max_heading_level: 4 +unlisted: true keywords: - send nexus signal - nexus query caller diff --git a/docs/encyclopedia/nexus/nexus.mdx b/docs/encyclopedia/nexus/nexus.mdx index e79655f24a..a0388ba76e 100644 --- a/docs/encyclopedia/nexus/nexus.mdx +++ b/docs/encyclopedia/nexus/nexus.mdx @@ -130,3 +130,14 @@ Each step is a separate, durable Operation with its own retries and failure hand - [Nexus error handling](/nexus/error-handling) - Error types and how they surface in caller Workflows. - [Nexus metrics](/nexus/metrics) - SDK, Cloud, and OSS cluster metrics. +## Pre-release: the new Nexus developer experience + +A new way of building Nexus Services is in pre-release. It combines a contract-first workflow, where one schema generates typed models and Service definitions for every language, with a single handler type that can back an Operation with a Workflow, an Update, or an Activity. + +The APIs are experimental, so expect them to change. + +- [Nexus SDK V2](/nexus/sdk-v2) - Implement Operations with `TemporalOperationHandler`, and get bidirectional linking across the Namespace boundary. +- [Nexus Client Code Generator](/nexus/client-code-generator) - Generate typed models, validators, and Service definitions for Go, Java, Python, and TypeScript from one schema. +- [Nexus Standalone Activity](/nexus/standalone-activity) - Back an Operation with a single durable step instead of a Workflow. +- [Development Walkthrough (Java)](/develop/java/nexus/development-walkthrough) - Build a Nexus Service end to end, adding one capability at a time. This walkthrough is not in the navigation yet, so this link is its entry point. + diff --git a/sidebars.js b/sidebars.js index 232ae8219e..a043e13db1 100644 --- a/sidebars.js +++ b/sidebars.js @@ -416,28 +416,6 @@ const developJavaCategory = { 'develop/java/nexus/quickstart', 'develop/java/nexus/feature-guide', 'develop/java/nexus/standalone-operations', - { - type: 'category', - label: 'Development Walkthrough', - collapsed: true, - link: { - type: 'doc', - id: 'develop/java/nexus/development-walkthrough/index', - }, - items: [ - 'develop/java/nexus/development-walkthrough/define-the-data-contract', - 'develop/java/nexus/development-walkthrough/generate-code', - 'develop/java/nexus/development-walkthrough/choose-backing-implementation', - 'develop/java/nexus/development-walkthrough/implement-the-service', - 'develop/java/nexus/development-walkthrough/publish-in-nexus', - 'develop/java/nexus/development-walkthrough/call-the-service', - 'develop/java/nexus/development-walkthrough/add-messaging', - 'develop/java/nexus/development-walkthrough/send-messages', - 'develop/java/nexus/development-walkthrough/add-a-standalone-activity', - 'develop/java/nexus/development-walkthrough/call-the-standalone-activity', - 'develop/java/nexus/development-walkthrough/debugging-and-tips', - ], - }, ], }, { From f56c913f4b9cea15d8c1830164a0e10ee214e5d3 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 13 Aug 2026 20:06:18 -0700 Subject: [PATCH 06/16] Updated code generation doc page --- .../nexus/nexus-client-code-generator.mdx | 229 +++++++++++++++--- 1 file changed, 193 insertions(+), 36 deletions(-) diff --git a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx index b3784fa3ff..2866a22215 100644 --- a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx +++ b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx @@ -22,13 +22,13 @@ A [Nexus Service](/nexus/services) is a contract meant to be shared across team Those teams often work in different languages, so the same request and response types get hand-written once per SDK. Hand-written copies drift: a field is required on one side and optional on the other, a bound is enforced by the caller but not the handler. -The **Nexus Client Code Generator** removes those copies. +The **[Nexus Client Code Generator](https://github.com/temporalio/nexgen)** removes those copies. You describe your types and [Nexus Operations](/nexus/operations) once in a definition file, and the generator emits the equivalent library code for Go, Java, Python, and TypeScript. -The generator is a command-line tool named `nexgen`, distributed from the [temporalio/nex-gen](https://github.com/temporalio/nex-gen) repository. +The generator is a command-line tool named `nexgen`, distributed from the [temporalio/nexgen](https://github.com/temporalio/nexgen) repository. :::caution -`nexgen` is pre-release software, currently at version 0.2.1. +`nexgen` is pre-release software. The supported schema subset, command-line options, and emitted code may change incompatibly before a stable release. It is not yet published to any package registry, so you build it from source as described in [Install the generator](#install-the-generator). @@ -39,11 +39,10 @@ It is not yet published to any package registry, so you build it from source as For every type in your definition file, the generator emits three things per language. - **A typed model.** An idiomatic struct, class, interface, or dataclass, with doc comments carried over from the schema. -- **A shared runtime validator.** One validator per type, used when a value is parsed off the wire and again when it is serialized onto the wire, so a payload cannot enter or leave your service in a shape the contract forbids. +- **A runtime validator.** One validator per type, applied when a value is parsed off the wire and again when it is serialized onto it. See [Validation guarantees](#validation-guarantees) for more. - **A [Nexus Service Contract](/glossary#nexus-service-contract) definition.** The generated Service and Operation declarations you register on a Worker and call from a caller Workflow. -Constraint failures do not surface one at a time. -They aggregate into a single native error listing every violation, each naming the offending field and the bound it broke. +Constraint failures are aggregated into a single native error listing every violation, each naming the offending field and the bound it broke. A handler maps that error to a `BAD_REQUEST` [Nexus error](/nexus/error-handling), so a malformed request tells the caller everything that was wrong with it in one response. The supported schema subset is deliberately strict. @@ -65,7 +64,7 @@ Use this when you only need data models shared across languages, with no Service **Nexus document.** Add a root `nexusrpc: "1.0.0"` marker to enable a `services` section. The root becomes an envelope: Services and their Operations sit at the top level, and your types live under `$defs`. -The examples on this page use `samples/schemas/chat.nexusrpc.yaml` from the repository, abbreviated here: +The examples on this page use [`samples/schemas/chat.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/chat.nexusrpc.yaml) from the repository, abbreviated here: ```yaml nexusrpc: '1.0.0' @@ -106,26 +105,74 @@ $defs: required: [messageId] ``` -`fqn` is the wire name of the Service, the name callers reference when executing an Operation. +### How names are derived + +You write two kinds of name in the schema: one for the Service, and one for each Operation. In the sample above they are `ChatService` and `sendMessage`: + +```yaml +services: + ChatService: # the Service name + operations: + sendMessage: # the Operation name +``` + +Those are the only names you write. From each one the generator produces two more: a wire name and a name in your code. You declare neither of them. + +Give each name the casing that matches what it becomes: + +- A **Service** name is PascalCase: `ChatService`. A Service becomes a type in the generated code, and types are PascalCase. +- An **Operation** name is camelCase: `sendMessage`. An Operation becomes a method on that type, and the generator cases it like any other member. + +The first letter is the part the generator enforces: a Service has to start uppercase and an Operation lowercase. Both must begin with a letter and contain only letters and digits. + +``` +service name `chatService` must match `^[A-Z][a-zA-Z\d]+$` (start uppercase, then +letters/digits); set the wire name via `fqn` if it must differ +``` + +:::note Overriding the wire name + +The `fqn` in that error — a fully qualified name — is optional, and you can skip it to start. + +It lets a Service or Operation carry a wire name of your choosing rather than the one the generator derives from the name you wrote. Because it never becomes a code identifier, it accepts characters a name cannot: that is how a Service gets a wire name of `example.chat.v1.ChatService`, or an Operation one of `poll-messages`. Wire names are covered just below. + +Use `fqn` when you need to match a contract that is already published, or when you want a versioned, namespaced wire name. Otherwise leave it out. + +::: + +**The wire name** is the string the caller and the handler exchange, and what appears in Event History and the Temporal UI. Neither side types it — both take it from the generated code. + +Unless you override it, the wire name is whatever is in the definition file converted to PascalCase, so in this case the service would be **`ChatService`** and **`SendMessage`**. + +In the sample above the Service does override it using `fqn`, so the Service wire name becomes **`example.chat.v1.ChatService`**. + +**The name in your code** is what you call. The generator recases the name you wrote to match the conventions of the language it is generating for. With no override in play, the two derived names line up like this: + +| You write | Wire name | Java | Go | Python | TypeScript | +| --- | --- | --- | --- | --- | --- | +| `ChatService` | `ChatService` | `ChatService` | `ChatService` | `ChatService` | `chatService` | +| `sendMessage` | `SendMessage` | `sendMessage` | `SendMessage` | `send_message` | `sendMessage` | + +Operations become camelCase methods in Java and TypeScript, snake_case in Python, and PascalCase in Go. An Operation's `input` and `output` are each optional. The `ping` Operation above declares neither, which generates an Operation that takes and returns nothing. When present, each must be an object type, so that a field can be added later without breaking the wire format. -The repository holds four example definitions under [`samples/schemas/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas): -[`chat.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/chat.nexusrpc.yaml), -the feature-diverse [`showcase.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/showcase.nexusrpc.yaml), -the pure-schema [`temporal.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/temporal.yaml), -and a multi-file closure under [`kb/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb) showing how types split across files resolve through `$ref`. -The `kb/` closure starts at [`kb.nexusrpc.yaml`](https://github.com/temporalio/nex-gen/blob/main/samples/schemas/kb/kb.nexusrpc.yaml) and pulls in types from its [`content/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb/content) and [`tree/`](https://github.com/temporalio/nex-gen/tree/main/samples/schemas/kb/tree) subdirectories. +The repository holds four example definitions under [`samples/schemas/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas): +[`chat.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/chat.nexusrpc.yaml), +the feature-diverse [`showcase.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/showcase.nexusrpc.yaml), +the pure-schema [`temporal.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/temporal.yaml), +and a multi-file closure under [`kb/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb) showing how types split across files resolve through `$ref`. +The `kb/` closure starts at [`kb.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/kb/kb.nexusrpc.yaml) and pulls in types from its [`content/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb/content) and [`tree/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb/tree) subdirectories. ## Install the generator -Build the `nexgen` binary from source with a Rust toolchain: +Build the `nexgen` binary from source with Cargo, the Rust build tool: ```bash -git clone https://github.com/temporalio/nex-gen.git -cd nex-gen +git clone https://github.com/temporalio/nexgen.git +cd nexgen cargo build --release ``` @@ -161,7 +208,7 @@ The package name is the directory name, so the example above generates `package ### Java -Java requires `--package-name`, and its last dot-separated segment must match the `--output` directory name: +Java requires `--package-name`. Point `--output` at the **full package path** beneath your source root, not just a directory named after the last segment: ```bash nexgen java samples/schemas/chat.nexusrpc.yaml \ @@ -169,7 +216,7 @@ nexgen java samples/schemas/chat.nexusrpc.yaml \ --package-name com.example.chat ``` -If the two disagree, generation stops and tells you how to reconcile them: +The generator checks only that the package name's last dot-separated segment matches the output directory's name. If they disagree, generation stops and tells you how to reconcile them: ``` `--package-name com.example.wrong` must end with the output directory name `chat`, @@ -177,14 +224,44 @@ but its last segment is `wrong`; point `--output` at a directory named `wrong` o change the package's last segment to `chat` ``` +:::caution Passing that check is not enough to compile + +The check compares one segment; Java requires the file's location to match its whole package declaration. `--output ./src/chat --package-name com.example.chat` passes, because `chat` matches `chat`, and still produces files that declare `package com.example.chat` while sitting at `src/chat/`. + +Nothing fails at generation time, and nothing necessarily fails when you compile the generated files on their own. It breaks when something imports them: + +``` +Main.java:1: error: package com.example.chat does not exist +import com.example.chat.ChatService; +``` + +Always give `--output` the entire package path beneath your source root — `./src/main/java/com/example/chat` for `com.example.chat`. If files land in the wrong place, delete them and generate again with a corrected `--output`, rather than editing the `package` line to match. + +::: + ### Python ```bash nexgen python samples/schemas/chat.nexusrpc.yaml --output ./chat ``` -This writes an importable package: `models.py`, `services.py`, and an `__init__.py` that re-exports both. -Generated models are [Pydantic](https://docs.pydantic.dev/) models, so your Worker and Client must use the Pydantic Data Converter described in [Use Pydantic models](/develop/python/data-handling/data-conversion#use-pydantic-models). +This writes an importable package: `models.py`, `services.py`, and an `__init__.py` that re-exports both. Place it where the code that imports it can reach it — the output directory *is* the package. + +That also means the directory name becomes an importable module name, so pick one that does not collide with the standard library. `./chat` is safe, but a Service whose subject happens to share a name with a stdlib module is not: `./email`, `./queue`, and `./calendar` each shadow one for anything on that path. Qualify those — `./calendar_v1`. + +:::note Python output needs Pydantic + +Generated models are [Pydantic](https://docs.pydantic.dev/) models. Installing the Python SDK on its own does not bring Pydantic with it, so importing the generated package fails with `ModuleNotFoundError: No module named 'pydantic'` until you add it. The SDK ships an extra for this: + +```bash +pip install 'temporalio[pydantic]' +``` + +Your Worker and Client then need the Pydantic Data Converter — see [Use Pydantic models](/develop/python/data-handling/data-conversion#use-pydantic-models) for the setup. + +That converter is not optional if your schema uses any temporal `format`. `datetime.date`, `datetime.time`, and `datetime.datetime` can only be converted by it, and the generator maps `date`, `time`, and `date-time` onto those types. + +::: ### TypeScript @@ -202,13 +279,15 @@ nexgen ts samples/schemas/temporal.yaml --output ./chat --date-time-types tempor It has no runtime dependency and round-trips losslessly, but you parse and compare the strings yourself. - `date` maps `date-time` fields to a JavaScript `Date`. This is lossy: a `Date` is a UTC instant, so the original offset is folded away and precision is capped at milliseconds. -- `temporal` maps to the TC39 Temporal API, preserving offset and sub-second precision, and requires the `Temporal` global or a polyfill. +- `temporal` maps to the [TC39 Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal) — a JavaScript standard for dates and times, unrelated to Temporal the platform. It preserves offset and sub-second precision, and requires the `Temporal` global. ## Use the generated code -The generated Service definition is a normal Nexus Service definition. -You register it on a Worker and call it from a caller Workflow exactly as described in your SDK's Nexus guide. -What differs per language is how the validator gets invoked. +**There is nothing new to learn here.** What you have is a Service definition and a set of types, and nothing about them depends on having been generated — write the same Service by hand and your Worker and caller code is identical. You register it on a Worker and call it from a caller Workflow exactly as described in your SDK's Nexus guide. + +The one addition is validation, and so a call can fail with a contract violation that hand-written types would not have caught. The per-language examples below exist for completeness and to show how to log those violations. Apart from that single `catch`, they are the code you would already be writing. + +Only the wiring of that validation differs between languages. | SDK | How validation reaches the wire | Extra step | | ---------- | ----------------------------------------------------------- | ---------- | @@ -220,6 +299,14 @@ What differs per language is how the validator gets invoked. In Go, Java, and Python the validator sits in the serialization hook the Temporal data converter already calls, so validation happens on its own once the models are in use. TypeScript requires an explicit call, covered in [Validate payloads in TypeScript](#validate-payloads-in-typescript). +### Validation guarantees + +The two directions do not check the same things. + +**Parsing a value off the wire** enforces required fields and every value constraint, aggregating all violations into one error. This is the direction that protects a handler from a malformed request. + +**Serializing a value onto the wire** enforces value constraints — lengths, bounds, counts, patterns. It does not report a required field you left unset. The field is omitted from the payload and the peer rejects it, so the failure surfaces as a `BAD_REQUEST` from the other side rather than as a local error at the point you built the object. + ### Go The generated `ChatService` value carries the Service name and one typed Operation reference per Operation. @@ -239,7 +326,7 @@ if err := service.Register(sendMessage); err != nil { w.RegisterNexusService(service) ``` -Call it from a caller Workflow, passing the generated Operation reference so the SDK type-checks the request and response: +Call it from a caller Workflow, passing the generated Operation reference so the SDK type-checks the request and response. A payload that violates the contract fails at the call, so that is where you catch it: ```go client := workflow.NewNexusClient("chat-endpoint", chat.ChatService.ServiceName) @@ -251,8 +338,22 @@ err := client.ExecuteOperation( chat.SendMessageInput{RoomId: "r1", Message: chat.Message{Kind: "text", Body: "hi"}}, workflow.NexusOperationOptions{}, ).Get(ctx, &output) + +if err != nil { + var validationErr *chat.ValidationError + if errors.As(err, &validationErr) { + for _, v := range validationErr.Violations { + logger.Error("contract violation", "path", v.Path, "reason", v.Reason) + } + } else { + logger.Error("Nexus call failed", "error", err) + } + return err +} ``` +`ValidationError` carries every violation as a `Violation` with a `Path` and a `Reason`. Reach it with `errors.As` rather than a type assertion, because the error arrives wrapped by the JSON encoder. It is generated into each package, so a consumer of two generated Services needs one `errors.As` per package. + ### Java The generator emits `ChatService` as an interface annotated with `@Service`, with one `@Operation` method per Operation. @@ -270,7 +371,7 @@ public final class ChatServiceImpl { Register it on a Worker with `worker.registerNexusServiceImplementation(new ChatServiceImpl())`. -On the caller side, the same interface works directly as a Workflow stub: +On the caller side, the same interface works directly as a Workflow stub. A payload that violates the contract fails at the call, so that is where you catch it: ```java ChatService chat = Workflow.newNexusServiceStub( @@ -282,9 +383,22 @@ ChatService chat = Workflow.newNexusServiceStub( .build()) .build()); -SendMessageOutput output = chat.sendMessage(new SendMessageInput("r1", message)); +try { + SendMessageOutput output = chat.sendMessage(new SendMessageInput("r1", message)); +} catch (DataConverterException e) { + if (e.getCause() instanceof ValidationException ve) { + ve.getViolations().forEach(v -> log.error("{}: {}", v.getPath(), v.getReason())); + } else { + log.error("Payload conversion failed", e); + } + throw e; +} ``` +`ValidationException` extends Jackson's `JsonMappingException`, so it is checked and always arrives wrapped. Converting the payload is what triggers it, so it reaches you as the cause of a `DataConverterException`, with the violation list intact. + +It is generated into each package, so a consumer of two generated Services has two unrelated exception types of the same name and needs a `catch` per package. + ### Python The generator emits `ChatService` as a `@service`-decorated class whose attributes are typed `Operation` declarations. @@ -314,6 +428,8 @@ output = await client.execute_operation( Generated Python fields are snake_case with the wire name as an alias. Construct models with either name, and read them with the snake_case attribute: `SendMessageInput(roomId="r1", ...)` constructs, and `output.message_id` reads. +There is nothing to catch at the call. Pydantic validates when the model is constructed, so an invalid `SendMessageInput` raises `pydantic.ValidationError` at the constructor and never reaches the Nexus client. Handle it where you build the model. + ### TypeScript The generator emits a `chatService` Service definition plus, for each type, an interface and a companion `Mapper` class: @@ -366,18 +482,59 @@ ValidationError: 2 validation error(s): roomId: required; message.body: expected The error also exposes a `violations` array of `{ path, reason }` objects, so a handler can convert it into a `BAD_REQUEST` Nexus error with the full list intact. +## Dates, times, and durations + +A `format` of `date-time`, `date`, `time`, or `duration` becomes a real date or duration type in the generated model, not a string you parse yourself. Java maps to `java.time`, Python to `datetime` and `timedelta`, Go to `time.Time` and `time.Duration`, and TypeScript is configurable — see [`--date-time-types`](#typescript). + +Every language encodes these identically, using [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) for dates and times and [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) for durations. A caller in one language and a handler in another exchange them with no conversion on either side, and without you writing any format-handling code. + +## Regenerate after a contract change + +Generated files carry a `DO NOT EDIT` header and are replaced wholesale on the next run. There is no merge step, so anything you add to them is lost. + +Two habits make this safe: + +- **Commit generated code and regenerate as its own commit.** The diff then shows exactly what the contract change did to each language. +- **Fix names in the schema, not the output.** When a generated identifier is wrong for your language, use a per-language naming override in the contract so the fix survives regeneration. + +If you generate into the wrong directory, delete what landed there and run the generator again with corrected flags. Do not edit the `package` or module declaration to match where the files ended up as the next run will overwrite it. + ## Schema defaults -A `default` in your schema is applied when reading, and is never written back to the wire. -The field stays optional in the generated model, and each language exposes the default differently. +A property can declare a `default`, which makes it optional for a caller to supply: + +```yaml +sampleValue: + type: integer + default: 0 +``` + +A caller that leaves `sampleValue` unset sends a payload without the field, and the receiver reads `0`. The default is never written into the payload, so an omitted field stays omitted rather than being filled in before it is sent. + +:::caution Changing a default is a breaking change + +The default lives in the generated code, not in the payload. Temporal replays a Workflow by re-reading the payloads already recorded in its [Event History](/encyclopedia/event-history) using whatever code the Worker is running now, so changing a default changes what those recorded payloads mean. + +If the value affects which commands the Workflow produces, replaying an in-flight Workflow fails with a [non-determinism error](/troubleshooting/execution-failures#non-determinism-error) — the [deterministic constraints](/workflow-definition#deterministic-constraints) that govern any change to Workflow code apply here too. If it does not affect commands, nothing fails and the behavior changes silently, which is harder to catch. + +Treat a default as part of the contract. To change the effective value, add a new field rather than editing an existing default. + +::: + +Because the field is optional, Go and Java give you two members side by side, so nothing depends on remembering that a default exists: + +- The field itself, which is empty when the caller omitted it — `getSampleValue()` returns `null` in Java, and `SampleValue` is a `nil *int64` in Go. +- An accessor named after it that substitutes the default — `getSampleValueOrDefault()` and `SampleValueOrDefault()`. + +Use the first when you need to know whether the caller supplied a value, and the second when you just want a number. + +TypeScript has no accessor. `sampleValue` is `undefined` when unset, and the generator exports a `DEFAULT_SAMPLE_VALUE` constant you apply yourself: `sampleValue ?? DEFAULT_SAMPLE_VALUE`. -- **Go** and **Java** generate an accessor: `PriorityOrDefault()` and `getPriorityOrDefault()`. -- **Python** applies the default through Pydantic, so reading the attribute returns it. -- **TypeScript** exports a module-level constant, such as `DEFAULT_PRIORITY`, that you apply yourself with `value.priority ?? DEFAULT_PRIORITY`. +Python has neither. Pydantic applies defaults when the model is constructed, so `sample_value` always holds a value and an omitted field reads the same as one explicitly set to `0`. ## Supported schema features -The generator implements a curated subset of JSON Schema 2020-12 chosen so that every accepted construct lowers identically into all four languages. +The generator implements a curated subset of [JSON Schema 2020-12](https://json-schema.org/draft/2020-12) chosen so that every accepted construct lowers identically into all four languages. Fully supported: `properties`, `required`, `default`, `minProperties` and `maxProperties`, `dependentRequired`, string and numeric bounds, `items`, `minItems` and `maxItems`, `minContains` and `maxContains`, `allOf`, the recognized nullable pattern `oneOf: [{type: T}, {type: "null"}]`, and the `title`, `description`, and `deprecated` annotations. @@ -385,11 +542,11 @@ Partially supported: `type` (single-string form only), `additionalProperties`, ` Deliberately rejected, because they have no coherent typed lowering across all four languages: `anyOf`, `not`, `if`/`then`/`else`, `dependentSchemas`, `prefixItems`, `unevaluatedProperties`, `unevaluatedItems`, `contentMediaType`, and `contentSchema`. -For the current per-keyword support table, see the [nex-gen README](https://github.com/temporalio/nex-gen#supported-json-schema-features). +For the current per-keyword support table, see the [nexgen README](https://github.com/temporalio/nexgen#supported-json-schema-features). :::tip RESOURCES -- [temporalio/nex-gen](https://github.com/temporalio/nex-gen) for the generator, its README, and the example schemas. +- [temporalio/nexgen](https://github.com/temporalio/nexgen) for the generator, its README, and the example schemas. - [Nexus Services](/nexus/services) for the Service contract concept. - Nexus feature guides for registering Services and calling Operations: [Go](/develop/go/nexus/feature-guide) | From ba4bc2a9f04dab8ee53c8a6ef5933e6908720f85 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 13 Aug 2026 20:15:36 -0700 Subject: [PATCH 07/16] Updating --- .../development-walkthrough/add-a-standalone-activity.mdx | 5 ----- .../java/nexus/development-walkthrough/add-messaging.mdx | 6 ------ .../nexus/development-walkthrough/call-the-service.mdx | 5 ----- .../call-the-standalone-activity.mdx | 4 ---- .../choose-backing-implementation.mdx | 5 ----- .../nexus/development-walkthrough/debugging-and-tips.mdx | 5 ----- .../development-walkthrough/define-the-data-contract.mdx | 5 ----- .../java/nexus/development-walkthrough/generate-code.mdx | 5 ----- .../development-walkthrough/implement-the-service.mdx | 5 ----- docs/develop/java/nexus/development-walkthrough/index.mdx | 7 ------- .../nexus/development-walkthrough/publish-in-nexus.mdx | 6 ------ .../java/nexus/development-walkthrough/send-messages.mdx | 5 ----- docs/encyclopedia/nexus/nexus-client-code-generator.mdx | 8 -------- docs/encyclopedia/nexus/nexus-sdk-v2.mdx | 7 ------- docs/encyclopedia/nexus/nexus-standalone-activity.mdx | 6 ------ 15 files changed, 84 deletions(-) diff --git a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx index 9dfe304203..1a626ce586 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx @@ -5,11 +5,6 @@ sidebar_label: 9. Add a Standalone Activity description: Back the notification Nexus Operation with a Standalone Activity instead of a Workflow, with no wrapper Workflow required. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus standalone activity - - activity backed operation - - start activity - - notification activity tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx index cef82db8b9..30ee2921f0 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -5,12 +5,6 @@ sidebar_label: 7. Add messaging description: Expose Signal, Query, and Update on the approval Workflow as Nexus Operations using the Nexus-aware Client. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus signal - - nexus query - - nexus update - - workflow message passing - - temporal operation handler tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx index d7c181719c..2620c2226b 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx @@ -5,11 +5,6 @@ sidebar_label: 6. Call the Service description: Call the approval Nexus Service from a caller Workflow in another Namespace using the generated Service interface. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus caller workflow - - call nexus operation - - cross namespace - - nexus service stub tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx index f9212f0a96..f8aec81a58 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -5,10 +5,6 @@ sidebar_label: 10. Call the Standalone Activity description: Call the Activity-backed notification Operation from a caller Workflow and complete the approval flow end to end. toc_max_heading_level: 4 unlisted: true -keywords: - - call nexus operation - - activity backed operation - - nexus caller workflow tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx index 63536f4510..1ac2fcfa06 100644 --- a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx +++ b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx @@ -5,11 +5,6 @@ sidebar_label: 3. Choose the backing implementation description: Decide whether each Nexus Operation is backed by a Standalone Activity, a Workflow, or an Entity Workflow. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus operation backing - - entity workflow - - standalone activity - - workflow backed operation tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx index b6899bb277..a0097623d6 100644 --- a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -5,11 +5,6 @@ sidebar_label: Debugging and tips description: Diagnose the most common Nexus failures, from Endpoint authorization to Query constraints, and avoid the pitfalls that are easy to miss. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus debugging - - nexus troubleshooting - - nexus pitfalls - - nexus operation hangs tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx index c8f92359fc..1db8b169f9 100644 --- a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx +++ b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx @@ -5,11 +5,6 @@ sidebar_label: 1. Define the data contract description: Plan an approval Nexus Service and write its data contract before any implementation, so every language shares one definition. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus data contract - - nexus service contract - - json schema - - api contract first tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx index 768c887599..56c913ccd2 100644 --- a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx +++ b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx @@ -5,11 +5,6 @@ sidebar_label: 2. Generate code description: Use the Nexus Client Code Generator to turn the approval contract into typed models, validators, and Service definitions for the handler and the caller. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus code generation - - nexgen - - generated models - - nexus service definition tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx index c511e01016..fe217af24a 100644 --- a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -5,11 +5,6 @@ sidebar_label: 4. Implement the Service description: Implement the approval Nexus Service in Java using TemporalOperationHandler, backed by a Workflow, and run a Worker that hosts it. toc_max_heading_level: 4 unlisted: true -keywords: - - temporal operation handler - - nexus service implementation - - nexus worker - - approval workflow tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/index.mdx b/docs/develop/java/nexus/development-walkthrough/index.mdx index ca9626008c..8530c0f018 100644 --- a/docs/develop/java/nexus/development-walkthrough/index.mdx +++ b/docs/develop/java/nexus/development-walkthrough/index.mdx @@ -5,13 +5,6 @@ sidebar_label: Development Walkthrough description: Build a Nexus Service end to end in Java, starting from a data contract and adding one Nexus capability at a time to solve an approval problem. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus walkthrough - - nexus java - - approval workflow - - data contract - - nexus service - - temporal operation handler tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx index cdab27070a..4bd4a51878 100644 --- a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx +++ b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx @@ -5,12 +5,6 @@ sidebar_label: 5. Publish in Nexus description: Create a Nexus Endpoint for the approval Service, allow caller Namespaces to reach it, and set up credentials for Temporal Cloud. toc_max_heading_level: 4 unlisted: true -keywords: - - nexus endpoint - - nexus registry - - allowed caller namespaces - - api key - - temporal cloud nexus tags: - Nexus - Java SDK diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx index ab60816392..c6ce7c97d5 100644 --- a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -5,11 +5,6 @@ sidebar_label: 8. Send messages description: Call the Signal, Query, and Update Operations from a caller Workflow, and start an approval with Signal-with-Start. toc_max_heading_level: 4 unlisted: true -keywords: - - send nexus signal - - nexus query caller - - nexus update caller - - signal with start tags: - Nexus - Java SDK diff --git a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx index 2866a22215..ad75b0f0a2 100644 --- a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx +++ b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx @@ -5,14 +5,6 @@ sidebar_label: Nexus Client Code Generator description: The Nexus Client Code Generator turns one schema into typed models, runtime validators, and Nexus Service definitions for Go, Java, Python, and TypeScript. toc_max_heading_level: 4 slug: /nexus/client-code-generator -keywords: - - nexus client code generator - - nexus code generation - - nexus service definition - - service contract - - json schema - - schema validation - - generated models tags: - Nexus - Concepts diff --git a/docs/encyclopedia/nexus/nexus-sdk-v2.mdx b/docs/encyclopedia/nexus/nexus-sdk-v2.mdx index 0f8f2476d9..37687ab815 100644 --- a/docs/encyclopedia/nexus/nexus-sdk-v2.mdx +++ b/docs/encyclopedia/nexus/nexus-sdk-v2.mdx @@ -5,13 +5,6 @@ sidebar_label: Nexus SDK V2 description: Implement Nexus Operations with the Temporal Operation Handler - back an Operation with a Workflow, an Update, or an Activity, and get bidirectional linking across the Namespace boundary. toc_max_heading_level: 4 slug: /nexus/sdk-v2 -keywords: - - nexus sdk v2 - - temporal operation handler - - nexus sdk ergonomics - - nexus signal - - nexus update - - bidirectional linking tags: - Nexus - Concepts diff --git a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx index 4d3ef086ab..f9591c1a04 100644 --- a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx +++ b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx @@ -5,12 +5,6 @@ sidebar_label: Nexus Standalone Activity description: Back a Nexus Operation with a Standalone Activity. toc_max_heading_level: 4 slug: /nexus/standalone-activity -keywords: - - nexus standalone activity - - activity backed nexus operation - - standalone activity - - start activity - - temporal operation handler tags: - Nexus - Concepts From 85b4e51291dcb34e248194c7570c2c9688ee1ff2 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Fri, 14 Aug 2026 09:31:45 -0700 Subject: [PATCH 08/16] Wording change --- docs/encyclopedia/nexus/nexus-client-code-generator.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx index ad75b0f0a2..6640a974fa 100644 --- a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx +++ b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx @@ -11,7 +11,7 @@ tags: --- A [Nexus Service](/nexus/services) is a contract meant to be shared across team boundaries. -Those teams often work in different languages, so the same request and response types get hand-written once per SDK. +Those teams often work in different languages, so the same request and response types get hand-written for each SDK. Hand-written copies drift: a field is required on one side and optional on the other, a bound is enforced by the caller but not the handler. The **[Nexus Client Code Generator](https://github.com/temporalio/nexgen)** removes those copies. From 198ec750f30577361a8803d21d14b699705996fe Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 17 Aug 2026 11:17:49 -0700 Subject: [PATCH 09/16] IDL doc update --- .../nexus/nexus-client-code-generator.mdx | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx index 6640a974fa..9bc5c88300 100644 --- a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx +++ b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx @@ -20,25 +20,26 @@ The generator is a command-line tool named `nexgen`, distributed from the [tempo :::caution -`nexgen` is pre-release software. -The supported schema subset, command-line options, and emitted code may change incompatibly before a stable release. +`nexgen` is pre-release software and may not retain backwards compatibility with previous versions of the tool. It is not yet published to any package registry, so you build it from source as described in [Install the generator](#install-the-generator). ::: ## What the generator produces -For every type in your definition file, the generator emits three things per language. +The generator produces a client library in Go, Java, Python, or TypeScript for the inputs and outputs of your Nexus Operations. The generated types check values against the contract as they are sent and received, so a violation surfaces as an error rather than as bad data. + +That client library contains three things: - **A typed model.** An idiomatic struct, class, interface, or dataclass, with doc comments carried over from the schema. - **A runtime validator.** One validator per type, applied when a value is parsed off the wire and again when it is serialized onto it. See [Validation guarantees](#validation-guarantees) for more. -- **A [Nexus Service Contract](/glossary#nexus-service-contract) definition.** The generated Service and Operation declarations you register on a Worker and call from a caller Workflow. +- **A [Nexus Service Contract](/glossary#nexus-service-contract) definition**, for a file that declares Services. These are the Service and Operation declarations you register on a Worker and call from a caller Workflow. A pure JSON Schema file declares none, so it produces only the models and their validators. -Constraint failures are aggregated into a single native error listing every violation, each naming the offending field and the bound it broke. +Additionally, constraint failures are aggregated into a single native error listing every violation, each naming the offending field and the bound it broke. A handler maps that error to a `BAD_REQUEST` [Nexus error](/nexus/error-handling), so a malformed request tells the caller everything that was wrong with it in one response. The supported schema subset is deliberately strict. -Anything ambiguous, or anything that cannot be expressed identically in all four languages, is rejected when you run the generator, with a diagnostic explaining how to express it instead. +Anything ambiguous, or anything that cannot be expressed identically in all generated languages, is rejected when you run the generator with a diagnostic explaining how to express it correctly. The generator prefers to fail loudly at generation time over emitting code that behaves differently in one language than another. ## Supported languages @@ -97,6 +98,8 @@ $defs: required: [messageId] ``` +See [Definition files](https://github.com/temporalio/nexgen#definition-files) in the generator's README for details on that file. + ### How names are derived You write two kinds of name in the schema: one for the Service, and one for each Operation. In the sample above they are `ChatService` and `sendMessage`: @@ -134,7 +137,7 @@ Use `fqn` when you need to match a contract that is already published, or when y **The wire name** is the string the caller and the handler exchange, and what appears in Event History and the Temporal UI. Neither side types it — both take it from the generated code. -Unless you override it, the wire name is whatever is in the definition file converted to PascalCase, so in this case the service would be **`ChatService`** and **`SendMessage`**. +Unless you override it, the wire name is the name from the definition file converted to PascalCase. In the sample that gives the Service a wire name of **`ChatService`** and the `sendMessage` Operation a wire name of **`SendMessage`**. In the sample above the Service does override it using `fqn`, so the Service wire name becomes **`example.chat.v1.ChatService`**. @@ -261,25 +264,27 @@ That converter is not optional if your schema uses any temporal `format`. `datet nexgen ts samples/schemas/chat.nexusrpc.yaml --output ./chat ``` -TypeScript accepts `--date-time-types` to choose how temporal `format` fields are represented in memory: +TypeScript accepts `--date-time-types` to choose how date and time fields are represented in memory. There are three choices: + +- `string`, the default, keeps every date and time field as the [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) string that appears on the wire. It adds no runtime dependency and round-trips losslessly, but you parse and compare the strings yourself. +- `date` maps `date-time` fields to a JavaScript `Date`. This is lossy: a `Date` is a UTC instant, so the original offset is folded away and precision is capped at milliseconds. +- `temporal` maps to the [TC39 Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal), a JavaScript standard for dates and times that is unrelated to Temporal the platform. It preserves the offset and sub-second precision, and requires the `Temporal` global. + +The chat schema has no date or time fields, so this command uses [`temporal.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/temporal.yaml), which has one field for each of `date`, `date-time`, `time`, and `duration`: ```bash -nexgen ts samples/schemas/temporal.yaml --output ./chat --date-time-types temporal +nexgen ts samples/schemas/temporal.yaml --output ./events --date-time-types temporal ``` -- `string` (the default) keeps every temporal field as the RFC 3339 string that appears on the wire. - It has no runtime dependency and round-trips losslessly, but you parse and compare the strings yourself. -- `date` maps `date-time` fields to a JavaScript `Date`. - This is lossy: a `Date` is a UTC instant, so the original offset is folded away and precision is capped at milliseconds. -- `temporal` maps to the [TC39 Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal) — a JavaScript standard for dates and times, unrelated to Temporal the platform. It preserves offset and sub-second precision, and requires the `Temporal` global. +## Dates, times, and durations -## Use the generated code +TypeScript's `--date-time-types` is the only place you choose how a date or time is represented. Elsewhere the generator decides: Java uses `java.time`, Python `datetime` and `timedelta`, and Go `time.Time` and `time.Duration`. Two cases hand you the wire string to work with instead of a date type — `format: time` in Java, and every date and time format under TypeScript's default `string` mode. -**There is nothing new to learn here.** What you have is a Service definition and a set of types, and nothing about them depends on having been generated — write the same Service by hand and your Worker and caller code is identical. You register it on a Worker and call it from a caller Workflow exactly as described in your SDK's Nexus guide. +Whichever type you get, every language writes the same bytes. Dates and times use [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339), which is a profile of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). ISO 8601 permits many optional spellings of the same instant, and RFC 3339 narrows them to one so two systems cannot read a timestamp differently. RFC 3339 specifies timestamps rather than durations, so durations follow ISO 8601. -The one addition is validation, and so a call can fail with a contract violation that hand-written types would not have caught. The per-language examples below exist for completeness and to show how to log those violations. Apart from that single `catch`, they are the code you would already be writing. +## How validation works -Only the wiring of that validation differs between languages. +Validation is the one behavior the generated types add, so a call can fail with a contract violation that hand-written types would not have caught. Only the wiring of that validation differs between languages. | SDK | How validation reaches the wire | Extra step | | ---------- | ----------------------------------------------------------- | ---------- | @@ -299,6 +304,14 @@ The two directions do not check the same things. **Serializing a value onto the wire** enforces value constraints — lengths, bounds, counts, patterns. It does not report a required field you left unset. The field is omitted from the payload and the peer rejects it, so the failure surfaces as a `BAD_REQUEST` from the other side rather than as a local error at the point you built the object. +For code that catches and logs a violation, see the per-language examples in [Use the generated code](#use-the-generated-code). + +## Use the generated code + +**Whether the code is generated or written by hand, you use it the same way.** It is a Service definition and a set of types: register it on a Worker, and call it from a caller Workflow exactly as described in your SDK's Nexus guide. The one difference is that generated types validate themselves, so a call can fail with a contract violation for you to catch. + +Each example below registers a handler, calls the Operation from a caller Workflow, and catches a validation failure. + ### Go The generated `ChatService` value carries the Service name and one typed Operation reference per Operation. @@ -460,7 +473,20 @@ const handler = nexus.serviceHandler(chatService, { }); ``` -The cast on the return value is expected: `toIntermediate` returns `unknown`, because its result is a plain wire value rather than the model type the Operation declares. +The caller side is the mirror image. Map the request out before executing the Operation, and map the result back in when it returns: + +```typescript +const client = workflow.createNexusServiceClient({ + service: chatService, + endpoint: 'chat-endpoint', +}); + +const wire = new SendMessageInputMapper().toIntermediate(input) as SendMessageInput; +const raw = await client.executeOperation(chatService.operations.sendMessage, wire); +const output = new SendMessageOutputMapper().fromIntermediate(raw); +``` + +The cast is expected in both examples: `toIntermediate` returns `unknown`, because its result is a plain wire value rather than the model type the Operation declares. Skipping the mapper is the failure to watch for, because nothing reports it. The value handed to your handler is typed as the model, since `nexus.operation` declares it that way, but at runtime it is only whatever was deserialized. @@ -474,12 +500,6 @@ ValidationError: 2 validation error(s): roomId: required; message.body: expected The error also exposes a `violations` array of `{ path, reason }` objects, so a handler can convert it into a `BAD_REQUEST` Nexus error with the full list intact. -## Dates, times, and durations - -A `format` of `date-time`, `date`, `time`, or `duration` becomes a real date or duration type in the generated model, not a string you parse yourself. Java maps to `java.time`, Python to `datetime` and `timedelta`, Go to `time.Time` and `time.Duration`, and TypeScript is configurable — see [`--date-time-types`](#typescript). - -Every language encodes these identically, using [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) for dates and times and [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) for durations. A caller in one language and a handler in another exchange them with no conversion on either side, and without you writing any format-handling code. - ## Regenerate after a contract change Generated files carry a `DO NOT EDIT` header and are replaced wholesale on the next run. There is no merge step, so anything you add to them is lost. @@ -487,7 +507,7 @@ Generated files carry a `DO NOT EDIT` header and are replaced wholesale on the n Two habits make this safe: - **Commit generated code and regenerate as its own commit.** The diff then shows exactly what the contract change did to each language. -- **Fix names in the schema, not the output.** When a generated identifier is wrong for your language, use a per-language naming override in the contract so the fix survives regeneration. +- **Fix names in the schema, not the output.** When a generated identifier is wrong for your language, set a per-language override in the contract so the fix survives regeneration. See [Naming and overrides](https://github.com/temporalio/nexgen#naming--overrides) for the available keys. If you generate into the wrong directory, delete what landed there and run the generator again with corrected flags. Do not edit the `package` or module declaration to match where the files ended up as the next run will overwrite it. @@ -509,7 +529,7 @@ The default lives in the generated code, not in the payload. Temporal replays a If the value affects which commands the Workflow produces, replaying an in-flight Workflow fails with a [non-determinism error](/troubleshooting/execution-failures#non-determinism-error) — the [deterministic constraints](/workflow-definition#deterministic-constraints) that govern any change to Workflow code apply here too. If it does not affect commands, nothing fails and the behavior changes silently, which is harder to catch. -Treat a default as part of the contract. To change the effective value, add a new field rather than editing an existing default. +Treat a default as part of the contract. Adding a replacement field is not sufficient on its own: old payloads omit the new field too, so replay reads that field's default and can still diverge. Any change to how existing payloads are interpreted needs a [Workflow versioning](/workflow-definition#workflow-versioning) plan that keeps in-flight Executions on their original behavior. ::: @@ -526,13 +546,13 @@ Python has neither. Pydantic applies defaults when the model is constructed, so ## Supported schema features -The generator implements a curated subset of [JSON Schema 2020-12](https://json-schema.org/draft/2020-12) chosen so that every accepted construct lowers identically into all four languages. +The generator implements a curated subset of [JSON Schema 2020-12](https://json-schema.org/draft/2020-12) chosen so that every accepted construct lowers identically into all generated languages. Fully supported: `properties`, `required`, `default`, `minProperties` and `maxProperties`, `dependentRequired`, string and numeric bounds, `items`, `minItems` and `maxItems`, `minContains` and `maxContains`, `allOf`, the recognized nullable pattern `oneOf: [{type: T}, {type: "null"}]`, and the `title`, `description`, and `deprecated` annotations. Partially supported: `type` (single-string form only), `additionalProperties`, `propertyNames`, `const` and `enum` (scalars only), `format`, `pattern` (a portable RE2-safe subset), `multipleOf`, `contentEncoding`, `uniqueItems`, `contains`, `oneOf` (branches must be separable by a decidable selector), and `$ref` with `$defs` (local files only). -Deliberately rejected, because they have no coherent typed lowering across all four languages: `anyOf`, `not`, `if`/`then`/`else`, `dependentSchemas`, `prefixItems`, `unevaluatedProperties`, `unevaluatedItems`, `contentMediaType`, and `contentSchema`. +Deliberately rejected, because they have no coherent typed lowering across all generated languages: `anyOf`, `not`, `if`/`then`/`else`, `dependentSchemas`, `prefixItems`, `unevaluatedProperties`, `unevaluatedItems`, `contentMediaType`, and `contentSchema`. For the current per-keyword support table, see the [nexgen README](https://github.com/temporalio/nexgen#supported-json-schema-features). From 8b37b445a62ed76dd182768cad40e40a2dc8386f Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 17 Aug 2026 16:47:52 -0700 Subject: [PATCH 10/16] Renaming from V2, working on SAA doc --- .../development-walkthrough/add-messaging.mdx | 6 +- .../call-the-standalone-activity.mdx | 2 +- .../debugging-and-tips.mdx | 6 +- .../implement-the-service.mdx | 6 +- .../nexus/development-walkthrough/index.mdx | 2 +- .../development-walkthrough/send-messages.mdx | 6 +- .../nexus/nexus-standalone-activity.mdx | 219 +++++++++++++++--- docs/encyclopedia/nexus/nexus.mdx | 2 +- ...-v2.mdx => temporal-operation-handler.mdx} | 13 +- sidebars.js | 2 +- 10 files changed, 203 insertions(+), 61 deletions(-) rename docs/encyclopedia/nexus/{nexus-sdk-v2.mdx => temporal-operation-handler.mdx} (98%) diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx index 30ee2921f0..8e654e642c 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -32,7 +32,7 @@ Two constraints apply to the Query handler. It must not block, and it must not m ## Expose them as Nexus Operations -These use `TemporalOperationHandler`, and they divide along the line described in [Nexus SDK V2](/nexus/sdk-v2#the-nexus-aware-client): Signal is **sync messaging**, and Update is an **async backing**. +These use `TemporalOperationHandler`, and they divide along the line described in [The Nexus-aware Client](/nexus/temporal-operation-handler#the-nexus-aware-client): Signal is **sync messaging**, and Update is an **async backing**. ### Signal @@ -52,7 +52,7 @@ Because it is an async backing, there is at most one per Operation invocation. A :::caution Query is not in the pre-release -`getApprovalStatus` belongs in the contract, but Query is not available as sync messaging on the Nexus-aware Client in the pre-release, so this Operation cannot be implemented yet. See [Nexus SDK V2](/nexus/sdk-v2). +`getApprovalStatus` belongs in the contract, but Query is not available as sync messaging on the Nexus-aware Client in the pre-release, so this Operation cannot be implemented yet. See [Temporal Operation Handler](/nexus/temporal-operation-handler). Adding the Query handler to the Workflow is still worth doing — it costs nothing and the Operation can be wired up once Query lands. Until then, a caller that needs in-flight progress has to obtain it outside this Service. @@ -74,6 +74,6 @@ Using a Query to fetch the outcome would mean polling for something that is alre - [Workflow message passing](/encyclopedia/workflow-message-passing) for Signals, Queries, and Updates. - [Handling messages](/handling-messages) for handler constraints, including Query restrictions. -- [Nexus SDK V2](/nexus/sdk-v2) for the sync side effect and async backing distinction. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) for the sync side effect and async backing distinction. ::: diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx index f8aec81a58..46a6aeb648 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -38,7 +38,7 @@ Every step crossed a Namespace boundary, and the caller never learned a Workflow Open the caller Workflow in the UI and follow the links. Because the handlers used the Nexus-aware Client, each Operation is connected to the Execution it started or messaged in the handler Namespace, and you can move between the two Namespaces in one view. -The exception is `getApprovalStatus`, which is [not implementable in the pre-release](/nexus/sdk-v2) because Query is not yet available as sync messaging. +The exception is `getApprovalStatus`, which is [not implementable in the pre-release](/nexus/temporal-operation-handler) because Query is not yet available as sync messaging. ## Where to go next diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx index a0097623d6..a7ac461e26 100644 --- a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -34,7 +34,7 @@ The handler used its own Temporal Client instead of the one `TemporalOperationHa Fetching a Client yourself works, and the Operation behaves correctly, but you lose the [bidirectional links](/nexus/execution-debugging#bi-directional-linking) that connect the two Executions. Use the injected Client for anything that starts or messages an Execution. -Note that Query, Cancel, and Terminate are [not available as sync messaging](/nexus/sdk-v2) in the pre-release. You can still send them with a Client of your own, but nothing links those messages back to the caller. +Note that Query, Cancel, and Terminate are [not available as sync messaging](/nexus/temporal-operation-handler) in the pre-release. You can still send them with a Client of your own, but nothing links those messages back to the caller. ## A Query returns stale data, hangs, or throws @@ -58,7 +58,7 @@ Use the Operation result for outcomes. Use a Query for in-flight progress. ### Expecting to re-attach to a running Operation -There is no Operation that attaches to an already-running Execution and waits for its result. Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2). +There is no Operation that attaches to an already-running Execution and waits for its result. Get Workflow Result as an async backing is [not yet available](/nexus/temporal-operation-handler). Whoever starts the Operation is who receives the result. If other systems need it, distribute it from the caller or notify them from the handler. @@ -103,6 +103,6 @@ Callers and handlers deploy independently, so both sides run different contract - [Nexus execution debugging](/nexus/execution-debugging) for tracing Operations across Namespaces. - [Nexus error handling](/nexus/error-handling) for the error model and retry behavior. - [Nexus security](/nexus/security) for Endpoint authorization. -- [Nexus SDK V2](/nexus/sdk-v2) for current per-SDK capability status. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) for current per-SDK capability status. ::: diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx index fe217af24a..58493047af 100644 --- a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -29,7 +29,7 @@ The blocking step is the reason this is a Workflow. It may wait weeks, across Wo ## Implement the Operation with TemporalOperationHandler -Use `TemporalOperationHandler` for every Temporal-backed Operation, including simple ones. It is the entry point in [Nexus SDK V2](/nexus/sdk-v2), and starting with it means an Operation can later gain a Signal or change its backing without changing shape. +Use `TemporalOperationHandler` for every Temporal-backed Operation, including simple ones. It is the entry point to the [Temporal Operation Handler](/nexus/temporal-operation-handler) programming model, and starting with it means an Operation can later gain a Signal or change its backing without changing shape. `TemporalOperationHandler.create(...)` gives your start handler a context, a Nexus-aware Client, and the Operation input. Call `startWorkflow` on that Client and return its result. The Operation then completes when the Workflow returns, delivering the Workflow's return value to the caller. @@ -41,7 +41,7 @@ Set the Workflow Id from the approval id, as decided in [step 3](/develop/java/n :::note -You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Coming from the earlier handler APIs](/nexus/sdk-v2#coming-from-the-earlier-handler-apis). +You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Coming from the earlier handler APIs](/nexus/temporal-operation-handler#coming-from-the-earlier-handler-apis). ::: @@ -67,7 +67,7 @@ The Service runs but nothing can reach it yet. [Publish it in Nexus](/develop/ja :::tip RESOURCES -- [Nexus SDK V2](/nexus/sdk-v2) for `TemporalOperationHandler` and the Nexus-aware Client. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) for the handler type and the Nexus-aware Client. - [Java Nexus feature guide](/develop/java/nexus/feature-guide) for the full handler and Worker API. - [Nexus error handling](/nexus/error-handling) for mapping failures to Nexus errors. diff --git a/docs/develop/java/nexus/development-walkthrough/index.mdx b/docs/develop/java/nexus/development-walkthrough/index.mdx index 8530c0f018..5bbe81c67f 100644 --- a/docs/develop/java/nexus/development-walkthrough/index.mdx +++ b/docs/develop/java/nexus/development-walkthrough/index.mdx @@ -13,7 +13,7 @@ tags: :::caution -This walkthrough covers [Nexus SDK V2](/nexus/sdk-v2), which is pre-release. +This walkthrough covers the [Temporal Operation Handler](/nexus/temporal-operation-handler), which is pre-release. APIs are experimental and may change in backwards-incompatible ways. Please do NOT review the documents under this page past the high level structure. Once we agree on the structure and form I will be working on the docs to match. For now those subpage contents should be considered placeholder text. diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx index c6ce7c97d5..93b3c3f0d8 100644 --- a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -38,7 +38,7 @@ This is also how you make a Signal safe to send to an approval that may not exis :::caution Update-with-Start is not yet available -Update-with-Start — starting a Workflow and running an Update against it atomically — is not available in any SDK yet. See [Nexus SDK V2](/nexus/sdk-v2). +Update-with-Start — starting a Workflow and running an Update against it atomically — is not available in any SDK yet. See [Temporal Operation Handler](/nexus/temporal-operation-handler). Until it lands, an Operation that needs both must start the Workflow and then submit the Update as a separate call, which is not atomic. Signal-with-Start is available and covers the case where the caller does not need a response. @@ -48,7 +48,7 @@ Until it lands, an Operation that needs both must start the Workflow and then su A caller that did not start an approval cannot ask Nexus for its final decision. -`requestApproval` delivers the decision to whoever awaited it. There is no Operation that attaches to an already-running approval and waits for its result, because Get Workflow Result as an async backing is [not yet available](/nexus/sdk-v2) in any SDK. +`requestApproval` delivers the decision to whoever awaited it. There is no Operation that attaches to an already-running approval and waits for its result, because Get Workflow Result as an async backing is [not yet available](/nexus/temporal-operation-handler) in any SDK. If several systems need the outcome, either have the caller that started the approval distribute it, or have the handler notify them — which is what the notification in the next step does. @@ -59,7 +59,7 @@ If several systems need the outcome, either have the caller that started the app :::tip RESOURCES - [Sending messages](/sending-messages) for Signal, Query, and Update semantics. -- [Nexus SDK V2](/nexus/sdk-v2) for which capabilities are available per SDK. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) for which capabilities are available per SDK. - [Java Nexus feature guide](/develop/java/nexus/feature-guide) for the caller API. ::: diff --git a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx index f9591c1a04..66e16d9030 100644 --- a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx +++ b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx @@ -14,7 +14,7 @@ import { SdkTabs } from '@site/src/components'; :::caution -Activity-backed Nexus Operations are pre-release and built on [Nexus SDK V2](/nexus/sdk-v2). +Activity-backed Nexus Operations are pre-release and built on the [Temporal Operation Handler](/nexus/temporal-operation-handler). `TemporalOperationHandler` is marked experimental in the SDKs and may change in backwards-incompatible ways. ::: @@ -22,7 +22,7 @@ Activity-backed Nexus Operations are pre-release and built on [Nexus SDK V2](/ne A Nexus Operation can be backed by a [Standalone Activity](/standalone-activity) as well as a Workflow. Starting the Operation starts an Activity Execution that has no parent Workflow, and the Operation completes when that Activity returns. -Reach for this shape when the work behind an Operation is a single durable step rather than a process: call an external API, run a computation, write to another system. +Use Standalone Activities when the work behind an Operation is a single durable step rather than a process. Examples might be calling an external API, running a computation, or writing to another system. Two things combine here, and it is worth separating them. An [Activity](/activities) gives that step automatic retries, timeouts, and a durable record of what happened. @@ -40,9 +40,9 @@ You get all of the following without building any of it yourself: - **A boundary between teams.** Callers get scoped access to named Operations, not write access to your Namespace, and they never see your Task Queues, your Workers, or your implementation. - **Room to change your mind.** As long as the contract holds you can rewrite what runs behind an Operation, including replacing the Activity with a Workflow later, and no caller changes. -Callers are free either way: a caller Workflow can invoke the Operation as one step of a larger process, or a Client can start it directly as a [Standalone Nexus Operation](/standalone-nexus-operation) with no Workflow on either side. +Either calling style works: a caller Workflow can invoke the Operation as one step of a larger process, or a Client can start it directly as a [Standalone Nexus Operation](/standalone-nexus-operation) with no Workflow on either side. -Here are some example use cases. +A sampling of customer use cases this was built to address follows. ### Durable webhook and event processing without running a queue @@ -93,17 +93,29 @@ The same shape fits anything that is an external trigger, one durable step, and Use `TemporalOperationHandler` and call `startActivity` on the injected Nexus-aware Client. The handler returns an async result carrying an activity-execution Operation token, and the server delivers the Activity's result to the caller through the Nexus completion callback when the Activity finishes. -Click the language tabs to see example code in each language - Go, Java, .Net, Python, and Typescript. - -:::note -This is still a rough draft for feedback. Not all languages are filled in yet. - -::: +Click the language tabs to see example code in each language - Go, Java, .NET, Python, and TypeScript. -Code coming in next draft +```go +var greetOp = temporalnexus.MustNewTemporalOperation( + temporalnexus.TemporalOperationOptions[GreetingInput, GreetingOutput]{ + Name: "greet", + Start: func( + ctx context.Context, + nc temporalnexus.NexusClient, + input GreetingInput, + opts temporalnexus.StartTemporalOperationOptions, + ) (temporalnexus.TemporalOperationResult[GreetingOutput], error) { + return temporalnexus.StartActivity(ctx, nc, client.StartActivityOptions{ + ID: "greet-" + opts.RequestID, + TaskQueue: TaskQueueName, + StartToCloseTimeout: 10 * time.Second, + }, Greet, input) + }, + }) +``` @@ -132,17 +144,68 @@ public class GreetingNexusServiceImpl { -Code coming in next draft +```python +from . import activities + + +@service_handler(service=GreetingNexusService) +class GreetingNexusServiceHandler: + @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), + ) +``` -Code coming in next draft +```typescript +export const greetingServiceHandler = nexus.serviceHandler(greetingService, { + greet: new temporalnexus.TemporalOperationHandler({ + async start(ctx, client, input) { + return await client.typedActivity().startActivity('greet', { + id: `greet-${ctx.requestId}`, + args: [input], + taskQueue: TASK_QUEUE_NAME, + startToCloseTimeout: '10s', + }); + }, + }), +}); +``` -Code coming in next draft +```csharp +[NexusServiceHandler(typeof(IGreetingNexusService))] +public class GreetingNexusServiceHandler +{ + [TemporalOperation] + public Task> Greet( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + GreetingInput input) => + client.StartActivityAsync( + () => GreetingActivities.GreetAsync(input), + new() + { + Id = $"greet-{ctx.RequestId}", + TaskQueue = HandlerWorker.TaskQueueName, + StartToCloseTimeout = TimeSpan.FromSeconds(10), + }); +} +``` @@ -154,7 +217,11 @@ What differs is how it is started, not what it is. -Code coming in next draft +```go +func Greet(ctx context.Context, input GreetingInput) (GreetingOutput, error) { + return GreetingOutput{Message: "Hello, " + input.Name}, nil +} +``` @@ -170,33 +237,50 @@ public interface GreetingActivities { -Code coming in next draft +```python +# activities.py +@activity.defn +async def greet(input: GreetingInput) -> GreetingOutput: + return GreetingOutput(message=f"Hello, {input.name}") +``` -Code coming in next draft +```typescript +export async function greet(input: GreetingInput): Promise { + return { message: `Hello, ${input.name}` }; +} +``` -Code coming in next draft +```csharp +public class GreetingActivities +{ + [Activity] + public static Task GreetAsync(GreetingInput input) => + Task.FromResult(new GreetingOutput($"Hello, {input.Name}")); +} +``` ### Required options -`StartActivityOptions` requires two values that a Workflow-called Activity does not need. +Starting an Activity this way needs values that a Workflow-called Activity does not. -- **An Activity ID**, unique within the Namespace. There is no parent Workflow to scope it. -- **A Task Queue.** It does not have to be the Task Queue the Nexus Endpoint targets, so the Activity can run on its own Worker fleet. +- **An Activity Id**, unique within the Namespace. There is no parent Workflow to scope it. +- **A timeout.** At least one of start-to-close or schedule-to-close. +- **A Task Queue.** Java requires it. Go, Python, TypeScript, and .NET default it to the Task Queue the Operation is running on. Setting it explicitly lets the Activity run on its own Worker fleet rather than the one the Endpoint targets. -Deriving the ID from the Nexus request ID makes the start idempotent. -The server retries a Nexus start request using the same request ID, so each retry targets the same Activity ID rather than starting a second Activity. +Deriving the Id from the Nexus request Id makes the start idempotent. +The server retries a Nexus start request using the same request Id, so each retry targets the same Activity Id rather than starting a second Activity. -Setting `setIdConflictPolicy(ACTIVITY_ID_CONFLICT_POLICY_USE_EXISTING)` attaches to an already-running Activity with that ID instead of failing. -Combined with an ID derived from the Operation *input* rather than the request ID, this lets several Nexus Operations share one Activity Execution and all receive its result. +Setting the Activity Id conflict policy to use-existing attaches to an already-running Activity with that Id instead of failing. +Combined with an Id derived from the Operation *input* rather than the request Id, this lets several Nexus Operations share one Activity Execution and all receive its result. ### Register the Worker @@ -206,7 +290,16 @@ There is no Workflow implementation to register. -Code coming in next draft +```go +w := worker.New(c, TaskQueueName, worker.Options{}) +w.RegisterActivity(Greet) + +service := nexus.NewService("greeting") +if err := service.Register(greetOp); err != nil { + log.Fatal(err) +} +w.RegisterNexusService(service) +``` @@ -220,17 +313,36 @@ worker.registerNexusServiceImplementation(new GreetingNexusServiceImpl()); -Code coming in next draft +```python +worker = Worker( + client, + task_queue=TASK_QUEUE_NAME, + activities=[activities.greet], + nexus_service_handlers=[GreetingNexusServiceHandler()], +) +``` -Code coming in next draft +```typescript +const worker = await Worker.create({ + taskQueue: TASK_QUEUE_NAME, + activities, + nexusServices: [greetingServiceHandler], +}); +``` -Code coming in next draft +```csharp +using var worker = new TemporalWorker( + client, + new TemporalWorkerOptions(TaskQueueName). + AddActivity(GreetingActivities.GreetAsync). + AddNexusService(new GreetingNexusServiceHandler())); +``` @@ -246,42 +358,73 @@ The server records the cancellation request, and the Worker only learns about it So an Activity that never heartbeats runs until it completes or hits its start-to-close timeout, no matter how many cancellation requests the caller sends. For a long-running Activity-backed Operation to be cancellable at all: -- Heartbeat from the Activity, and let the resulting completion exception propagate. +- Heartbeat from the Activity, so it learns that cancellation was requested. +- Let the resulting cancellation exception propagate. A cancelled Activity is not retried, but one that swallows the cancellation and throws an ordinary failure instead is. - Set a heartbeat timeout so the server notices a Worker that has stopped heartbeating. -- Set maximum attempts to 1, or a cancelled attempt is retried and the Operation stays running instead of ending as cancelled. -Code coming in next draft +```go +client.StartActivityOptions{ + ID: "greet-" + opts.RequestID, + TaskQueue: TaskQueueName, + StartToCloseTimeout: 10 * time.Minute, + HeartbeatTimeout: 5 * time.Second, +} +``` ```java StartActivityOptions.newBuilder() - .setId("greeting-" + context.getRequestId()) + .setId("greet-" + context.getRequestId()) .setTaskQueue(HandlerWorker.TASK_QUEUE_NAME) .setStartToCloseTimeout(Duration.ofMinutes(10)) .setHeartbeatTimeout(Duration.ofSeconds(5)) - .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).build()) .build(); ``` -Code coming in next draft +```python +await client.start_activity( + activities.greet, + input, + id=f"greet-{ctx.request_id}", + task_queue=TASK_QUEUE_NAME, + start_to_close_timeout=timedelta(minutes=10), + heartbeat_timeout=timedelta(seconds=5), +) +``` -Code coming in next draft +```typescript +await client.typedActivity().startActivity('greet', { + id: `greet-${ctx.requestId}`, + args: [input], + taskQueue: TASK_QUEUE_NAME, + startToCloseTimeout: '10m', + heartbeatTimeout: '5s', +}); +``` -Code coming in next draft +```csharp +new StartActivityOptions +{ + Id = $"greet-{ctx.RequestId}", + TaskQueue = HandlerWorker.TaskQueueName, + StartToCloseTimeout = TimeSpan.FromMinutes(10), + HeartbeatTimeout = TimeSpan.FromSeconds(5), +} +``` @@ -300,7 +443,7 @@ Sample code: `{code not yet live}` :::tip RESOURCES -- [Nexus SDK V2](/nexus/sdk-v2) for `TemporalOperationHandler` and the Nexus-aware Client. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) for the handler type and the Nexus-aware Client. - [Standalone Activity](/standalone-activity) for the underlying concept, and [Java: Standalone Activities](/develop/java/activities/standalone-activities) for the SDK API. - [Standalone Nexus Operation](/standalone-nexus-operation) for starting Operations without a caller Workflow. - [Development Walkthrough](/develop/java/nexus/development-walkthrough) uses an Activity-backed Operation in context. diff --git a/docs/encyclopedia/nexus/nexus.mdx b/docs/encyclopedia/nexus/nexus.mdx index a0388ba76e..38781924ea 100644 --- a/docs/encyclopedia/nexus/nexus.mdx +++ b/docs/encyclopedia/nexus/nexus.mdx @@ -136,7 +136,7 @@ A new way of building Nexus Services is in pre-release. It combines a contract-f The APIs are experimental, so expect them to change. -- [Nexus SDK V2](/nexus/sdk-v2) - Implement Operations with `TemporalOperationHandler`, and get bidirectional linking across the Namespace boundary. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) - Implement Operations with a single handler type, and get bidirectional linking across the Namespace boundary. - [Nexus Client Code Generator](/nexus/client-code-generator) - Generate typed models, validators, and Service definitions for Go, Java, Python, and TypeScript from one schema. - [Nexus Standalone Activity](/nexus/standalone-activity) - Back an Operation with a single durable step instead of a Workflow. - [Development Walkthrough (Java)](/develop/java/nexus/development-walkthrough) - Build a Nexus Service end to end, adding one capability at a time. This walkthrough is not in the navigation yet, so this link is its entry point. diff --git a/docs/encyclopedia/nexus/nexus-sdk-v2.mdx b/docs/encyclopedia/nexus/temporal-operation-handler.mdx similarity index 98% rename from docs/encyclopedia/nexus/nexus-sdk-v2.mdx rename to docs/encyclopedia/nexus/temporal-operation-handler.mdx index 37687ab815..c00c869646 100644 --- a/docs/encyclopedia/nexus/nexus-sdk-v2.mdx +++ b/docs/encyclopedia/nexus/temporal-operation-handler.mdx @@ -1,10 +1,10 @@ --- -id: nexus-sdk-v2 -title: Nexus SDK V2 -sidebar_label: Nexus SDK V2 +id: temporal-operation-handler +title: Temporal Operation Handler +sidebar_label: Temporal Operation Handler description: Implement Nexus Operations with the Temporal Operation Handler - back an Operation with a Workflow, an Update, or an Activity, and get bidirectional linking across the Namespace boundary. toc_max_heading_level: 4 -slug: /nexus/sdk-v2 +slug: /nexus/temporal-operation-handler tags: - Nexus - Concepts @@ -14,9 +14,8 @@ import { SdkTabs } from '@site/src/components'; :::caution -Nexus SDK V2 is pre-release. +The Temporal Operation Handler is pre-release. `TemporalOperationHandler` is marked experimental in the SDKs and may change in backwards-incompatible ways. -"SDK V2" is a working title used while the feature is in pre-release. ::: @@ -489,7 +488,7 @@ Replace it with [Send a Signal from an Operation](#send-a-signal-from-an-operati - [Nexus Client Code Generator](/nexus/client-code-generator) to generate Service contracts and typed models from one schema. - [Nexus Standalone Activity](/nexus/standalone-activity) for Activity-backed Operations. - [Bidirectional linking](/nexus/execution-debugging#bi-directional-linking) for what the Nexus-aware Client gives you. -- [Development Walkthrough](/develop/java/nexus/development-walkthrough) builds a Nexus Service end to end using SDK V2. +- [Development Walkthrough](/develop/java/nexus/development-walkthrough) builds a Nexus Service end to end with the Temporal Operation Handler. - Nexus feature guides: [Go](/develop/go/nexus/feature-guide) | [Java](/develop/java/nexus/feature-guide) | diff --git a/sidebars.js b/sidebars.js index a043e13db1..b97a6b460c 100644 --- a/sidebars.js +++ b/sidebars.js @@ -2069,7 +2069,7 @@ module.exports = { }, items: [ 'encyclopedia/nexus/nexus-services', - 'encyclopedia/nexus/nexus-sdk-v2', + 'encyclopedia/nexus/temporal-operation-handler', 'encyclopedia/nexus/nexus-client-code-generator', 'encyclopedia/nexus/nexus-standalone-activity', 'encyclopedia/nexus/nexus-operations', From 431a734d3b30b29184dbe1e93496b913af491338 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Mon, 17 Aug 2026 18:27:48 -0700 Subject: [PATCH 11/16] Working on operation handler doc --- .../nexus/temporal-operation-handler.mdx | 216 +++++++++++++++--- 1 file changed, 179 insertions(+), 37 deletions(-) diff --git a/docs/encyclopedia/nexus/temporal-operation-handler.mdx b/docs/encyclopedia/nexus/temporal-operation-handler.mdx index c00c869646..dda4fd3586 100644 --- a/docs/encyclopedia/nexus/temporal-operation-handler.mdx +++ b/docs/encyclopedia/nexus/temporal-operation-handler.mdx @@ -27,7 +27,7 @@ Whichever you choose, the handler is the same shape, and every Execution it touc ## 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](/nexus/standalone-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. +**Back an Operation with whichever primitive fits the work.** A multi-step process is a Workflow. A single durable step is an [Activity](/activities), 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. @@ -41,37 +41,31 @@ Whichever you choose, the handler is the same shape, and every Execution it touc ## The Nexus-aware Client -`TemporalOperationHandler.create(...)` gives your start handler three things: a context, a Client, and the Operation input. +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. +Each SDK spells them differently; the examples below show the exact call per language. **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. -- `client.startWorkflow(...)` — the Operation completes when the Workflow returns -- `client.startWorkflowUpdate(...)` — the Operation completes when the Update completes -- `client.startActivity(...)` — the Operation completes when the Activity returns; see [Nexus Standalone Activity](/nexus/standalone-activity) +- 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](/nexus/standalone-activity) -**Sync messaging — as many as you need.** Reach these through `client.getWorkflowClient()`. +**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 and Signal-with-Start +- 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 -Query, Cancel, and Terminate are not part of the pre-release. -You can still reach them through a Temporal Client of your own, but a message sent that way is not linked back to the caller. - -A handler that only sends messages returns `TemporalOperationResult.sync(...)`, and the Operation completes immediately. +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, a `cancelOrder` Operation that sends a Signal, and a `greet` Operation backed by an Activity. Click the language tabs to see example code in each language - Go, Java, .Net, Python, and Typescript. - -:::note -This is still a rough draft for feedback. Not all languages are filled in yet. - -::: +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. Click the language tabs to see example code in each language - Go, Java, .NET, Python, and TypeScript. ### Back an Operation with a Workflow @@ -161,6 +155,111 @@ TemporalOperationHandler.FromHandleFactory( Go exposes the start calls as package-level functions taking the Client, rather than as methods on it, because Go does not allow generic methods on a non-generic struct. +### 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. + + + + +```go +op := temporalnexus.MustNewTemporalOperation( + temporalnexus.TemporalOperationOptions[UpdateAddressInput, AddressOutput]{ + Name: "updateShippingAddress", + Start: func( + ctx context.Context, + nc temporalnexus.NexusClient, + input UpdateAddressInput, + _ temporalnexus.StartTemporalOperationOptions, + ) (temporalnexus.TemporalOperationResult[AddressOutput], error) { + return temporalnexus.StartUpdateWorkflow[AddressOutput](ctx, nc, + client.UpdateWorkflowOptions{ + WorkflowID: "order-" + input.OrderID, + UpdateName: "updateShippingAddress", + Args: []any{input}, + WaitForStage: client.WorkflowUpdateStageAccepted, + }) + }, + }) +``` + + + + +```java +@OperationImpl +public OperationHandler updateShippingAddress() { + return TemporalOperationHandler.create( + (context, client, input) -> + client.startWorkflowUpdate( + OrderWorkflow.class, + "order-" + input.getOrderId(), + OrderWorkflow::updateShippingAddress, + input, + UpdateOptions.newBuilder(AddressOutput.class) + .setUpdateName("updateShippingAddress") + .setWaitForStage(WorkflowUpdateStage.ACCEPTED) + .build())); +} +``` + + + + +```python +@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, + ) +``` + + + + +```typescript +const updateShippingAddressOp = new temporalnexus.TemporalOperationHandler< + UpdateAddressInput, + AddressOutput +>({ + async start(ctx, client, input) { + return await client + .getWorkflowHandle(`order-${input.orderId}`) + .update(shippingAddressUpdate, { args: [input] }); + }, +}); +``` + + + + +```csharp +TemporalOperationHandler.FromHandleFactory( + async (context, client, input) => + await client.StartWorkflowUpdateAsync( + $"order-{input.OrderId}", + wf => wf.UpdateShippingAddressAsync(input), + new() { WaitForStage = WorkflowUpdateStage.Accepted })); +``` + + + + +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". Go, Java, and .NET require you to set it and fail the Operation if it is anything else. Python and TypeScript set it for you. +- **A callback URL is required.** The Operation is how the Update's result reaches the caller, so a caller that provided no callback URL gets a `BAD_REQUEST` handler error. +- **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. @@ -254,7 +353,7 @@ The same Client also offers Signal-with-Start, and a handler may send several me ### 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 a Task Queue. See [Nexus Standalone Activity](/nexus/standalone-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. Java also requires a Task Queue; the other SDKs default it to the Task Queue the Operation is running on. See [Nexus Standalone Activity](/nexus/standalone-activity). @@ -267,13 +366,13 @@ op := temporalnexus.MustNewTemporalOperation( ctx context.Context, nc temporalnexus.NexusClient, input GreetingInput, - _ temporalnexus.StartTemporalOperationOptions, + opts temporalnexus.StartTemporalOperationOptions, ) (temporalnexus.TemporalOperationResult[GreetingOutput], error) { return temporalnexus.StartActivity(ctx, nc, client.StartActivityOptions{ - ID: "greet-" + input.Name, + ID: "greet-" + opts.RequestID, TaskQueue: TaskQueueName, StartToCloseTimeout: 10 * time.Second, - }, GreetingActivities.Greet, input) + }, Greet, input) }, }) ``` @@ -299,6 +398,42 @@ public OperationHandler greet() { ``` + + +```python +@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), + ) +``` + + + + +```typescript +const greet = new temporalnexus.TemporalOperationHandler({ + async start(ctx, client, input) { + return await client.typedActivity().startActivity('greet', { + id: `greet-${ctx.requestId}`, + args: [input], + taskQueue: TASK_QUEUE_NAME, + startToCloseTimeout: '10s', + }); + }, +}); +``` + + ```csharp @@ -308,23 +443,13 @@ TemporalOperationHandler.FromHandleFactory( () => GreetingActivities.GreetAsync(input), new() { - Id = $"greet-{input.Name}", + Id = $"greet-{context.RequestId}", TaskQueue = TaskQueueName, - ScheduleToCloseTimeout = TimeSpan.FromMinutes(1), + StartToCloseTimeout = TimeSpan.FromSeconds(10), })); ``` - - -Code coming in next draft - - - - -Code coming in next draft - - ## Coming from the earlier handler APIs @@ -335,9 +460,9 @@ Earlier SDK versions had a separate helper per pattern. Existing handlers will k | If you used | Use instead | | --- | --- | -| The Workflow-run helper (`WorkflowRunOperation`, `NewWorkflowRunOperation`, `@workflow_run_operation`) | `TemporalOperationHandler` with `startWorkflow` | +| 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 `startActivity` | +| 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](/nexus/execution-debugging#bi-directional-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. @@ -470,12 +595,29 @@ async def cancel_order( -Code coming in next draft +```typescript +nexus.serviceHandler(orderService, { + async cancelOrder(ctx, input) { + await temporalnexus + .getClient() + .workflow.getHandle(`order-${input.orderId}`) + .signal(requestCancellation, input); + }, +}); +``` -Code coming in next draft +```csharp +OperationHandler.Sync(async (ctx, input) => +{ + await NexusOperationExecutionContext.Current.TemporalClient + .GetWorkflowHandle($"order-{input.OrderId}") + .SignalAsync("requestCancellation", new object?[] { input }); + return default; +}); +``` From 3ede7b081d7fe8c086fe5ae2c16627b7656789e3 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Wed, 19 Aug 2026 16:32:15 -0700 Subject: [PATCH 12/16] Working on docs --- .../add-a-standalone-activity.mdx | 22 +++--- .../development-walkthrough/add-messaging.mdx | 55 ++++++++------ .../call-the-service.mdx | 62 ++++++++++++---- .../call-the-standalone-activity.mdx | 23 +++--- .../choose-backing-implementation.mdx | 66 ++++++++++++----- .../debugging-and-tips.mdx | 38 +++++----- .../define-the-data-contract.mdx | 44 +++++++---- .../development-walkthrough/generate-code.mdx | 30 ++++---- .../implement-the-service.mdx | 22 +++--- .../nexus/development-walkthrough/index.mdx | 70 ++++++++++++------ .../publish-in-nexus.mdx | 14 +++- .../development-walkthrough/send-messages.mdx | 73 +++++++++++++------ .../nexus/nexus-client-code-generator.mdx | 8 +- .../nexus/nexus-standalone-activity.mdx | 2 +- docs/encyclopedia/nexus/nexus.mdx | 2 +- .../nexus/temporal-operation-handler.mdx | 2 +- 16 files changed, 347 insertions(+), 186 deletions(-) diff --git a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx index 1a626ce586..a7f8085670 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx @@ -18,15 +18,15 @@ This one is not a Workflow. It is a single outbound notification with no state, The Activity is an ordinary Activity. In this walkthrough it is a placeholder that does nothing — no email is sent. Real logic would call an email provider, push to a notification service, or write to an outbox. -Nothing about it is Nexus-specific. The same Activity could be called from a Workflow. +Nothing in it is Nexus-specific. The same Activity Function can be invoked from a Workflow and started behind this Operation with no code changes — what differs is what starts it, not how it is written. `{sample code will be here}` ## Back the Operation with it -Use `TemporalOperationHandler` as with every other Operation, but call `startActivity` on the Nexus-aware Client instead of `startWorkflow`. The Operation starts an Activity Execution with no parent Workflow and completes when the Activity returns. +Use `TemporalOperationHandler` as with every other Operation, but start an Activity on the [Client](/nexus/temporal-operation-handler#the-nexus-aware-client) instead of a Workflow. The Operation starts an Activity Execution with no parent Workflow and completes when the Activity returns. -Before Activity-backed Operations, this Operation would have needed a Workflow whose only job was to call this one Activity — a wrapper with its own Event History and Workflow Id, providing nothing. +This is the right shape whenever an Operation is one durable step behind a team boundary. The Activity supplies the durability — retries on the policy you set, timeouts you control, and a record of every attempt — and the Operation supplies the contract, so the notification is reachable by other teams without them sharing your code or your Namespace. `{sample code will be here}` @@ -37,9 +37,11 @@ Before Activity-backed Operations, this Operation would have needed a Workflow w - **An Activity Id**, unique within the Namespace. - **A Task Queue.** It does not have to be the Endpoint's target Task Queue, so notifications can run on their own Worker fleet. -Derive the Activity Id from the Nexus request Id to make the start idempotent. The server retries a Nexus start request with the same request Id, so each retry targets the same Activity Id instead of sending a second notification. +**To keep server retries from sending a second email, derive the Activity Id from the Nexus request Id.** The server retries Nexus start requests, and the request Id travels with them, so every retry lands on the same Activity Id and the notification goes out once. Without that, a retried request is a duplicate message to a real person. -That last point matters here more than usual. A duplicate Workflow start is usually harmless; a duplicate notification is a second email to a real person. +The same pattern applies to any Operation whose work is externally visible and cannot be taken back: charging a card, posting to a webhook, creating a ticket, writing to a system with no dedup of its own. Deriving the Id from the request Id costs nothing and removes the whole class of duplicate-side-effect bugs. + +Deriving the Id from the Operation *input* instead is a different tool for a different job: it makes several Operations share one Activity Execution and all receive its result. See [Nexus Standalone Activity](/nexus/standalone-activity#required-options). ## Register the Activity on the Worker @@ -49,15 +51,15 @@ Add the Activity implementation to the same Worker that hosts the Nexus Service. ## Cancellation needs heartbeating -For this notification the point is moot — it finishes immediately. But it is worth knowing before you write a longer Activity-backed Operation, because the behavior differs from a Workflow-backed one. - -A Workflow is interrupted by a cancellation request. An Activity is not: the server records the request, and the Worker only finds out on its next heartbeat. An Activity that never heartbeats runs until it completes or hits its start-to-close timeout, however many cancellation requests arrive. +This notification finishes immediately, so cancellation never comes up for it. It does come up for any longer Activity-backed Operation, and the behavior differs from a Workflow-backed one: an Activity is not interrupted by a cancellation request, so an Activity that never heartbeats runs to completion or to its timeout no matter how many cancellations arrive. -Making a long-running Activity-backed Operation cancellable takes three settings together — heartbeating from the Activity, a heartbeat timeout, and maximum attempts of 1 so a cancelled attempt is not retried. See [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating). +If you write a long-running Activity-backed Operation, read [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating) before you ship it. ## Next -[Call the Standalone Activity](/develop/java/nexus/development-walkthrough/call-the-standalone-activity). +**[Step 10 - Call the Standalone Activity](/develop/java/nexus/development-walkthrough/call-the-standalone-activity)** - complete the approval flow end to end. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx index 8e654e642c..e9c9fffad0 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -2,7 +2,7 @@ id: add-messaging title: Step 7 - Add messaging sidebar_label: 7. Add messaging -description: Expose Signal, Query, and Update on the approval Workflow as Nexus Operations using the Nexus-aware Client. +description: Expose a Signal and an Update on the approval Workflow as Nexus Operations using the Nexus-aware Client. toc_max_heading_level: 4 unlisted: true tags: @@ -10,70 +10,77 @@ tags: - Java SDK --- -The approval blocks waiting for a decision. Now give callers a way to interact with it while it waits. +The approval blocks waiting for a decision. **Messages** give callers a way to interact with it while it waits. -Three Operations get added, one per [message](/sending-messages) type. Which message type to use is determined by what the caller needs back, not by preference. +Two Operations get added to the workflow sample problem. Which [message](/sending-messages) type each one uses is decided by what the caller needs back, not by preference. | Operation | Message type | Why this type | | --- | --- | --- | | `remindApprover` | Signal | Fire-and-forget. The caller does not need a response, only for the nudge to happen. | -| `getApprovalStatus` | Query | Reads state without changing it. Never blocks, never writes. | | `submitDecision` | Update | Changes state *and* returns a result the caller needs — confirmation the decision was recorded. | +That is the whole rule. If the caller can proceed without hearing anything back, a Signal is enough. If the caller needs to know what the message did, it needs an Update. + ## Add the handlers to the Workflow -On the Workflow, add a Signal handler that increments the reminder count, a Query handler that returns the current progress, and an Update handler that records the decision and unblocks the wait. +On the Workflow, add a Signal handler that increments the reminder count and an Update handler that records the decision and unblocks the wait. The Update is what ends the approval. It records `APPROVED` or `DENIED`, which satisfies the condition the Workflow is blocked on, and the Workflow then returns that decision as its result. `{sample code will be here}` -Two constraints apply to the Query handler. It must not block, and it must not mutate Workflow state — a Query is served by replaying history, so anything it changes is invisible and anything it waits on stalls the Query. Return only what is already in memory. - ## Expose them as Nexus Operations -These use `TemporalOperationHandler`, and they divide along the line described in [The Nexus-aware Client](/nexus/temporal-operation-handler#the-nexus-aware-client): Signal is **sync messaging**, and Update is an **async backing**. +Both use `TemporalOperationHandler`, and they divide along the line described in [The Nexus-aware Client](/nexus/temporal-operation-handler#the-nexus-aware-client): a Signal is **sync messaging**, and an Update is an **async backing**. ### Signal -Reach it through `client.getWorkflowClient()` on the Nexus-aware Client, then return `TemporalOperationResult.sync(...)`. The Operation completes immediately, during the handler call. +Send the Signal through the Client, then return a synchronous result. The Operation completes immediately, during the handler call. + +:::caution The handler has under 10 seconds + +A synchronous handler must finish inside the [10-second handler deadline](/cloud/limits#nexus-operation-request-timeout), and the budget you actually get is smaller: the clock starts on the caller's side and the request still has to route through matching. -You can send as many messages as you want in one handler. Using the injected Client rather than your own is what gets the message linked back to the caller. +Sending one Signal is comfortably inside it. A handler that sends several messages, or does slow work before returning, is not. Overrunning gives the caller a context deadline exceeded error, which it then retries with exponential backoff until the schedule-to-close timeout expires. + +::: `{sample code will be here}` ### Update -Use `client.startWorkflowUpdate(...)`, which is an async backing: the Operation completes when the Update completes, and its result is delivered through the Nexus completion callback. If the Update happens to come back already complete — a retried request, or one that failed validation — the result returns synchronously instead. +Start the Update on the Client. This is an async backing: the Operation completes when the Update completes, and its result is delivered through the Nexus completion callback. If the Update happens to come back already complete — a retried request, or one that failed validation — the result returns synchronously instead. -Because it is an async backing, there is at most one per Operation invocation. A handler can still combine it with sync side effects. +An Update-backed Operation carries two requirements. It targets a Workflow that already exists, so a `submitDecision` for a purchase with no approval running fails. And because it is an async backing, there is at most one per Operation invocation, though a handler can still combine it with sync side effects. `{sample code will be here}` -:::caution Query is not in the pre-release +## Do not poll for the decision -`getApprovalStatus` belongs in the contract, but Query is not available as sync messaging on the Nexus-aware Client in the pre-release, so this Operation cannot be implemented yet. See [Temporal Operation Handler](/nexus/temporal-operation-handler). +There is one design mistake worth naming, because it is the most common one in this shape: reaching for a message to fetch the final decision. -Adding the Query handler to the Workflow is still worth doing — it costs nothing and the Operation can be wired up once Query lands. Until then, a caller that needs in-flight progress has to obtain it outside this Service. +The decision is the result of `requestApproval`, and it reaches the caller without anyone asking for it. -::: +When the handler started the approval, Nexus attached a [completion callback](/glossary#nexus-async-completion-callback) to that Workflow. The moment the Workflow returns, the handler's Namespace delivers the callback to the caller's Nexus Machinery, which records a `NexusOperationCompleted` event in the caller Workflow's history. The caller Worker picks that up on its next Workflow Task, and the caller Workflow resumes with the decision. See the [asynchronous Operation lifecycle](/nexus/operations#asynchronous-operation-lifecycle) for the full sequence. -## Keep the responsibilities separate +A caller that instead asks the approval for its status in a loop is polling for something already on its way. -It is worth restating why there are three Operations rather than one flexible one, because collapsing them is a common mistake. +Messages are for changing a running approval or nudging it along, not for collecting its outcome. `remindApprover` asks the approver again. `submitDecision` supplies the decision and confirms it landed. Neither is a way to read the result. -`getApprovalStatus` reports **in-flight progress only** — whether a decision is still pending, and how many reminders have gone out. It does not return the final decision. The decision is the result of `requestApproval`, which the caller is already awaiting from [step 6](/develop/java/nexus/development-walkthrough/call-the-service). +Both also stop working the moment the approval completes. The Temporal Service accepts a Signal or an Update only while the Workflow is still running, and rejects one sent to a closed Workflow with `NOT_FOUND: workflow execution already completed`. That happens as soon as the decision lands, not when the [Retention Period](/temporal-service/temporal-server#retention-period) later expires and the Execution is deleted. A second `submitDecision` for an approval that has already been decided fails this way, and so does any attempt to use these Operations to look up a past decision. -Using a Query to fetch the outcome would mean polling for something that is already being pushed, and it would break once the [Retention Period](/temporal-service/temporal-server#retention-period) expires and the history the Query replays is gone. +If more than one system needs the outcome, see [step 8](/develop/java/nexus/development-walkthrough/send-messages#when-the-approval-already-exists) for how additional callers attach to a running approval and receive the same decision. ## Next -[Send messages](/develop/java/nexus/development-walkthrough/send-messages) from the caller. +**[Step 8 - Send messages](/develop/java/nexus/development-walkthrough/send-messages)** - call the messaging Operations, and handle an approval that may not exist yet. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES -- [Workflow message passing](/encyclopedia/workflow-message-passing) for Signals, Queries, and Updates. -- [Handling messages](/handling-messages) for handler constraints, including Query restrictions. -- [Temporal Operation Handler](/nexus/temporal-operation-handler) for the sync side effect and async backing distinction. +- [Workflow message passing](/encyclopedia/workflow-message-passing) for Signals and Updates. +- [Handling messages](/handling-messages) for handler constraints. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) for the sync messaging and async backing distinction. ::: diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx index 2620c2226b..345f3da816 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx @@ -2,7 +2,7 @@ id: call-the-service title: Step 6 - Call the Service sidebar_label: 6. Call the Service -description: Call the approval Nexus Service from a caller Workflow in another Namespace using the generated Service interface. +description: Call the approval Nexus Service from a caller Workflow in another Namespace, using the generated Service interface, and run the callers from other languages against the same handler. toc_max_heading_level: 4 unlisted: true tags: @@ -10,35 +10,65 @@ tags: - Java SDK --- -Call `requestApproval` from a Workflow in the caller Namespace. The caller knows the Endpoint name and the contract, and nothing else about the handler. +Call the approval Operations from a Workflow in the caller Namespace. The caller knows two things — the Endpoint name and the contract from [step 1](/develop/java/nexus/development-walkthrough/define-the-data-contract) — and nothing else about the handler. -## Use the generated interface as a stub +The caller built here is Java, the same language as the handler, but nothing about the handler requires that. [Call it from another language](#call-it-from-another-language) covers the cross-language case, which is the same call against the same Endpoint. -In Java, the generated Service interface works directly as a Nexus Service stub. Create it inside the caller Workflow with the Endpoint name and Operation options, then call its methods as if they were local. +## What the caller gets from the contract + +The caller does not hand-write request types, response types, or Operation names. [Step 2](/develop/java/nexus/development-walkthrough/generate-code) generated all of it from the contract, and the caller works against that generated code: + +- **A Service definition** naming the Service and its Operations, so an Operation name is a symbol rather than a string you can misspell. +- **Typed models** for every input and output in the contract. +- **Runtime validators** that reject a payload violating the contract before it reaches the wire. + +The practical effect is that the contract is enforced twice. A field the contract does not have fails at build time in a typed language, and a payload the contract forbids fails at the boundary rather than inside the handler's Workflow. + +## Call the Operations from a caller Workflow -Because the stub is the generated interface, the call is type-checked against the contract at compile time. A field the contract does not have will not compile, and a payload the contract forbids is rejected by the generated validator before it reaches the wire. +The flow follows the walkthrough sample problem. Check whether the purchase needs approval at all; if it does, request one and wait for the decision. + +In Java, the generated Service interface works directly as a Nexus Service stub. Create it inside the caller Workflow with the Endpoint name and Operation options, then call its methods as if they were local. `{sample code will be here}` +Nothing in this caller is aware of how the handler is built. It does not know which Task Queue the handler's Worker polls, or that `requestApproval` is backed by a Workflow while `checkApprovalRequired` is backed by nothing at all. It knows the Endpoint name and the contract. + +That is the property worth pausing on: the handler team can change what backs an Operation, move the handler to another Namespace, or rewrite it in another language, and this caller keeps working. + ## Await the decision `requestApproval` returns `APPROVED` or `DENIED`. That value is the approval Workflow's return value, delivered to the caller through the Nexus completion callback when the Workflow finishes. The caller does not poll. It awaits the Operation, and the wait is durable — the caller Workflow can be evicted, the Worker can restart, and the result still arrives. -Set a schedule-to-close timeout that reflects how long an approval can legitimately take. A human approval measured in days needs a timeout in days; the default is not going to be right. See [Nexus Operations](/nexus/operations) for the timeout model. +`checkApprovalRequired` behaves differently and it is worth noticing the contrast. It returns during the call, because nothing durable backs it. There is no callback, no Operation token, and nothing to await. -## Callers in other languages +### Set timeouts -The caller here is Java, but nothing about the handler requires that. +A caller sets three timeouts on a Nexus Operation, each bounding a different stage: -:::note For reviewers +- **Schedule-to-close** bounds the whole Operation, from scheduling to completion. Set it to reflect how long an approval can legitimately take — a human approval measured in days needs a timeout in days, and the default is not going to be right. +- **Schedule-to-start** bounds how long the caller waits for the handler to pick the Operation up. Set it when you want a handler that is down to fail fast, even though the approval itself may run for days. +- **Start-to-close** bounds an asynchronous Operation after it has started. Synchronous Operations like `checkApprovalRequired` ignore it, because they complete as part of the start request. -Every sample in this walkthrough is generated from the contract written in [step 1](/develop/java/nexus/development-walkthrough/define-the-data-contract), so a caller in any supported language talks to this same Java handler without changes on either side. Readers working in Go, Python, or TypeScript will be able to find a caller for their language in that language's sample repository rather than porting this one by hand. +See [Nexus Operations](/nexus/operations#timeouts) for the full timeout model. -To generate a caller for another language from this contract, see [Generate code](/nexus/client-code-generator#generate-code) in the Nexus Client Code Generator documentation. +## Call it from another language -::: +The caller does not have to be written in the same language as the handler. Each language has a sample repository that builds this same approval Service from this same contract, and each one carries a working caller as well as a working handler: + +| Language | Sample | +| --- | --- | +| Go | `{sample repo link}` | +| Python | `{sample repo link}` | +| TypeScript | `{sample repo link}` | + +Check the README in each repository for how to run its client. Point it at the Endpoint created in [step 5](/develop/java/nexus/development-walkthrough/publish-in-nexus) and it drives the Java handler built here, with no changes on either side. + +The interop runs both directions. Every one of those clients can call this Java Service, and the Java caller built in this step can call the Service from any of those repositories. The contract is the only thing the two sides share, so neither side needs to know the other's language, Namespace, or deployment. + +To generate a caller for another language from this contract yourself rather than running a sample, see [Generate code](/nexus/client-code-generator#generate-code). ## Calling without a caller Workflow @@ -46,9 +76,15 @@ A caller Workflow is the usual pattern and the one this walkthrough uses, becaus If you only need to run one Operation and have nothing to orchestrate, a Client can start an Operation directly with no caller Workflow at all. That is a [Standalone Nexus Operation](/standalone-nexus-operation), and it uses the same Service contract, the same handler, and the same Endpoint — only the caller side differs. See [Java: Standalone Operations](/develop/java/nexus/standalone-operations). +`checkApprovalRequired` is a natural fit for this. A caller that only wants to know whether approval is needed has nothing to orchestrate and no result to await. + ## Next -The Service can start an approval and return a decision. [Add messaging](/develop/java/nexus/development-walkthrough/add-messaging) so callers can interact with an approval while it is pending. +The Service can start an approval and return a decision. + +**[Step 7 - Add messaging](/develop/java/nexus/development-walkthrough/add-messaging)** - let callers interact with an approval while it is pending. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx index 46a6aeb648..aac4021aaf 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -14,7 +14,7 @@ Call `notifyRequester` once the decision is final. From the caller's side there ## The caller cannot tell the difference -`notifyRequester` is called exactly like `requestApproval`: through the same generated Service stub, with the same Endpoint, the same type checking, and the same error handling. +`notifyRequester` is called exactly like `requestApproval`: through the same generated stub, with the same Endpoint, the same type checking, and the same error handling. A caller in any language calls it the same way, as in [step 6](/develop/java/nexus/development-walkthrough/call-the-service). Nothing in the caller reveals that this Operation is backed by an Activity and the other by a Workflow. That is the contract doing its job. The handler team could later replace the notification Activity with a Workflow that retries across providers and escalates on failure, and no caller would change. @@ -22,23 +22,24 @@ Nothing in the caller reveals that this Operation is backed by an Activity and t ## Complete the flow -With all ten steps in place, the caller Workflow runs the whole approval: +With all ten steps in place, the caller runs the whole approval: -1. Call `requestApproval` and await it. The Operation starts the approval Workflow in the handler Namespace. -2. While it is pending, other systems call `remindApprover` to nudge and `getApprovalStatus` to report progress. -3. Someone calls `submitDecision` with `APPROVED` or `DENIED`. The Update records it, confirms to that caller, and unblocks the approval Workflow. -4. The approval Workflow returns the decision, which resolves the `requestApproval` Operation the original caller has been awaiting. -5. The caller calls `notifyRequester` with the decision, backed by the notification Activity. +1. Call `checkApprovalRequired`. It answers during the call, with nothing durable created. If the purchase is under the threshold, the flow stops here. +2. Call `requestApproval` and await it. The Operation starts the approval Workflow in the handler Namespace, or attaches to one that another Operation already started. +3. While it is pending, other systems call `remindApprover` to nudge the approver and `attachApprovalContext` to add supporting information. +4. Someone calls `submitDecision` with `APPROVED` or `DENIED`. The Update records it, confirms to that caller, and unblocks the approval Workflow. +5. The approval Workflow returns the decision, which resolves the `requestApproval` Operation every attached caller has been awaiting. +6. The caller calls `notifyRequester` with the decision, backed by the notification Activity. `{sample code will be here}` -Every step crossed a Namespace boundary, and the caller never learned a Workflow Id, a Task Queue, or which primitive backed any Operation. +Every step crossed a Namespace boundary, and the caller never learned a Workflow Id, a Task Queue, or which primitive backed any Operation. One Operation ran with no durable Execution at all, one started a Workflow, two sent messages to it, one started a Workflow if it was not already running, and one started an Activity — and from the caller's side they were all just Operations. ## Trace it end to end -Open the caller Workflow in the UI and follow the links. Because the handlers used the Nexus-aware Client, each Operation is connected to the Execution it started or messaged in the handler Namespace, and you can move between the two Namespaces in one view. +Open the caller Workflow in the UI and follow the links. Because the handlers used the [Client](/nexus/temporal-operation-handler#the-nexus-aware-client) rather than constructing their own, each Operation is connected to the Execution it started or messaged in the handler Namespace, and you can move between the two Namespaces in one view. -The exception is `getApprovalStatus`, which is [not implementable in the pre-release](/nexus/temporal-operation-handler) because Query is not yet available as sync messaging. +`checkApprovalRequired` is the exception, and not because anything is wrong: it started no Execution, so there is nothing on the handler side to link to. An Operation with no backing appears as a completed Operation and nothing more. ## Where to go next @@ -51,6 +52,8 @@ The Service is complete but minimal. Natural extensions: Before running this against anything real, read [Debugging, common pitfalls, and tips](/develop/java/nexus/development-walkthrough/debugging-and-tips). +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). + :::tip RESOURCES - [Nexus execution debugging](/nexus/execution-debugging) for tracing across Namespaces. diff --git a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx index 1ac2fcfa06..b37fd542c1 100644 --- a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx +++ b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx @@ -2,7 +2,7 @@ id: choose-backing-implementation title: Step 3 - Choose the backing implementation sidebar_label: 3. Choose the backing implementation -description: Decide whether each Nexus Operation is backed by a Standalone Activity, a Workflow, or an Entity Workflow. +description: Decide what runs behind each Nexus Operation - nothing, a Workflow, or a Standalone Activity - and implement the Operation that needs no backing at all. toc_max_heading_level: 4 unlisted: true tags: @@ -12,50 +12,76 @@ tags: The contract says nothing about what runs behind an Operation. That is deliberate — it is the handler's private decision, and it can change later without touching callers. -There are three shapes to choose from, and picking the wrong one is the most common source of trouble later. +There are three choices, and picking the wrong one is the most common source of trouble later. This step makes the choice for each Operation in the walkthrough's example approval Service, then implements the simplest one. -## Standalone Activity +## No backing Execution -One step, no waiting, no state. Call an external API, run a computation, send a notification. +The handler computes an answer and returns it. Nothing durable is created: no Workflow, no Activity, nothing to cancel, nothing in Event History. -The Operation starts an Activity Execution with no parent Workflow, and completes when the Activity returns. You get retries and a durable record without a wrapper Workflow that exists only to call one Activity. +This fits work that cannot meaningfully fail and returns immediately — applying a rule to the input, deriving a value, reading configuration the handler already holds. The Operation completes during the handler call, and the caller gets the answer in the response. -The tradeoff is that an Activity has no Workflow's ability to receive messages or hold state, and cancellation only works if the Activity heartbeats. See [Nexus Standalone Activity](/nexus/standalone-activity). +The limit is that you get no durability. The code runs inside the Nexus Operation handler, so it is bounded by the request deadline, and a failure fails the request rather than retrying a step. If the work can fail in a way you would want retried, it needs an Activity instead. ## Workflow -More than one step, or any need for durable intermediate state. +More than one step, any need to wait, or any need for durable intermediate state. -The Operation starts a Workflow and completes when that Workflow returns, so the Workflow's return value is the Operation's result. Use this when the work orchestrates several Activities, needs a timer, or needs to survive a Worker restart partway through. +The Operation starts a Workflow and completes when that Workflow returns, so the Workflow's return value is the Operation's result. Use this when the work orchestrates several Activities, needs a timer, needs to receive [messages](/sending-messages) while it runs, or needs to survive a Worker restart partway through. -## Entity Workflow +A Workflow that represents one long-lived thing and stays reachable for messages is still just a Workflow — what makes it interactive is that it has message handlers and a Workflow Id you can predict, not a different kind of primitive. -A Workflow that represents one long-lived thing and stays available for interaction while it runs. +## Standalone Activity -The distinguishing feature is that it accepts [messages](/sending-messages) — Signals, Queries, and Updates — against a stable Workflow Id derived from the entity it represents. It is still a Workflow-backed Operation; "entity" describes how you use it, not a separate mechanism. +One step, no waiting, no state. Call an external API, run a computation, send a notification. -## The choice for the approval Service +The Operation starts an Activity Execution with no parent Workflow, and completes when the Activity returns. You get retries, timeouts, and a durable record of every attempt without a wrapper Workflow that exists only to call one Activity. -The approval problem needs both. +The tradeoff is that an Activity cannot receive messages or hold state, and cancellation only works if the Activity heartbeats. See [Nexus Standalone Activity](/nexus/standalone-activity). + +## The choice for the approval Service | Operation | Backing | Why | | --- | --- | --- | -| `requestApproval` | Entity Workflow | Blocks for a human decision, holds the reminder count, and must accept messages while pending | +| `checkApprovalRequired` | None | Applies a threshold to the input. Nothing to orchestrate, nothing that can fail in a retryable way | +| `requestApproval` | Workflow | Blocks for a human decision, holds the reminder count, and accepts messages while pending | +| `remindApprover` | Workflow (message) | A Signal to the approval started by `requestApproval` | +| `submitDecision` | Workflow (message) | An Update to that same approval, because the caller needs a result back | +| `attachApprovalContext` | Workflow (message) | A Signal that also starts the approval if it does not exist yet | | `notifyRequester` | Standalone Activity | One outbound notification, no state, nothing to wait for | -An approval is a textbook entity: it exists for a while, it has identity, and other systems interact with it during its lifetime. Backing it with an Activity would not work at all — an Activity cannot block for a human and cannot receive a Signal. +Three of these are worth the contrast. + +An approval has to be a Workflow. It exists for a while, it has identity, and other systems interact with it during its lifetime. Backing it with an Activity would not work at all — an Activity cannot block for a human and cannot receive a Signal. + +The notification is the opposite. It is a single side effect with nothing to orchestrate, so a Workflow would add an Event History and a Workflow Id for no benefit. But it does touch the outside world and can fail, so it needs an Activity rather than nothing. -The notification is the opposite. It is a single side effect with nothing to orchestrate, so a Workflow would add an Event History and a Workflow Id for no benefit. +`checkApprovalRequired` is the case for no backing at all. Compare it against the notification: both are "one small thing," and they get opposite answers. Sending mail can fail and you want that retried with a record of each attempt. Comparing an amount to a threshold cannot fail in any way worth retrying, so an Activity Execution would be pure overhead. -## Give the entity a stable Id +## Give the approval Workflow a stable Id -An Entity Workflow needs a Workflow Id derived from the entity, not a random one, so that later messages can find it. Deriving the approval's Workflow Id from the approval id means a caller that knows the approval id can reach the right Execution. +The approval needs a Workflow Id derived from the purchase, not a random one, so that later messages can find it. Deriving it from the item id means a caller that knows the item id can reach the right Execution without the handler handing out Workflow Ids. -This also makes the start idempotent. A retried Nexus start request targets the same Workflow Id rather than starting a second approval. +This also makes the start idempotent. A retried Nexus start request targets the same Workflow Id rather than starting a second approval for one purchase. + +It matters again in [step 8](/develop/java/nexus/development-walkthrough/send-messages), where `attachApprovalContext` may start the approval before `requestApproval` is ever called. Both Operations derive the same Workflow Id from the same item id, which is what lets them agree on which Execution they mean. + +## Build the Operation that needs no backing + +`checkApprovalRequired` needs no Workflow, no Activity, and no Worker registration beyond the Service itself, so it is the shortest path to a working Operation. + +Implement it with `TemporalOperationHandler` like every other Operation, apply the threshold, and return a synchronous result. The Operation completes during the handler call. + +`{sample code will be here}` + +Using `TemporalOperationHandler` here rather than a plain synchronous handler is what lets this Operation grow later. If spend policy moves out of the handler and into a policy service, this becomes an Activity-backed Operation — and no caller changes, because the contract did not. + +Nothing runs this Operation yet. [Step 4](/develop/java/nexus/development-walkthrough/implement-the-service#run-the-worker) starts the Worker that hosts the Service, and [step 5](/develop/java/nexus/development-walkthrough/publish-in-nexus) makes it reachable, so this is the first Operation you will see respond once those are in place. ## Next -[Implement the Service](/develop/java/nexus/development-walkthrough/implement-the-service) with a Workflow-backed Operation. +**[Step 4 - Implement the Service](/develop/java/nexus/development-walkthrough/implement-the-service)** - back the approval with a Workflow and run a Worker. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx index a7ac461e26..f9cb26f4c4 100644 --- a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -2,7 +2,7 @@ id: debugging-and-tips title: Debugging, common pitfalls, and tips sidebar_label: Debugging and tips -description: Diagnose the most common Nexus failures, from Endpoint authorization to Query constraints, and avoid the pitfalls that are easy to miss. +description: Diagnose the most common Nexus failures, from Endpoint authorization to Workflow Id conflicts, and avoid the pitfalls that are easy to miss. toc_max_heading_level: 4 unlisted: true tags: @@ -30,41 +30,37 @@ Creating an Endpoint does not authorize anyone to call it. Endpoints reject call ## The caller and handler are not linked in the UI -The handler used its own Temporal Client instead of the one `TemporalOperationHandler` injects. +The handler constructed its own Temporal Client instead of using the one `TemporalOperationHandler` provides. -Fetching a Client yourself works, and the Operation behaves correctly, but you lose the [bidirectional links](/nexus/execution-debugging#bi-directional-linking) that connect the two Executions. Use the injected Client for anything that starts or messages an Execution. +Constructing a Client yourself works, and the Operation behaves correctly, but you lose the [bidirectional links](/nexus/execution-debugging#bi-directional-linking) that connect the two Executions. Use the [Client](/nexus/temporal-operation-handler#the-nexus-aware-client) your handler receives for anything that starts or messages an Execution. -Note that Query, Cancel, and Terminate are [not available as sync messaging](/nexus/temporal-operation-handler) in the pre-release. You can still send them with a Client of your own, but nothing links those messages back to the caller. +## The Operation fails because the Workflow already exists -## A Query returns stale data, hangs, or throws +A Workflow-backed Operation starts a Workflow, and by default starting one whose Id is already running is an error. If two Operations derive the same Workflow Id — which is normal and usually intended — the second one fails. -Query handlers have two hard constraints, and violating either fails in confusing ways. +This is the behavior to expect, not a bug. A Workflow-backed Operation has only started successfully once its completion callback is attached, so failing beats reporting success to a caller that would then wait for a result nobody will deliver. -**A Query must not block.** It is served synchronously by replaying history. Waiting on anything stalls the Query rather than delaying it. - -**A Query must not mutate state.** Changes made during a Query are not recorded in Event History, so they are invisible and will not survive. Return only what is already in memory. - -If a Query against a completed approval fails, the [Retention Period](/temporal-service/temporal-server#retention-period) has probably expired and the history it needs to replay is gone. +When you want the second caller to join the Execution rather than fail, set the Workflow Id conflict policy to use-existing. See [When the approval already exists](/develop/java/nexus/development-walkthrough/send-messages#when-the-approval-already-exists). ## Pitfalls that are easy to miss -### Using a Query to get the final result +### Polling for a result that is already being delivered The single most common design mistake in this shape. -The approval's decision is the result of `requestApproval` — the Workflow's return value, pushed to the caller when the Workflow completes. Querying for it instead means polling for something already being delivered, it requires the Workflow code to stay deployed and replay-compatible, and it stops working when history ages out. +The approval's decision is the result of `requestApproval` — the Workflow's return value, pushed to whoever awaited the Operation the moment the Workflow completes. Asking the approval for its status in a loop means polling for something already on its way to you, and it stops working entirely once the approval completes and its [Retention Period](/temporal-service/temporal-server#retention-period) expires. -Use the Operation result for outcomes. Use a Query for in-flight progress. +Use the Operation result for outcomes. Use messages to change a running approval, not to read it. -### Expecting to re-attach to a running Operation +### Expecting a late caller to collect a finished result -There is no Operation that attaches to an already-running Execution and waits for its result. Get Workflow Result as an async backing is [not yet available](/nexus/temporal-operation-handler). +Whoever is attached to an Operation receives its result. A caller that shows up after the approval has completed has nothing to attach to. -Whoever starts the Operation is who receives the result. If other systems need it, distribute it from the caller or notify them from the handler. +While the approval is still running, additional callers can attach with the use-existing conflict policy and all receive the same decision. Once it has completed, they cannot — so either attach before it finishes, or have the handler notify them, which is what `notifyRequester` does. ### An Activity-backed Operation that will not cancel -An Activity is not interrupted by cancellation the way a Workflow is. Without heartbeating, a heartbeat timeout, and maximum attempts of 1, a cancellation request has no effect and the Operation runs to its timeout. All three settings are needed together. See [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating). +An Activity is not interrupted by cancellation the way a Workflow is. Without heartbeating from the Activity and a heartbeat timeout, a cancellation request has no effect and the Operation runs to its timeout. Let the resulting cancellation exception propagate: a cancelled Activity is not retried, but one that swallows the cancellation and throws an ordinary failure instead is. See [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating). ### Duplicate side effects on retry @@ -74,11 +70,11 @@ Derive the Workflow Id or Activity Id from the Nexus request Id, or from the Ope ### Sending a Signal to a Workflow that may not exist -A Signal to a missing Workflow fails. Use Signal-with-Start when the target may not be running yet; it starts the Workflow if needed and delivers the Signal either way. +A Signal to a missing Workflow fails. Use Signal-with-Start when the target may not be running yet; it starts the Workflow if needed and delivers the Signal either way. Remember that its Operation input has to carry whatever the Workflow needs to start, not just the message. See [Attach information before the approval exists](/develop/java/nexus/development-walkthrough/send-messages#attach-information-before-the-approval-exists). ### More than one async backing per handler invocation -A handler can perform unlimited sync side effects but at most one async backing. Calling `startWorkflow` and `startWorkflowUpdate` in the same invocation is not a valid Operation. Compose sync side effects freely; pick one thing for the caller to await. +A handler can perform unlimited sync side effects but at most one async backing. Starting a Workflow and starting a Workflow Update in the same invocation is not a valid Operation. Compose sync side effects freely; pick one thing for the caller to await. ### Hand-editing generated code @@ -98,6 +94,8 @@ Callers and handlers deploy independently, so both sides run different contract **Use `TemporalOperationHandler` even when the Operation is trivial.** An Operation that starts synchronous can later gain an async backing or a Signal without changing shape. +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). + :::tip RESOURCES - [Nexus execution debugging](/nexus/execution-debugging) for tracing Operations across Namespaces. diff --git a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx index 1db8b169f9..44bee4c10c 100644 --- a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx +++ b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx @@ -2,7 +2,7 @@ id: define-the-data-contract title: Step 1 - Define the data contract sidebar_label: 1. Define the data contract -description: Plan an approval Nexus Service and write its data contract before any implementation, so every language shares one definition. +description: Plan a purchase approval Nexus Service and write its data contract before any implementation, so every language shares one definition. toc_max_heading_level: 4 unlisted: true tags: @@ -18,45 +18,57 @@ The contract is the only thing a caller and a handler share. Everything else — Writing the contract first is what makes the Service polyglot. -**Every sample in this walkthrough, in every language, is generated from this one contract.** A Go caller can call the Java handler built here. The Java caller built here can call a Python handler. Neither side hand-writes the request and response types, so neither side can drift from the other. +**Every sample in this walkthrough, in every language, is generated from this one contract.** A Go caller can call a Java handler. A TypeScript caller can call a Python handler. What language a side is written in has no bearing on whether the two can talk — the only thing that has to match is the contract they were both generated from. Each side picks whatever language suits it, and as long as both were built against this contract, they interoperate. -The alternative — code-first, where you expose an existing Workflow and derive the contract from its signature — ties the contract to one implementation's shape. It also gives you no way to review the API before building it. Temporal does not currently have a good path from code back to a generated contract, so the contract-first order is the one to follow. +That is why the contract comes before any implementation. It is written once, in no particular language, and every implementation in this walkthrough is generated from it. ## Plan the Operations -Work backwards from what callers need, not from what your Workflow happens to do. +As with all API design, work backwards from what callers need, not from what your Workflow happens to do. -For the approval problem, callers need to start an approval and learn the outcome, nudge a pending approval, check on progress, submit a decision, and be notified when it is final. That produces five Operations: +For the approval problem, callers need to check whether a purchase needs approval at all, start an approval and learn its outcome, nudge a pending approval, attach supporting information to a purchase, submit a decision, and be notified when the decision is final. That produces six Operations: | Operation | Input | Output | Added in | | --- | --- | --- | --- | +| `checkApprovalRequired` | Item id, requester, amount | Whether approval is needed, and the threshold applied | Step 3 | | `requestApproval` | Item id, requester, amount | `APPROVED` or `DENIED` | Step 4 | | `remindApprover` | Approval id | Nothing | Step 7 | -| `getApprovalStatus` | Approval id | Pending or decided, reminders sent | Step 7 | | `submitDecision` | Approval id, decision | Confirmation of the recorded decision | Step 7 | +| `attachApprovalContext` | Item id, requester, amount, note | Nothing | Step 8 | | `notifyRequester` | Requester, decision | Nothing | Step 9 | -Two decisions in that table are worth explaining, because they are easy to get wrong. +Three of those are worth explaining now, because they are easy to get wrong. They also introduce the three shapes an Operation can take, named here and chosen per Operation in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation): -**`requestApproval` returns the final decision.** It does not return an approval id for the caller to poll. The Operation is backed by a Workflow, so the Operation completes when that Workflow returns, and the Workflow's return value *is* the Operation's result. The caller awaits the Operation and receives `APPROVED` or `DENIED`. +**`checkApprovalRequired` answers a question without starting anything.** A small purchase may not need approval, and finding that out should not create an approval, a Workflow, or any durable record. This is the synchronous case: the Operation applies a spend threshold and returns the answer during the call, so a caller can skip the rest of this Service entirely. -**`getApprovalStatus` reports in-flight progress only.** It is tempting to use it to fetch the final decision too, but that is the wrong tool. The decision already arrives as the result of `requestApproval`. A Query is served by replaying history in a Worker, which means the Workflow code must still be deployed and replay-compatible, and it stops working once the [Retention Period](/temporal-service/temporal-server#retention-period) expires. Use the Operation result for the outcome, and the Query for what is happening while the approval is still open. +**`requestApproval` returns the final decision.** It does not return an approval id for the caller to poll. The Operation is Workflow-backed, so it completes when that Workflow returns, and the Workflow's return value *is* the Operation's result. The caller awaits the Operation and receives `APPROVED` or `DENIED`. -## Shape the types +**`attachApprovalContext` does not require the approval to exist.** Supporting information — a justification, a link to a quote, a manager's note — is produced by a different system than the one requesting approval, and the two messages can arrive in either order. The Operation is written so that either order works, which means both might have to start the approval workflow. Since this message might have to start the workflow, its input needs to include the purchase details so that the workflow has enough information to start. [Step 8](/develop/java/nexus/development-walkthrough/send-messages#attach-information-before-the-approval-exists) covers this in detail. -Two constraints apply when writing the contract. +## Contract design rules -An Operation's input and output are each optional, but when present each must be an **object type**. A bare string works today and then cannot grow a field tomorrow without breaking the wire format. `remindApprover` returns nothing at all, which is fine. +An Operation's input and output are each optional, but when present each must be an **object type**. If the input is only a single variable a class wrapping that is still required. This allows you to add a field later without breaking the wire format. Conversely, though, returning nothing at all is fine, which `remindApprover` does. -Keep the types **forward-compatible**. Callers and handlers deploy independently and will run different versions of the contract at the same time. Adding an optional field is safe; making an existing field required, or removing one, is not. +Keep the types **forward-compatible** if you change the contract. Callers and handlers deploy independently and will run different versions of the contract at the same time. Adding an optional field is safe; making an existing field required, or removing one, is not. -Types are modeled with JSON Schema 2020-12. See [Definition files](/nexus/client-code-generator#definition-files) for the two file flavors and the supported subset. +## Write the contract -`{sample code will be here}` +Contracts are modeled with JSON Schema 2020-12. Each definition file is one of two kinds, decided by what sits at its root: + +- **Nexus document** - the root carries a `nexusrpc: '1.0.0'` marker and acts as an envelope, with Services and their Operations at the top level and types under `$defs`. Only this kind can declare a Service. +- **Pure JSON Schema** - the root is itself a type, with reusable types under `$defs`. No Service or Operation declarations, just data models shared across languages. + +A file is one or the other, never both. A contract can span several files, with a Nexus document pulling in types from pure-schema files through `$ref`. + +The approval contract declares a Service with six Operations, so its entry file is a Nexus document. + +**[Definition files](/nexus/client-code-generator#definition-files)** documents both flavors in full, with the supported subset of JSON Schema and a worked example to model this contract on. Read it before writing the approval contract. ## Next -With the contract written, [generate code from it](/develop/java/nexus/development-walkthrough/generate-code) for the handler and the caller. +**[Step 2 - Generate code from the contract](/develop/java/nexus/development-walkthrough/generate-code)** - turn the contract into the typed code both sides use. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES diff --git a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx index 56c913ccd2..1037cfc0c6 100644 --- a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx +++ b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx @@ -2,7 +2,7 @@ id: generate-code title: Step 2 - Generate code from the contract sidebar_label: 2. Generate code -description: Use the Nexus Client Code Generator to turn the approval contract into typed models, validators, and Service definitions for the handler and the caller. +description: Use the Nexus Client Code Generator to turn the approval contract into typed models, validators, and Service definitions in every supported language. toc_max_heading_level: 4 unlisted: true tags: @@ -10,36 +10,40 @@ tags: - Java SDK --- -Generate the library code before writing any implementation. Both sides of the Service use it: the handler implements against the generated Service definition, and the caller invokes against the same one. +Generate the library code from the contract before writing any implementation. Both sides of the Service use it: the handler implements against the generated Service definition, and the caller invokes against the same one. ## What generation produces -For each type in the contract, the [Nexus Client Code Generator](/nexus/client-code-generator) emits a typed model, a runtime validator, and — for a contract that declares Services — a Nexus Service definition. +For each type in the contract, the [Nexus Client Code Generator](/nexus/client-code-generator) emits a typed model and a runtime validator. If the contract declares Services, it will also create a Nexus Service definition. -In Java the Service definition is an interface annotated with `@Service`, carrying one `@Operation` method per Operation. That interface is used on both sides and in two different ways: +The generated Service definition carries one member per Operation, in whatever form is idiomatic for the language. It is used on both sides and in two different ways: - The **handler** provides an implementation for it, which the Worker registers. -- The **caller** uses the interface directly as a Workflow stub, so calls are type-checked against the contract. +- The **caller** uses it to invoke Operations, so calls are type-checked against the contract. + +`nexgen` generates Go, Java, Python, and TypeScript, which is the set of languages this walkthrough covers. The generated validators run when a payload is parsed and again when it is serialized, so a request that violates the contract is rejected at the boundary rather than reaching your Workflow. Violations aggregate into one error naming every field that failed, which a handler maps to `BAD_REQUEST`. -## Generate for Java +## Generate the code -Java generation requires a package name whose last segment matches the output directory name. See [Generate code](/nexus/client-code-generator#generate-code) for the full command shape and the per-language flags. +Generation is one command per language, with a few per-language flags — Java, for instance, requires a package name whose last segment matches the output directory name. -`{sample code will be here}` +**[Generate code](/nexus/client-code-generator#generate-code)** has the full command shape and the flags each language takes, with examples for each. -Commit the generated code, and regenerate whenever the contract changes. Do not hand-edit it — the files are marked as generated, and your edits are lost on the next run. If a generated name is wrong for Java, fix it in the contract with a per-language naming override rather than editing the output. See the [Nexus Client Code Generator](/nexus/client-code-generator). +Commit the generated code, and regenerate whenever the contract changes. Do not hand-edit it — the files are marked as generated, and your edits are lost on the next run. When a generated name is wrong for your language, fix it in the contract with a per-language naming override rather than editing the output. See the **[Nexus Client Code Generator](/nexus/client-code-generator)** for more details. -## Generate for other languages +## One contract, four languages -The same contract produces a caller in any supported language. Generating a Go, Python, or TypeScript client from this contract is one command each, and the result talks to the Java handler built in this walkthrough without any coordination beyond the contract. +Run the generator once per language and you have that language's contract code — typed models, validators, and the Service definition. That is not a working handler or caller; you still write those. It is the part both sides have to agree on, generated from one source instead of hand-written twice, and the same generated code serves whichever side you are building. Nothing about a handler needs to know which languages its callers use, and nothing about a caller needs to know which language implements the handler. -This is the step where the contract-first ordering pays off. Nothing about the handler needs to know which languages its callers use. +This is the step where the contract-first ordering pays off, and it is what makes the cross-language call in [step 6](/develop/java/nexus/development-walkthrough/call-the-service) work without any coordination beyond the contract. ## Next -With the types in hand, [choose what backs each Operation](/develop/java/nexus/development-walkthrough/choose-backing-implementation). +**[Step 3 - Choose the backing implementation](/develop/java/nexus/development-walkthrough/choose-backing-implementation)** - decide what runs behind each Operation, and build the one that needs nothing. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx index 58493047af..817cc2834e 100644 --- a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -2,7 +2,7 @@ id: implement-the-service title: Step 4 - Implement the Service sidebar_label: 4. Implement the Service -description: Implement the approval Nexus Service in Java using TemporalOperationHandler, backed by a Workflow, and run a Worker that hosts it. +description: Implement the approval Nexus Service with TemporalOperationHandler, backed by a Workflow, and run a Worker that hosts it. toc_max_heading_level: 4 unlisted: true tags: @@ -10,11 +10,11 @@ tags: - Java SDK --- -Implement the generated Service interface, back `requestApproval` with the approval Workflow, and run a Worker that hosts both. +[Step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#build-the-operation-that-needs-no-backing) implemented the one Operation that needs no backing. This step adds the one at the center of the Service: back `requestApproval` with the approval Workflow, then run a Worker that hosts the Service, the Workflow, and the Activities. ## Write the approval Workflow -The Workflow is an ordinary Temporal Workflow. Nothing in it is Nexus-specific, and it could be started directly by a Client instead. +The Workflow is an ordinary Temporal Workflow — the interactive one chosen in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#workflow). Nothing in it is Nexus-specific, and it could be started directly by a Client instead. What makes it interactive is the message handlers added in [step 7](/develop/java/nexus/development-walkthrough/add-messaging) and a Workflow Id you can predict. For the approval, it needs to: @@ -33,17 +33,13 @@ Use `TemporalOperationHandler` for every Temporal-backed Operation, including si `TemporalOperationHandler.create(...)` gives your start handler a context, a Nexus-aware Client, and the Operation input. Call `startWorkflow` on that Client and return its result. The Operation then completes when the Workflow returns, delivering the Workflow's return value to the caller. -The Client is not an ordinary Temporal Client. It propagates bidirectional links and request Ids automatically, so the caller's Execution and the approval Workflow are connected in the UI. Fetching your own Client inside a handler works but gives up that linking. +The Client is not an ordinary Temporal Client. It propagates bidirectional links and request Ids automatically, so the caller's Execution and the approval Workflow are connected in the UI without you wiring anything. Fetching your own Client inside a handler works, and the Operation behaves correctly, but you give up that linking. This is the single biggest reason to use the injected Client for anything that starts or messages an Execution. `{sample code will be here}` -Set the Workflow Id from the approval id, as decided in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#give-the-entity-a-stable-id). +Set the Workflow Id from the item id, as decided in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#give-the-approval-workflow-a-stable-id). -:::note - -You may see older examples using `WorkflowRunOperation.fromWorkflowMethod` or a synchronous `OperationHandler`. Both still work and are not being removed, but they are de-emphasized in favor of `TemporalOperationHandler`. See [Coming from the earlier handler APIs](/nexus/temporal-operation-handler#coming-from-the-earlier-handler-apis). - -::: +By default, starting a Workflow whose Id is already running **fails the Operation**. That is the right default here: the Operation has only started successfully once its completion callback is attached to a Workflow, so failing loudly beats reporting success to a caller that would then wait forever. [Step 8](/develop/java/nexus/development-walkthrough/send-messages#when-the-approval-already-exists) revisits this option, because once another Operation can create the approval first, this Operation needs to attach to it instead of failing. ## Run the Worker @@ -63,7 +59,11 @@ An **application failure** — the approval cannot proceed for a business reason ## Next -The Service runs but nothing can reach it yet. [Publish it in Nexus](/develop/java/nexus/development-walkthrough/publish-in-nexus). +The Service runs but nothing can reach it yet. + +**[Step 5 - Publish in Nexus](/develop/java/nexus/development-walkthrough/publish-in-nexus)** - create the Endpoint that makes it reachable. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES diff --git a/docs/develop/java/nexus/development-walkthrough/index.mdx b/docs/develop/java/nexus/development-walkthrough/index.mdx index 5bbe81c67f..cbb171b0e1 100644 --- a/docs/develop/java/nexus/development-walkthrough/index.mdx +++ b/docs/develop/java/nexus/development-walkthrough/index.mdx @@ -1,8 +1,8 @@ --- id: index -title: Nexus Development Walkthrough - Java SDK -sidebar_label: Development Walkthrough -description: Build a Nexus Service end to end in Java, starting from a data contract and adding one Nexus capability at a time to solve an approval problem. +title: Nexus Microservice Development Walkthrough - Java SDK +sidebar_label: Microservice Development Walkthrough +description: Build a Nexus Service end to end in Java, starting from a data contract and adding one Nexus capability at a time to solve a purchase approval problem. toc_max_heading_level: 4 unlisted: true tags: @@ -26,48 +26,68 @@ This walkthrough builds one Nexus Service from nothing to a complete API, adding A [Nexus Service](/evaluate/nexus) is a contract that one team publishes and other teams call, across [Namespace](/namespaces) boundaries, without sharing code or a deployment. -## A sample problem +## The walkthrough problem -A purchase request needs approval before it can proceed. +This guide is easier to follow grounded in a real problem. So this walkthrough builds a Service that solves a common problem, a purchase approval workflow. In the process of building that Service, the capabilities of Nexus can be fully demonstrated. -Approval is slow and human-driven: someone has to look at the request and decide. The system needs to survive that wait, which may be minutes or weeks. While a request is pending, other systems need to nudge the approver and check on progress. Eventually a decision arrives, and the requesting system needs the outcome. +**A purchase request needs approval before it can proceed.** + +Approval is slow and human-driven: someone has to look at the request and decide. The system needs to survive that wait, which may be minutes or weeks. While a request is pending, other systems might need to nudge the approver or attach information to the request. Eventually a decision arrives, and the requesting system needs the outcome. + +Human-in-the-loop approval is a good reference problem because it is a scenario customers frequently use Temporal and Nexus to solve. Concretely, the Service needs to: +- Tell a caller whether a purchase needs approval at all, before any durable work starts - Start an approval and, eventually, return `APPROVED` or `DENIED` - Accept a nudge that asks the approver again, and count how many have been sent -- Report progress while the approval is still pending +- Accept supporting information for a purchase, whether or not its approval exists yet - Accept a decision from the caller and confirm it was recorded - Send a notification when the decision is final -Each of those maps onto a different Nexus capability, which is what makes it a useful walkthrough. By the end, this sample service will exercise a Workflow-backed Operation, a Signal, a Query, an Update, and an Activity-backed Operation. +Each of those needs a different Nexus capability which will be introduced and demonstrated. ## One contract, every language The walkthrough begins with the data contract, before any implementation, and that ordering is the point. -**The equivalent sample for each language is written against the same contract.** Because the contract is the only thing the two sides share, any caller can call any handler: the Go sample walkthrough caller can drive this Java handler for example, and the Java caller here can drive the handler from each other language's Nexus Development Walkthrough. Handler and caller do not need to agree on a language, only on the contract. +**This walkthrough builds the Service in Java.** The same contract has a sample implementation in every language the generator supports, and the reasoning at each step — what the contract should say, what backs each Operation, which message type to reach for — is the same in all of them. + +Working sample code, all built from the one contract: + +| Language | Sample | +| --- | --- | +| Java (this walkthrough) | `{sample repo link}` | +| Go | `{sample repo link}` | +| Python | `{sample repo link}` | +| TypeScript | `{sample repo link}` | + +**Any caller can call any handler, because the contract is the only thing the two sides share.** A Go caller can drive the Python handler; the TypeScript caller can drive the Java handler. Handler and caller do not need to agree on a language, only on the contract. [Step 6](/develop/java/nexus/development-walkthrough/call-the-service) builds the Java caller and then points at the other languages' samples, which call the Java handler built here without changes on either side. :::note -The idea is that we write a sample repo for each language that implements this project. Then we should be able to run the client from any sample project against the handler from any sample project. +The idea is that we write a sample repo for each language that implements this project. Then we should be able to run the client from any sample project against the handler from any sample project. The code will come as soon as the docs are (mostly) done, I didn't want to have to keep rewriting the code to match changing docs. + +This set of docs is for Java, one question is where this will live - if it's in the develop section, then we should have a matching set of docs for each language. If it is in the Encyclopedia, then we can have a single doc with code tabs for the sample code in each language. ::: The [Nexus Client Code Generator](/nexus/client-code-generator) makes this easy. It takes the contract and emits typed models, runtime validators, and Service definitions for Go, Java, Python, and TypeScript, so neither side hand-writes the types and neither side can drift from the contract. ## Steps -1. [Define the data contract](/develop/java/nexus/development-walkthrough/define-the-data-contract) -2. [Generate code from the contract](/develop/java/nexus/development-walkthrough/generate-code) -3. [Choose the backing implementation](/develop/java/nexus/development-walkthrough/choose-backing-implementation) -4. [Implement the Service](/develop/java/nexus/development-walkthrough/implement-the-service) -5. [Publish in Nexus](/develop/java/nexus/development-walkthrough/publish-in-nexus) -6. [Call the Service](/develop/java/nexus/development-walkthrough/call-the-service) -7. [Add messaging](/develop/java/nexus/development-walkthrough/add-messaging) -8. [Send messages](/develop/java/nexus/development-walkthrough/send-messages) -9. [Add a Standalone Activity](/develop/java/nexus/development-walkthrough/add-a-standalone-activity) -10. [Call the Standalone Activity](/develop/java/nexus/development-walkthrough/call-the-standalone-activity) +Each step adds one capability to the approval Service. + +1. [Define the data contract](/develop/java/nexus/development-walkthrough/define-the-data-contract) - plan the Operations callers need and write them down +2. [Generate code from the contract](/develop/java/nexus/development-walkthrough/generate-code) - produce typed models and Service definitions for both sides +3. [Choose the backing implementation](/develop/java/nexus/development-walkthrough/choose-backing-implementation) - decide what runs behind each Operation, and build the one that needs nothing +4. [Implement the Service](/develop/java/nexus/development-walkthrough/implement-the-service) - back the approval with a Workflow +5. [Publish in Nexus](/develop/java/nexus/development-walkthrough/publish-in-nexus) - make the Service reachable from another Namespace +6. [Call the Service](/develop/java/nexus/development-walkthrough/call-the-service) - request an approval from a caller Workflow, and from the other languages' samples +7. [Add messaging](/develop/java/nexus/development-walkthrough/add-messaging) - nudge a pending approval and submit its decision +8. [Send messages](/develop/java/nexus/development-walkthrough/send-messages) - call those Operations, and attach information to an approval that may not exist yet +9. [Add a Standalone Activity](/develop/java/nexus/development-walkthrough/add-a-standalone-activity) - notify the requester with no Workflow behind it +10. [Call the Standalone Activity](/develop/java/nexus/development-walkthrough/call-the-standalone-activity) - complete the flow end to end Then: [Debugging, common pitfalls, and tips](/develop/java/nexus/development-walkthrough/debugging-and-tips). @@ -75,4 +95,12 @@ Then: [Debugging, common pitfalls, and tips](/develop/java/nexus/development-wal You need two Namespaces, one for the handler and one for the caller, so the walkthrough crosses a real Namespace boundary. A [local development server](/develop/run-a-development-server) with two Namespaces is enough for steps 1 through 4; step 5 covers both the development server and Temporal Cloud. -If you have not used Nexus before, read [Nexus Services](/nexus/services) and [Nexus Operations](/nexus/operations) first, or work through the shorter [Nexus quickstart](/develop/java/nexus/quickstart). +If you do not already have Namespaces you want to work in, create them: + +`{sample code will be here}` + +If you have not used Nexus before, read [Nexus Services](/nexus/services) and [Nexus Operations](/nexus/operations) first, or work through the shorter [Java Nexus quickstart](/develop/java/nexus/quickstart). + +## Start + +**[Step 1 - Define the data contract](/develop/java/nexus/development-walkthrough/define-the-data-contract)** - plan the Operations callers need and write them down. diff --git a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx index 4bd4a51878..612f5840a9 100644 --- a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx +++ b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx @@ -28,11 +28,17 @@ Endpoint names are unique within the Registry. In Temporal Cloud the Registry is ## Allow caller Namespaces -This is the step people miss, because the failure looks like a routing problem rather than a permissions one. +:::note This section applies to Temporal Cloud only + +Endpoint caller authorization is a Temporal Cloud feature. If you are working through this walkthrough against a development server or a self-hosted Temporal Service on your own machine, skip to [Verify it is reachable](#verify-it-is-reachable) — there is no allowed-caller list to configure. + +::: + +In Temporal Cloud this is the step people miss, because the failure looks like a routing problem rather than a permissions one. An Endpoint **rejects callers that are not on its allowed list**. Creating the Endpoint is not enough — you have to name the Namespaces permitted to call it. The caller Namespace in this walkthrough is separate from the handler Namespace, so it has to be added explicitly. -In Temporal Cloud, set the allowed caller Namespaces when you create or edit the Endpoint in the UI, or with `tcld`. Add the caller Namespace, including its Account suffix. +Set the allowed caller Namespaces when you create or edit the Endpoint in the UI, or with `tcld`. Add the caller Namespace, including its Account suffix. If a call fails as unauthorized and the Endpoint clearly exists, check this list first. @@ -52,7 +58,9 @@ A Worker that is not polling is the other common cause of a call that appears to ## Next -[Call the Service](/develop/java/nexus/development-walkthrough/call-the-service) from the caller Namespace. +**[Step 6 - Call the Service](/develop/java/nexus/development-walkthrough/call-the-service)** - request an approval from the caller Namespace. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx index 93b3c3f0d8..11c825ba7a 100644 --- a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -2,7 +2,7 @@ id: send-messages title: Step 8 - Send messages sidebar_label: 8. Send messages -description: Call the Signal, Query, and Update Operations from a caller Workflow, and start an approval with Signal-with-Start. +description: Call the Signal and Update Operations from a caller, attach information to an approval that may not exist yet with Signal-with-Start, and handle the case where the approval already exists. toc_max_heading_level: 4 unlisted: true tags: @@ -10,11 +10,11 @@ tags: - Java SDK --- -From the caller's side, the three messaging Operations are just Operations. They are called through the same generated Service stub as `requestApproval`, with the same type checking. +From the caller's side, the messaging Operations are just Operations. They are called through the same generated stub as `requestApproval`, with the same type checking. -The caller does not know that one is a Signal, one is a Query, and one is an Update. That is the handler's implementation detail, and it can change without breaking callers. +The caller does not know that one is a Signal and one is an Update. That is the handler's implementation detail, and it can change without breaking callers. -## Nudge, check, and decide +## Nudge and decide `{sample code will be here}` @@ -22,44 +22,75 @@ What differs between them is what you get back and how long it takes. `remindApprover` returns nothing and completes as soon as the Signal is accepted. Accepted is not the same as handled — the Signal is durably recorded and the Workflow will process it, but the Operation does not wait for that. If the caller needs confirmation that the nudge took effect, it needs an Update, not a Signal. -`getApprovalStatus` returns the current progress immediately. Call it when something needs to report on a pending approval; do not call it in a loop waiting for the decision. Note that this Operation is [not implementable in the pre-release](/develop/java/nexus/development-walkthrough/add-messaging#expose-them-as-nexus-operations), because Query is not yet available as sync messaging. - `submitDecision` returns confirmation that the decision was recorded. This is the point of using an Update: the caller learns the outcome of its own message. Once it succeeds, the approval Workflow unblocks and completes, which resolves the `requestApproval` Operation that the original caller is still awaiting. -## Start and Signal in one call +Both require the approval to already be running. A nudge or a decision for a purchase nobody has requested approval for has nothing to reach, and the Operation fails. + +## Attach information before the approval exists + +The next Operation does not have that requirement, and the reason is worth the detail. -Sometimes a caller wants to start an approval and immediately attach information to it, without a race between the two calls. +Supporting information for a purchase — a justification, a link to a quote, a manager's note — comes from a different system than the one requesting approval. Those two systems run independently, so their messages arrive in whatever order the network and their schedules produce. Sometimes the context arrives first. -Signal-with-Start does both atomically: if the target Workflow is not running it is started, and either way the Signal is delivered. It is a sync side effect on the Nexus-aware Client, reached through `client.getWorkflowClient()`, so a handler Operation can offer it directly. +A plain Signal cannot handle that. Sending one to a Workflow that does not exist fails, and "fail if the approval has not been requested yet" is the wrong behavior for a message whose whole job is to be available whenever it shows up. -This is also how you make a Signal safe to send to an approval that may not exist yet. A plain Signal to a missing Workflow fails; Signal-with-Start creates it. +`attachApprovalContext` uses **Signal-with-Start** instead. If the approval is already running, the note is delivered to it. If it is not, the approval is started and then the note is delivered. Either order works, and the caller does not have to know which happened. `{sample code will be here}` -:::caution Update-with-Start is not yet available +Signal-with-Start is sync messaging on the [Client](/nexus/temporal-operation-handler#the-nexus-aware-client), so the Operation completes during the handler call and returns nothing. The caller gets no confirmation that a human read the note, only that it was durably attached. -Update-with-Start — starting a Workflow and running an Update against it atomically — is not available in any SDK yet. See [Temporal Operation Handler](/nexus/temporal-operation-handler). +### Input needs enough to start the Workflow -Until it lands, an Operation that needs both must start the Workflow and then submit the Update as a separate call, which is not atomic. Signal-with-Start is available and covers the case where the caller does not need a response. +Look at the contract from [step 1](/develop/java/nexus/development-walkthrough/define-the-data-contract#plan-the-operations) and `attachApprovalContext` carries more than it seems to need: the item id, the requester, the amount, *and* the note. `remindApprover` gets by with just an approval id. -::: +That is a direct consequence of Signal-with-Start. The Operation might have to start the approval Workflow, and starting it requires whatever the Workflow needs to run. An Operation that can create the thing it messages has to carry enough input to create it. + +This is the general rule for any with-Start message: its input is the union of what the message needs and what the Workflow's start needs. + +## When the approval already exists + +Signal-with-Start introduces a case the Service did not have before. `attachApprovalContext` can create the approval, so by the time anyone calls `requestApproval` for that purchase, a Workflow with that Id may already be running. + +Both Operations derive the same Workflow Id from the same item id, as decided in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#give-the-approval-workflow-a-stable-id). That is deliberate — it is what lets them agree on which approval they mean — and it is also what creates the collision. + +**By default, `requestApproval` fails in this situation.** Starting a Workflow whose Id is already running is an error, and the Nexus Operation fails with it. + +That default is not arbitrary strictness. A Workflow-backed Operation has only started successfully once its completion callback is attached to a Workflow. If the start quietly did nothing on a conflict, the Operation would report success with no callback attached, and the caller would wait for a decision that could never be delivered. Failing immediately is better than hanging forever. -## You cannot re-attach to get a result +The fix is to change the Workflow Id conflict policy on `requestApproval` from its default to **use-existing**. With that set, a start against an already-running approval attaches the Operation's completion callback to that Execution instead of failing. The caller then awaits the approval that is already in flight and receives its decision when it completes. -A caller that did not start an approval cannot ask Nexus for its final decision. +`{sample code will be here}` + +Two things follow from this, and both are useful. + +**More than one caller can await the same approval.** Every caller whose callback is attached is notified when the Workflow completes, so several systems can each call `requestApproval` for the same purchase and all receive the same decision. The first call creates the approval; the rest attach to it. + +**The Operation becomes idempotent for callers, not just for retries.** [Step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#give-the-approval-workflow-a-stable-id) made the start idempotent against server retries of one request. Use-existing extends that to genuinely separate callers, which is what you want for a purchase that two systems might both submit. + +One limit to know: use-existing attaches to a *running* Execution. If the approval has already completed, there is nothing to attach to, and the call starts a fresh approval rather than returning the old decision. Whoever needs the outcome of a finished approval has to have been attached while it was open, or be told by the handler — which is what the notification in the next step does. -`requestApproval` delivers the decision to whoever awaited it. There is no Operation that attaches to an already-running approval and waits for its result, because Get Workflow Result as an async backing is [not yet available](/nexus/temporal-operation-handler) in any SDK. +:::note The two Workflow Id policies -If several systems need the outcome, either have the caller that started the approval distribute it, or have the handler notify them — which is what the notification in the next step does. +Two policies govern a start against a Workflow Id already in use, and they cover cases that never overlap: + +- The [Workflow Id conflict policy](/workflow-execution/workflowid-runid#workflow-id-conflict-policy) applies while a Workflow with that Id is **running**. It defaults to failing with `Workflow execution already started` — the default this section replaces with use-existing. +- The [Workflow Id reuse policy](/workflow-execution/workflowid-runid#workflow-id-reuse-policy) applies once the previous Workflow with that Id has **closed**. It defaults to Allow Duplicate, which permits a new Execution. + +Setting use-existing changes only the running case. A start against a completed approval falls to the reuse policy and opens a new one. Set the reuse policy as well if a second approval for the same item is not what you want. + +::: ## Next -[Add a Standalone Activity](/develop/java/nexus/development-walkthrough/add-a-standalone-activity) to notify the requester once a decision is final. +**[Step 9 - Add a Standalone Activity](/develop/java/nexus/development-walkthrough/add-a-standalone-activity)** - notify the requester with no Workflow behind it. + +Back to the [Microservice Development Walkthrough overview](/develop/java/nexus/development-walkthrough). :::tip RESOURCES -- [Sending messages](/sending-messages) for Signal, Query, and Update semantics. -- [Temporal Operation Handler](/nexus/temporal-operation-handler) for which capabilities are available per SDK. +- [Sending messages](/sending-messages) for Signal and Update semantics. +- [Temporal Operation Handler](/nexus/temporal-operation-handler) for sync messaging and async backings. - [Java Nexus feature guide](/develop/java/nexus/feature-guide) for the caller API. ::: diff --git a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx index 9bc5c88300..84f6d472cf 100644 --- a/docs/encyclopedia/nexus/nexus-client-code-generator.mdx +++ b/docs/encyclopedia/nexus/nexus-client-code-generator.mdx @@ -49,13 +49,19 @@ The generator prefers to fail loudly at generation time over emitting code that ## Definition files Types are modeled with [JSON Schema 2020-12](https://json-schema.org/draft/2020-12). -A definition file comes in two flavors. +A definition file is one of two kinds, decided by what sits at its root. +A file is one or the other, never both. **Pure JSON Schema.** The root of the document is itself a type, and reusable types live under `$defs`. Use this when you only need data models shared across languages, with no Service or Operation declarations. **Nexus document.** Add a root `nexusrpc: "1.0.0"` marker to enable a `services` section. The root becomes an envelope: Services and their Operations sit at the top level, and your types live under `$defs`. +Only this kind can declare a Service. + +The two kinds compose across files, so a contract is not limited to one of them. +A Service contract is often a Nexus document declaring the Services and Operations, plus pure JSON Schema files holding the types it `$ref`s by relative path. +The [`kb/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb) closure described below is built that way. The examples on this page use [`samples/schemas/chat.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/chat.nexusrpc.yaml) from the repository, abbreviated here: diff --git a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx index 66e16d9030..1c0048caa2 100644 --- a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx +++ b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx @@ -446,6 +446,6 @@ Sample code: `{code not yet live}` - [Temporal Operation Handler](/nexus/temporal-operation-handler) for the handler type and the Nexus-aware Client. - [Standalone Activity](/standalone-activity) for the underlying concept, and [Java: Standalone Activities](/develop/java/activities/standalone-activities) for the SDK API. - [Standalone Nexus Operation](/standalone-nexus-operation) for starting Operations without a caller Workflow. -- [Development Walkthrough](/develop/java/nexus/development-walkthrough) uses an Activity-backed Operation in context. +- [Microservice Development Walkthrough](/develop/java/nexus/development-walkthrough) uses an Activity-backed Operation in context. ::: diff --git a/docs/encyclopedia/nexus/nexus.mdx b/docs/encyclopedia/nexus/nexus.mdx index 38781924ea..7883bf085d 100644 --- a/docs/encyclopedia/nexus/nexus.mdx +++ b/docs/encyclopedia/nexus/nexus.mdx @@ -139,5 +139,5 @@ The APIs are experimental, so expect them to change. - [Temporal Operation Handler](/nexus/temporal-operation-handler) - Implement Operations with a single handler type, and get bidirectional linking across the Namespace boundary. - [Nexus Client Code Generator](/nexus/client-code-generator) - Generate typed models, validators, and Service definitions for Go, Java, Python, and TypeScript from one schema. - [Nexus Standalone Activity](/nexus/standalone-activity) - Back an Operation with a single durable step instead of a Workflow. -- [Development Walkthrough (Java)](/develop/java/nexus/development-walkthrough) - Build a Nexus Service end to end, adding one capability at a time. This walkthrough is not in the navigation yet, so this link is its entry point. +- [Microservice Development Walkthrough](/develop/java/nexus/development-walkthrough) - Build a Nexus Service end to end, adding one capability at a time. This walkthrough is not in the navigation yet, so this link is its entry point. diff --git a/docs/encyclopedia/nexus/temporal-operation-handler.mdx b/docs/encyclopedia/nexus/temporal-operation-handler.mdx index dda4fd3586..34e48514e2 100644 --- a/docs/encyclopedia/nexus/temporal-operation-handler.mdx +++ b/docs/encyclopedia/nexus/temporal-operation-handler.mdx @@ -630,7 +630,7 @@ Replace it with [Send a Signal from an Operation](#send-a-signal-from-an-operati - [Nexus Client Code Generator](/nexus/client-code-generator) to generate Service contracts and typed models from one schema. - [Nexus Standalone Activity](/nexus/standalone-activity) for Activity-backed Operations. - [Bidirectional linking](/nexus/execution-debugging#bi-directional-linking) for what the Nexus-aware Client gives you. -- [Development Walkthrough](/develop/java/nexus/development-walkthrough) builds a Nexus Service end to end with the Temporal Operation Handler. +- [Microservice Development Walkthrough](/develop/java/nexus/development-walkthrough) builds a Nexus Service end to end with the Temporal Operation Handler. - Nexus feature guides: [Go](/develop/go/nexus/feature-guide) | [Java](/develop/java/nexus/feature-guide) | From 02ebd7e7990f68e8d8c5c4925a6cf1678ecb6b81 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Fri, 21 Aug 2026 16:16:16 -0700 Subject: [PATCH 13/16] Update docs/encyclopedia/nexus/nexus.mdx Co-authored-by: Quinn Klassen --- docs/encyclopedia/nexus/nexus.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/encyclopedia/nexus/nexus.mdx b/docs/encyclopedia/nexus/nexus.mdx index 7883bf085d..50963ea81d 100644 --- a/docs/encyclopedia/nexus/nexus.mdx +++ b/docs/encyclopedia/nexus/nexus.mdx @@ -132,7 +132,7 @@ Each step is a separate, durable Operation with its own retries and failure hand ## Pre-release: the new Nexus developer experience -A new way of building Nexus Services is in pre-release. It combines a contract-first workflow, where one schema generates typed models and Service definitions for every language, with a single handler type that can back an Operation with a Workflow, an Update, or an Activity. +A new way of building Nexus Services is in pre-release. It combines a contract-first approach, where one schema generates typed models and Service definitions for every language, with a single handler type that can back an Operation with a Workflow, an Update, or an Activity. The APIs are experimental, so expect them to change. From 5cd245ad1c6d83a66b03360cb0b046e4e679c7af Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Fri, 21 Aug 2026 16:16:28 -0700 Subject: [PATCH 14/16] Update docs/encyclopedia/nexus/temporal-operation-handler.mdx Co-authored-by: Quinn Klassen --- docs/encyclopedia/nexus/temporal-operation-handler.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/encyclopedia/nexus/temporal-operation-handler.mdx b/docs/encyclopedia/nexus/temporal-operation-handler.mdx index 34e48514e2..803a3e4fc2 100644 --- a/docs/encyclopedia/nexus/temporal-operation-handler.mdx +++ b/docs/encyclopedia/nexus/temporal-operation-handler.mdx @@ -37,7 +37,7 @@ Whichever you choose, the handler is the same shape, and every Execution it touc **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 pick up a Signal, without changing handler type or breaking its contract. +**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 From 1a5acd9291d27c0354cf63689faa83493147f67e Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Fri, 21 Aug 2026 16:47:25 -0700 Subject: [PATCH 15/16] Responding to PR comments --- .../add-a-standalone-activity.mdx | 7 +- .../debugging-and-tips.mdx | 2 +- .../nexus/nexus-standalone-activity.mdx | 92 ++----------------- .../nexus/temporal-operation-handler.mdx | 76 ++++++++------- 4 files changed, 54 insertions(+), 123 deletions(-) diff --git a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx index a7f8085670..3bd95877c7 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx @@ -32,10 +32,9 @@ This is the right shape whenever an Operation is one durable step behind a team ### Options an Activity-backed Operation requires -`StartActivityOptions` needs two things that a Workflow-called Activity does not, because there is no parent Workflow to supply them: +`StartActivityOptions` needs an **Activity Id**, unique within the Namespace, which a Workflow-called Activity does not, because there is no parent Workflow to scope it. -- **An Activity Id**, unique within the Namespace. -- **A Task Queue.** It does not have to be the Endpoint's target Task Queue, so notifications can run on their own Worker fleet. +The Task Queue is optional and defaults to the one the Operation is running on. Set it explicitly to run notifications on their own Worker fleet rather than the one the Endpoint targets. **To keep server retries from sending a second email, derive the Activity Id from the Nexus request Id.** The server retries Nexus start requests, and the request Id travels with them, so every retry lands on the same Activity Id and the notification goes out once. Without that, a retried request is a duplicate message to a real person. @@ -53,7 +52,7 @@ Add the Activity implementation to the same Worker that hosts the Nexus Service. This notification finishes immediately, so cancellation never comes up for it. It does come up for any longer Activity-backed Operation, and the behavior differs from a Workflow-backed one: an Activity is not interrupted by a cancellation request, so an Activity that never heartbeats runs to completion or to its timeout no matter how many cancellations arrive. -If you write a long-running Activity-backed Operation, read [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating) before you ship it. +If you write a long-running Activity-backed Operation, read [Activity cancellation](/activity-execution#cancellation) before you ship it. None of the mechanics are Nexus-specific. ## Next diff --git a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx index f9cb26f4c4..5b6df85374 100644 --- a/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx +++ b/docs/develop/java/nexus/development-walkthrough/debugging-and-tips.mdx @@ -60,7 +60,7 @@ While the approval is still running, additional callers can attach with the use- ### An Activity-backed Operation that will not cancel -An Activity is not interrupted by cancellation the way a Workflow is. Without heartbeating from the Activity and a heartbeat timeout, a cancellation request has no effect and the Operation runs to its timeout. Let the resulting cancellation exception propagate: a cancelled Activity is not retried, but one that swallows the cancellation and throws an ordinary failure instead is. See [Cancellation requires heartbeating](/nexus/standalone-activity#cancellation-requires-heartbeating). +An Activity is not interrupted by cancellation the way a Workflow is. Without heartbeating from the Activity and a heartbeat timeout, a cancellation request has no effect and the Operation runs to its timeout. Let the resulting cancellation exception propagate: a cancelled Activity is not retried, but one that swallows the cancellation and throws an ordinary failure instead is. See [Activity cancellation](/activity-execution#cancellation). ### Duplicate side effects on retry diff --git a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx index 1c0048caa2..8db95bda0b 100644 --- a/docs/encyclopedia/nexus/nexus-standalone-activity.mdx +++ b/docs/encyclopedia/nexus/nexus-standalone-activity.mdx @@ -110,7 +110,6 @@ var greetOp = temporalnexus.MustNewTemporalOperation( ) (temporalnexus.TemporalOperationResult[GreetingOutput], error) { return temporalnexus.StartActivity(ctx, nc, client.StartActivityOptions{ ID: "greet-" + opts.RequestID, - TaskQueue: TaskQueueName, StartToCloseTimeout: 10 * time.Second, }, Greet, input) }, @@ -134,7 +133,6 @@ public class GreetingNexusServiceImpl { input, StartActivityOptions.newBuilder() .setId("greet-" + context.getRequestId()) - .setTaskQueue(HandlerWorker.TASK_QUEUE_NAME) .setStartToCloseTimeout(Duration.ofSeconds(10)) .build())); } @@ -161,7 +159,6 @@ class GreetingNexusServiceHandler: activities.greet, input, id=f"greet-{ctx.request_id}", - task_queue=TASK_QUEUE_NAME, start_to_close_timeout=timedelta(seconds=10), ) ``` @@ -176,7 +173,6 @@ export const greetingServiceHandler = nexus.serviceHandler(greetingService, { return await client.typedActivity().startActivity('greet', { id: `greet-${ctx.requestId}`, args: [input], - taskQueue: TASK_QUEUE_NAME, startToCloseTimeout: '10s', }); }, @@ -201,7 +197,6 @@ public class GreetingNexusServiceHandler new() { Id = $"greet-{ctx.RequestId}", - TaskQueue = HandlerWorker.TaskQueueName, StartToCloseTimeout = TimeSpan.FromSeconds(10), }); } @@ -274,7 +269,8 @@ Starting an Activity this way needs values that a Workflow-called Activity does - **An Activity Id**, unique within the Namespace. There is no parent Workflow to scope it. - **A timeout.** At least one of start-to-close or schedule-to-close. -- **A Task Queue.** Java requires it. Go, Python, TypeScript, and .NET default it to the Task Queue the Operation is running on. Setting it explicitly lets the Activity run on its own Worker fleet rather than the one the Endpoint targets. + +The Task Queue is optional. It defaults to the Task Queue the Operation is running on, which is what the samples above rely on. Set it explicitly to run the Activity on its own Worker fleet rather than the one the Endpoint targets. Deriving the Id from the Nexus request Id makes the start idempotent. The server retries a Nexus start request using the same request Id, so each retry targets the same Activity Id rather than starting a second Activity. @@ -347,89 +343,13 @@ using var worker = new TemporalWorker( -## Cancellation requires heartbeating - -This is the biggest behavioral difference from a Workflow-backed Operation, and the easiest thing to get wrong. - -A Workflow is interrupted by a cancellation request: a blocking call throws and, if the failure propagates, the Workflow and its Operation both end as cancelled. -An Activity is not interrupted. -The server records the cancellation request, and the Worker only learns about it on the next heartbeat. - -So an Activity that never heartbeats runs until it completes or hits its start-to-close timeout, no matter how many cancellation requests the caller sends. -For a long-running Activity-backed Operation to be cancellable at all: - -- Heartbeat from the Activity, so it learns that cancellation was requested. -- Let the resulting cancellation exception propagate. A cancelled Activity is not retried, but one that swallows the cancellation and throws an ordinary failure instead is. -- Set a heartbeat timeout so the server notices a Worker that has stopped heartbeating. - - - - -```go -client.StartActivityOptions{ - ID: "greet-" + opts.RequestID, - TaskQueue: TaskQueueName, - StartToCloseTimeout: 10 * time.Minute, - HeartbeatTimeout: 5 * time.Second, -} -``` - - - +## Cancellation -```java -StartActivityOptions.newBuilder() - .setId("greet-" + context.getRequestId()) - .setTaskQueue(HandlerWorker.TASK_QUEUE_NAME) - .setStartToCloseTimeout(Duration.ofMinutes(10)) - .setHeartbeatTimeout(Duration.ofSeconds(5)) - .build(); -``` +Worth remembering, because it is the one behavioral difference from a Workflow-backed Operation that surprises people. - - - -```python -await client.start_activity( - activities.greet, - input, - id=f"greet-{ctx.request_id}", - task_queue=TASK_QUEUE_NAME, - start_to_close_timeout=timedelta(minutes=10), - heartbeat_timeout=timedelta(seconds=5), -) -``` - - - - -```typescript -await client.typedActivity().startActivity('greet', { - id: `greet-${ctx.requestId}`, - args: [input], - taskQueue: TASK_QUEUE_NAME, - startToCloseTimeout: '10m', - heartbeatTimeout: '5s', -}); -``` - - - - -```csharp -new StartActivityOptions -{ - Id = $"greet-{ctx.RequestId}", - TaskQueue = HandlerWorker.TaskQueueName, - StartToCloseTimeout = TimeSpan.FromMinutes(10), - HeartbeatTimeout = TimeSpan.FromSeconds(5), -} -``` - - - +A Workflow is interrupted by a cancellation request. An Activity is not: the Worker only learns about it on the next heartbeat, so an Activity that never heartbeats runs until it completes or hits its timeout, no matter how many cancellation requests the caller sends. -For a short Activity that finishes well inside its timeout, none of this applies. +Nothing about this is Nexus-specific. See [Activity cancellation](/activity-execution#cancellation) for how to heartbeat, what to do with the resulting cancellation exception, and why a heartbeat timeout matters. For a short Activity that finishes well inside its timeout and doesn't have the risk of hanging, however, a heartbeat is not needed. ## Choose between an Activity and a Workflow diff --git a/docs/encyclopedia/nexus/temporal-operation-handler.mdx b/docs/encyclopedia/nexus/temporal-operation-handler.mdx index 803a3e4fc2..89adf32ac5 100644 --- a/docs/encyclopedia/nexus/temporal-operation-handler.mdx +++ b/docs/encyclopedia/nexus/temporal-operation-handler.mdx @@ -143,11 +143,14 @@ const startGreeting = new temporalnexus.TemporalOperationHandler ```csharp -TemporalOperationHandler.FromHandleFactory( - async (context, client, input) => - await client.StartWorkflowAsync( - (GreetingWorkflow wf) => wf.RunAsync(input), - new() { Id = $"greeting-{input.Name}" })); +[TemporalOperation] +public Task> StartGreeting( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + GreetingInput input) => + client.StartWorkflowAsync( + (GreetingWorkflow wf) => wf.RunAsync(input), + new() { Id = $"greeting-{input.Name}" }); ``` @@ -241,12 +244,15 @@ const updateShippingAddressOp = new temporalnexus.TemporalOperationHandler< ```csharp -TemporalOperationHandler.FromHandleFactory( - async (context, client, input) => - await client.StartWorkflowUpdateAsync( - $"order-{input.OrderId}", - wf => wf.UpdateShippingAddressAsync(input), - new() { WaitForStage = WorkflowUpdateStage.Accepted })); +[TemporalOperation] +public Task> UpdateShippingAddress( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + UpdateAddressInput input) => + client.StartWorkflowUpdateAsync( + $"order-{input.OrderId}", + wf => wf.UpdateShippingAddressAsync(input), + new() { WaitForStage = WorkflowUpdateStage.Accepted }); ``` @@ -254,8 +260,7 @@ TemporalOperationHandler.FromHandleFactory( 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". Go, Java, and .NET require you to set it and fail the Operation if it is anything else. Python and TypeScript set it for you. -- **A callback URL is required.** The Operation is how the Update's result reaches the caller, so a caller that provided no callback URL gets a `BAD_REQUEST` handler error. +- **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. @@ -336,14 +341,17 @@ const cancelOrder = new temporalnexus.TemporalOperationHandler ```csharp -TemporalOperationHandler.FromHandleFactory( - async (context, client, input) => - { - await client.TemporalClient - .GetWorkflowHandle($"order-{input.OrderId}") - .SignalAsync("requestCancellation", new object?[] { input }); - return TemporalOperationResult.SyncResult(default); - }); +[TemporalOperation] +public async Task> CancelOrder( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + CancelOrderInput input) +{ + await client.TemporalClient + .GetWorkflowHandle($"order-{input.OrderId}") + .SignalAsync("requestCancellation", new object?[] { input }); + return TemporalOperationResult.SyncResult(default); +} ``` @@ -353,7 +361,7 @@ The same Client also offers Signal-with-Start, and a handler may send several me ### 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. Java also requires a Task Queue; the other SDKs default it to the Task Queue the Operation is running on. See [Nexus Standalone Activity](/nexus/standalone-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/standalone-activity). @@ -437,16 +445,18 @@ const greet = new temporalnexus.TemporalOperationHandler ```csharp -TemporalOperationHandler.FromHandleFactory( - async (context, client, input) => - await client.StartActivityAsync( - () => GreetingActivities.GreetAsync(input), - new() - { - Id = $"greet-{context.RequestId}", - TaskQueue = TaskQueueName, - StartToCloseTimeout = TimeSpan.FromSeconds(10), - })); +[TemporalOperation] +public Task> Greet( + TemporalOperationStartContext ctx, + ITemporalNexusClient client, + GreetingInput input) => + client.StartActivityAsync( + () => GreetingActivities.GreetAsync(input), + new() + { + Id = $"greet-{ctx.RequestId}", + StartToCloseTimeout = TimeSpan.FromSeconds(10), + }); ``` @@ -458,6 +468,8 @@ 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 | From 6005943f9c042d465fe9ec5cb05ca014ec825012 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Fri, 21 Aug 2026 17:05:06 -0700 Subject: [PATCH 16/16] Updated walkthrough doc with sample Java code --- .../add-a-standalone-activity.mdx | 9 ++++++--- .../development-walkthrough/add-messaging.mdx | 17 ++++++++++++++--- .../call-the-service.mdx | 9 ++++++++- .../call-the-standalone-activity.mdx | 5 +++-- .../choose-backing-implementation.mdx | 8 +++++++- .../define-the-data-contract.mdx | 13 +++++++++---- .../development-walkthrough/generate-code.mdx | 14 ++++++++++++++ .../implement-the-service.mdx | 9 ++++++--- .../nexus/development-walkthrough/index.mdx | 5 ++++- .../publish-in-nexus.mdx | 13 +++++++++++-- .../development-walkthrough/send-messages.mdx | 10 ++++++---- 11 files changed, 88 insertions(+), 24 deletions(-) diff --git a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx index 3bd95877c7..f655ac638b 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-a-standalone-activity.mdx @@ -20,7 +20,8 @@ The Activity is an ordinary Activity. In this walkthrough it is a placeholder th Nothing in it is Nexus-specific. The same Activity Function can be invoked from a Workflow and started behind this Operation with no code changes — what differs is what starts it, not how it is written. -`{sample code will be here}` + + ## Back the Operation with it @@ -28,7 +29,8 @@ Use `TemporalOperationHandler` as with every other Operation, but start an Activ This is the right shape whenever an Operation is one durable step behind a team boundary. The Activity supplies the durability — retries on the policy you set, timeouts you control, and a record of every attempt — and the Operation supplies the contract, so the notification is reachable by other teams without them sharing your code or your Namespace. -`{sample code will be here}` + + ### Options an Activity-backed Operation requires @@ -46,7 +48,8 @@ Deriving the Id from the Operation *input* instead is a different tool for a dif Add the Activity implementation to the same Worker that hosts the Nexus Service. An Activity-backed Operation needs no Workflow implementation registered for it. -`{sample code will be here}` + + ## Cancellation needs heartbeating diff --git a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx index e9c9fffad0..45ff43750c 100644 --- a/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx +++ b/docs/develop/java/nexus/development-walkthrough/add-messaging.mdx @@ -27,7 +27,8 @@ On the Workflow, add a Signal handler that increments the reminder count and an The Update is what ends the approval. It records `APPROVED` or `DENIED`, which satisfies the condition the Workflow is blocked on, and the Workflow then returns that decision as its result. -`{sample code will be here}` + + ## Expose them as Nexus Operations @@ -45,7 +46,8 @@ Sending one Signal is comfortably inside it. A handler that sends several messag ::: -`{sample code will be here}` + + ### Update @@ -53,7 +55,16 @@ Start the Update on the Client. This is an async backing: the Operation complete An Update-backed Operation carries two requirements. It targets a Workflow that already exists, so a `submitDecision` for a purchase with no approval running fails. And because it is an async backing, there is at most one per Operation invocation, though a handler can still combine it with sync side effects. -`{sample code will be here}` +### Reject a bad Update before it changes anything + +An Update can also refuse a request, which is the other thing a Signal cannot do. By the time a Signal handler runs the message has already been accepted and written to history; there is nowhere left to say no. + +The approval uses that. A **validator** runs before the handler and rejects a second decision for an approval that has already been decided — without it, the later decision would silently overwrite the earlier one. A rejected Update never runs the handler, never reaches Event History, and surfaces to the caller as a failed Operation. + +The validator is the method annotated `@UpdateValidatorMethod` in the Workflow interface above. It takes the same arguments as the handler, returns nothing, and must not change Workflow state. + + + ## Do not poll for the decision diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx index 345f3da816..58618f50c8 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-service.mdx @@ -30,7 +30,14 @@ The flow follows the walkthrough sample problem. Check whether the purchase need In Java, the generated Service interface works directly as a Nexus Service stub. Create it inside the caller Workflow with the Endpoint name and Operation options, then call its methods as if they were local. -`{sample code will be here}` + + + +The Endpoint name is not in the Workflow. It is bound once when the caller Worker registers the +Workflow, so the Workflow refers to the Service by its contract alone: + + + Nothing in this caller is aware of how the handler is built. It does not know which Task Queue the handler's Worker polls, or that `requestApproval` is backed by a Workflow while `checkApprovalRequired` is backed by nothing at all. It knows the Endpoint name and the contract. diff --git a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx index aac4021aaf..d4bb4c3500 100644 --- a/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx +++ b/docs/develop/java/nexus/development-walkthrough/call-the-standalone-activity.mdx @@ -18,7 +18,7 @@ Call `notifyRequester` once the decision is final. From the caller's side there Nothing in the caller reveals that this Operation is backed by an Activity and the other by a Workflow. That is the contract doing its job. The handler team could later replace the notification Activity with a Workflow that retries across providers and escalates on failure, and no caller would change. -`{sample code will be here}` +The call sits alongside the others in the caller Workflow from [step 6](/develop/java/nexus/development-walkthrough/call-the-service#call-the-operations-from-a-caller-workflow) — same stub, same shape, no hint of what runs behind it. ## Complete the flow @@ -31,7 +31,8 @@ With all ten steps in place, the caller runs the whole approval: 5. The approval Workflow returns the decision, which resolves the `requestApproval` Operation every attached caller has been awaiting. 6. The caller calls `notifyRequester` with the decision, backed by the notification Activity. -`{sample code will be here}` + + Every step crossed a Namespace boundary, and the caller never learned a Workflow Id, a Task Queue, or which primitive backed any Operation. One Operation ran with no durable Execution at all, one started a Workflow, two sent messages to it, one started a Workflow if it was not already running, and one started an Activity — and from the caller's side they were all just Operations. diff --git a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx index b37fd542c1..c7f8554323 100644 --- a/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx +++ b/docs/develop/java/nexus/development-walkthrough/choose-backing-implementation.mdx @@ -63,6 +63,11 @@ The approval needs a Workflow Id derived from the purchase, not a random one, so This also makes the start idempotent. A retried Nexus start request targets the same Workflow Id rather than starting a second approval for one purchase. +Deriving it in one place keeps the two Operations that need it from drifting apart: + + + + It matters again in [step 8](/develop/java/nexus/development-walkthrough/send-messages), where `attachApprovalContext` may start the approval before `requestApproval` is ever called. Both Operations derive the same Workflow Id from the same item id, which is what lets them agree on which Execution they mean. ## Build the Operation that needs no backing @@ -71,7 +76,8 @@ It matters again in [step 8](/develop/java/nexus/development-walkthrough/send-me Implement it with `TemporalOperationHandler` like every other Operation, apply the threshold, and return a synchronous result. The Operation completes during the handler call. -`{sample code will be here}` + + Using `TemporalOperationHandler` here rather than a plain synchronous handler is what lets this Operation grow later. If spend policy moves out of the handler and into a policy service, this becomes an Activity-backed Operation — and no caller changes, because the contract did not. diff --git a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx index 44bee4c10c..baddd3085a 100644 --- a/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx +++ b/docs/develop/java/nexus/development-walkthrough/define-the-data-contract.mdx @@ -32,10 +32,10 @@ For the approval problem, callers need to check whether a purchase needs approva | --- | --- | --- | --- | | `checkApprovalRequired` | Item id, requester, amount | Whether approval is needed, and the threshold applied | Step 3 | | `requestApproval` | Item id, requester, amount | `APPROVED` or `DENIED` | Step 4 | -| `remindApprover` | Approval id | Nothing | Step 7 | -| `submitDecision` | Approval id, decision | Confirmation of the recorded decision | Step 7 | +| `remindApprover` | Item id | Nothing | Step 7 | +| `submitDecision` | Item id, decision | The decision recorded, and how many reminders preceded it | Step 7 | | `attachApprovalContext` | Item id, requester, amount, note | Nothing | Step 8 | -| `notifyRequester` | Requester, decision | Nothing | Step 9 | +| `notifyRequester` | Requester, decision | Where the notification was delivered | Step 9 | Three of those are worth explaining now, because they are easy to get wrong. They also introduce the three shapes an Operation can take, named here and chosen per Operation in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation): @@ -62,7 +62,12 @@ A file is one or the other, never both. A contract can span several files, with The approval contract declares a Service with six Operations, so its entry file is a Nexus document. -**[Definition files](/nexus/client-code-generator#definition-files)** documents both flavors in full, with the supported subset of JSON Schema and a worked example to model this contract on. Read it before writing the approval contract. +**[Definition files](/nexus/client-code-generator#definition-files)** documents both flavors in full, with the supported subset of JSON Schema and a worked example to model this contract on. + +Here is the approval contract in full. Every Operation the walkthrough builds is declared here, before any implementation exists: + + + ## Next diff --git a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx index 1037cfc0c6..f003dee7bb 100644 --- a/docs/develop/java/nexus/development-walkthrough/generate-code.mdx +++ b/docs/develop/java/nexus/development-walkthrough/generate-code.mdx @@ -31,6 +31,20 @@ Generation is one command per language, with a few per-language flags — Java, **[Generate code](/nexus/client-code-generator#generate-code)** has the full command shape and the flags each language takes, with examples for each. +For the approval contract, generating the Java code is one command: + +```bash +nexgen java \ + --output core/src/main/java/io/temporal/samples/nexuswalkthrough/generatedservice \ + --package-name io.temporal.samples.nexuswalkthrough.generatedservice \ + core/src/main/java/io/temporal/samples/nexuswalkthrough/approval.nexusrpc.yaml +``` + +Two things to know. Java requires the package name's last segment to match the output directory's +name, which is why both end in `generatedservice`. And generation **clears the output directory**, +so keep the contract outside it — a schema stored in the output directory is deleted the first time +you regenerate. + Commit the generated code, and regenerate whenever the contract changes. Do not hand-edit it — the files are marked as generated, and your edits are lost on the next run. When a generated name is wrong for your language, fix it in the contract with a per-language naming override rather than editing the output. See the **[Nexus Client Code Generator](/nexus/client-code-generator)** for more details. ## One contract, four languages diff --git a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx index 817cc2834e..873d699667 100644 --- a/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx +++ b/docs/develop/java/nexus/development-walkthrough/implement-the-service.mdx @@ -25,7 +25,8 @@ For the approval, it needs to: The blocking step is the reason this is a Workflow. It may wait weeks, across Worker restarts and deployments, and the wait costs nothing while it is idle. -`{sample code will be here}` + + ## Implement the Operation with TemporalOperationHandler @@ -35,7 +36,8 @@ Use `TemporalOperationHandler` for every Temporal-backed Operation, including si The Client is not an ordinary Temporal Client. It propagates bidirectional links and request Ids automatically, so the caller's Execution and the approval Workflow are connected in the UI without you wiring anything. Fetching your own Client inside a handler works, and the Operation behaves correctly, but you give up that linking. This is the single biggest reason to use the injected Client for anything that starts or messages an Execution. -`{sample code will be here}` + + Set the Workflow Id from the item id, as decided in [step 3](/develop/java/nexus/development-walkthrough/choose-backing-implementation#give-the-approval-workflow-a-stable-id). @@ -45,7 +47,8 @@ By default, starting a Workflow whose Id is already running **fails the Operatio One Worker hosts the Nexus Service implementation, the Workflow implementation, and the Activity implementations. Its Task Queue has to match the Task Queue the Nexus Endpoint targets, which you create in the next step. -`{sample code will be here}` + + A Worker registering a Nexus Service does not need to be the same Worker that runs the backing Workflow. Splitting them is a normal choice for larger deployments — see [Nexus patterns](/nexus/patterns). diff --git a/docs/develop/java/nexus/development-walkthrough/index.mdx b/docs/develop/java/nexus/development-walkthrough/index.mdx index cbb171b0e1..19bb4846be 100644 --- a/docs/develop/java/nexus/development-walkthrough/index.mdx +++ b/docs/develop/java/nexus/development-walkthrough/index.mdx @@ -97,7 +97,10 @@ You need two Namespaces, one for the handler and one for the caller, so the walk If you do not already have Namespaces you want to work in, create them: -`{sample code will be here}` +```bash +temporal operator namespace create --namespace approval-handler-namespace +temporal operator namespace create --namespace approval-caller-namespace +``` If you have not used Nexus before, read [Nexus Services](/nexus/services) and [Nexus Operations](/nexus/operations) first, or work through the shorter [Java Nexus quickstart](/develop/java/nexus/quickstart). diff --git a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx index 612f5840a9..dd0f7058ca 100644 --- a/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx +++ b/docs/develop/java/nexus/development-walkthrough/publish-in-nexus.mdx @@ -20,7 +20,13 @@ An Endpoint needs three things: a unique name, the target Namespace where the ha On a development server, create it with the CLI: -`{sample code will be here}` +```bash +temporal operator nexus endpoint create \ + --name approval-endpoint \ + --target-namespace approval-handler-namespace \ + --target-task-queue approval-handler-task-queue \ + --description-file description.md +``` In Temporal Cloud, create it in the UI under Nexus, or with `tcld`. See [Create a Nexus Endpoint](/nexus/registry#create-a-nexus-endpoint). @@ -48,7 +54,10 @@ On a development server there is nothing to configure. Both Namespaces are local For Temporal Cloud, the caller and handler connect as separate clients, each to its own Namespace. Generate an API key with access to both Namespaces, or use mTLS certificates. The SDK's [environment configuration](/develop/environment-configuration) support lets you keep one profile per Namespace and select between them with an environment variable, which is cleaner than passing connection options in code. -`{sample code will be here}` +```bash +temporal workflow list --namespace approval-handler-namespace +temporal activity list --namespace approval-handler-namespace +``` ## Verify it is reachable diff --git a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx index 11c825ba7a..4b73ef8c44 100644 --- a/docs/develop/java/nexus/development-walkthrough/send-messages.mdx +++ b/docs/develop/java/nexus/development-walkthrough/send-messages.mdx @@ -16,7 +16,7 @@ The caller does not know that one is a Signal and one is an Update. That is the ## Nudge and decide -`{sample code will be here}` +The caller calls both Operations exactly like any other, as shown in the caller Workflow in [step 6](/develop/java/nexus/development-walkthrough/call-the-service#call-the-operations-from-a-caller-workflow). `remindApprover` returns nothing, so there is nothing to assign; `submitDecision` returns the confirmation the Update produced. What differs between them is what you get back and how long it takes. @@ -36,13 +36,14 @@ A plain Signal cannot handle that. Sending one to a Workflow that does not exist `attachApprovalContext` uses **Signal-with-Start** instead. If the approval is already running, the note is delivered to it. If it is not, the approval is started and then the note is delivered. Either order works, and the caller does not have to know which happened. -`{sample code will be here}` + + Signal-with-Start is sync messaging on the [Client](/nexus/temporal-operation-handler#the-nexus-aware-client), so the Operation completes during the handler call and returns nothing. The caller gets no confirmation that a human read the note, only that it was durably attached. ### Input needs enough to start the Workflow -Look at the contract from [step 1](/develop/java/nexus/development-walkthrough/define-the-data-contract#plan-the-operations) and `attachApprovalContext` carries more than it seems to need: the item id, the requester, the amount, *and* the note. `remindApprover` gets by with just an approval id. +Look at the contract from [step 1](/develop/java/nexus/development-walkthrough/define-the-data-contract#plan-the-operations) and `attachApprovalContext` carries more than it seems to need: the item id, the requester, the amount, *and* the note. `remindApprover` gets by with just the item id. That is a direct consequence of Signal-with-Start. The Operation might have to start the approval Workflow, and starting it requires whatever the Workflow needs to run. An Operation that can create the thing it messages has to carry enough input to create it. @@ -60,7 +61,8 @@ That default is not arbitrary strictness. A Workflow-backed Operation has only s The fix is to change the Workflow Id conflict policy on `requestApproval` from its default to **use-existing**. With that set, a start against an already-running approval attaches the Operation's completion callback to that Execution instead of failing. The caller then awaits the approval that is already in flight and receives its decision when it completes. -`{sample code will be here}` + + Two things follow from this, and both are useful.