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..09e82bfc22 --- /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 [Nexus SDK V2](/nexus/sdk-v2) 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..5a432672e1 --- /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), 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..2b1336e10a --- /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) 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). + +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..47d596a3bf --- /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 [Updated handler methods](/nexus/sdk-v2#updated-handler-methods). + +::: + +## 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..96fe8a550b --- /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 [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. + +::: + +## 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) 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-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/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 7f751809b1..ade3cff89c 100644 --- a/sidebars.js +++ b/sidebars.js @@ -392,6 +392,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', + ], + }, ], }, { @@ -1948,6 +1970,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',