# Nexus SDK V2

> Implement Nexus Operations with the Temporal Operation Handler - back an Operation with a Workflow, an Update, or an Activity, and get bidirectional linking across the Namespace boundary.

> **⚠️ 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.
>

A [Nexus Service](/nexus/services) publishes Operations that other teams call across [Namespace](/namespaces) boundaries.
`TemporalOperationHandler` is how you implement those Operations.

It is a single handler type that can back an Operation with any Temporal primitive — start a Workflow, run an Update, start an Activity — or complete the Operation inline with no backing Execution at all.
Whichever you choose, the handler is the same shape, and every Execution it touches is connected back to the caller automatically.

## What you can do with it

**Back an Operation with whichever primitive fits the work.** A multi-step process is a Workflow. A single durable step is an [Activity](/nexus/standalone-activity), with no Workflow wrapped around it. A change to something already running is an Update. The caller sees the same Operation contract either way, and you can change your choice later without touching callers.

**Combine messaging and a backing in one handler.** A handler can Signal a running Workflow to unblock it and then return a different Execution's result for the caller to await. These are not separate handler types you pick between; they compose inside one start handler.

**Get observability across the Namespace boundary without wiring it.** The Client handed to your handler propagates [bidirectional links](/nexus/execution-debugging#bi-directional-linking) and request Ids on every call it makes. The caller-side and handler-side Executions are connected in the UI and in [Event History](/encyclopedia/event-history), so a single trace crosses the boundary between two teams' Namespaces.

**Stay idempotent through retries.** The server retries Nexus start requests, and the request Id travels with them. Deriving the backing Execution's Id from it means a retry targets the same Execution rather than starting a second one.

**Cancel through the same handler.** The Operation token records which kind of Execution backs the Operation, so a cancellation request reaches the right place. The default behavior is usually what you want, and each kind can be overridden when it is not.

**Grow a handler without rewriting it.** An Operation that starts out completing inline can later gain an async backing, or pick up a Signal, without changing handler type or breaking its contract.

## The Nexus-aware Client

`TemporalOperationHandler.create(...)` gives your start handler three things: a context, a Client, and the Operation input.

The Client is what makes the linking automatic, so prefer it over constructing your own inside a handler.
Reaching for your own Client still works, but messages sent that way are not connected back to the caller.

The Client exposes two kinds of call, and the distinction shapes how you write the handler.

**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 and Signal-with-Start

Query, Cancel, and Terminate are not part of the pre-release.
You can still reach them through a Temporal Client of your own, but a message sent that way is not linked back to the caller.

A handler that only sends messages returns `TemporalOperationResult.sync(...)`, and the Operation completes immediately.

## Write an Operation handler

The examples below use a Nexus Service with a `startGreeting` Operation backed by a Workflow, a `cancelOrder` Operation that sends a Signal, and a `greet` Operation backed by an Activity. Click the language tabs to see example code in each language - Go, Java, .Net, Python, and Typescript.

> **📝 Note:**
> This is still a rough draft for feedback. Not all languages are filled in yet.
>

### Back an Operation with a Workflow

Call `startWorkflow` on the Client and return its result. The Operation completes when the Workflow returns, delivering the Workflow's return value to the caller.

**Go**

```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**

```java
@OperationImpl
public OperationHandler<GreetingInput, GreetingOutput> startGreeting() {
  return TemporalOperationHandler.create(
      (context, client, input) ->
          client.startWorkflow(
              GreetingWorkflow.class,
              GreetingWorkflow::greet,
              input,
              WorkflowOptions.newBuilder()
                  .setWorkflowId("greeting-" + input.getName())
                  .build()));
}
```

**Python**

```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**

```typescript
const startGreeting = new temporalnexus.TemporalOperationHandler<GreetingInput, GreetingOutput>({
  async start(ctx, client, input) {
    return await client.startWorkflow(greetingWorkflow, {
      args: [input],
      workflowId: `greeting-${input.name}`,
    });
  },
});
```

**.NET**

```csharp
TemporalOperationHandler.FromHandleFactory<GreetingInput, GreetingOutput>(
    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

Reach the Workflow Client through the injected Client, send the message, and return a synchronous result. The Operation completes during the handler call, and the Signal is linked back to the caller.

**Go**

```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**

```java
@OperationImpl
public OperationHandler<CancelOrderInput, Void> cancelOrder() {
  return TemporalOperationHandler.create(
      (context, client, input) -> {
        client.getWorkflowClient()
            .newUntypedWorkflowStub("order-" + input.getOrderId())
            .signal("requestCancellation", input);
        return TemporalOperationResult.sync(null);
      });
}
```

**Python**

```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**

```typescript
const cancelOrder = new temporalnexus.TemporalOperationHandler<CancelOrderInput, void>({
  async start(ctx, client, input) {
    await client.getWorkflowHandle(`order-${input.orderId}`).signal(requestCancellation, input);
    return temporalnexus.TemporalOperationResult.sync(undefined);
  },
});
```

**.NET**

```csharp
TemporalOperationHandler.FromHandleFactory<CancelOrderInput, NoValue>(
    async (context, client, input) =>
    {
        await client.TemporalClient
            .GetWorkflowHandle($"order-{input.OrderId}")
            .SignalAsync("requestCancellation", new object?[] { input });
        return TemporalOperationResult<NoValue>.SyncResult(default);
    });
```

The same Client also offers Signal-with-Start, and a handler may send several messages before returning.

### Back an Operation with an Activity

Call `startActivity` when the work is a single durable step. The Activity runs with no parent Workflow, so the options require an Activity Id and a Task Queue. See [Nexus Standalone Activity](/nexus/standalone-activity).

**Go**

```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**

```java
@OperationImpl
public OperationHandler<GreetingInput, GreetingOutput> 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()));
}
```

**.NET**

```csharp
TemporalOperationHandler.FromHandleFactory<GreetingInput, GreetingOutput>(
    async (context, client, input) =>
        await client.StartActivityAsync<GreetingOutput>(
            () => GreetingActivities.GreetAsync(input),
            new()
            {
                Id = $"greet-{input.Name}",
                TaskQueue = TaskQueueName,
                ScheduleToCloseTimeout = TimeSpan.FromMinutes(1),
            }));
```

**Python**

Code coming in next draft

**TypeScript**

Code coming in next draft

## Coming from the earlier handler APIs

Skip this section if you are new to Nexus.

Earlier SDK versions had a separate helper per pattern. Existing handlers will keep working, there is no forced migration.

| If you used | Use instead |
| --- | --- |
| The Workflow-run helper (`WorkflowRunOperation`, `NewWorkflowRunOperation`, `@workflow_run_operation`) | `TemporalOperationHandler` with `startWorkflow` |
| The synchronous handler (`OperationHandler.sync`, `nexus.NewSyncOperation`, `@sync_operation`) | `TemporalOperationHandler` returning a sync result |
| A Workflow wrapping a single Activity | `TemporalOperationHandler` with `startActivity` |
| A Temporal Client fetched inside a handler | The Client injected into the start handler |

Two things improve when you migrate. Messages and Executions get [bidirectional linking](/nexus/execution-debugging#bi-directional-linking), which hand-fetched Clients do not produce. And one handler type covers every case, so an Operation can change what backs it without changing shape.

### Migrating a Workflow-backed Operation

The earlier helper reached the Client through the Operation context and returned a Workflow handle or method reference, rather than being handed a Client and returning an Operation result:

**Go**

```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**

```java
@OperationImpl
public OperationHandler<GreetingInput, GreetingOutput> startGreeting() {
  return WorkflowRunOperation.fromWorkflowMethod(
      (ctx, details, input) ->
          Nexus.getOperationContext()
                  .getWorkflowClient()
                  .newWorkflowStub(
                      GreetingWorkflow.class,
                      WorkflowOptions.newBuilder()
                          .setWorkflowId("greeting-" + input.getName())
                          .build())
              ::greet);
}
```

**Python**

```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**

```typescript
const startGreeting = new temporalnexus.WorkflowRunOperationHandler(
  async (ctx, input: GreetingInput) =>
    await temporalnexus.startWorkflow(ctx, greetingWorkflow, {
      args: [input],
      workflowId: `greeting-${input.name}`,
    }),
);
```

**.NET**

```csharp
WorkflowRunOperationHandler.FromHandleFactory<GreetingInput, GreetingOutput>(
    async (context, input) =>
        await context.StartWorkflowAsync(
            (GreetingWorkflow wf) => wf.RunAsync(input),
            new() { Id = $"greeting-{input.Name}" }));
```

Replace it with [Back an Operation with a Workflow](#back-an-operation-with-a-workflow).

### Migrating a synchronous Operation

A messaging Operation used to be a synchronous handler that fetched its own Client, which is why those messages produced no links:

**Go**

```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**

```java
@OperationImpl
public OperationHandler<CancelOrderInput, Void> cancelOrder() {
  return OperationHandler.sync(
      (ctx, details, input) -> {
        Nexus.getOperationContext()
            .getWorkflowClient()
            .newUntypedWorkflowStub("order-" + input.getOrderId())
            .signal("requestCancellation", input);
        return null;
      });
}
```

**Python**

```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**

Code coming in next draft

**.NET**

Code coming in next draft

Replace it with [Send a Signal from an Operation](#send-a-signal-from-an-operation).

> **💡 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)
>
