Skip to content

Commit 3c8a12e

Browse files
authored
Add Nexus SAA sample (#529)
1 parent a2397f3 commit 3c8a12e

7 files changed

Lines changed: 343 additions & 12 deletions

File tree

README.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -233,22 +233,10 @@ These samples demonstrate some common control flow patterns using Temporal's Go
233233

234234
- [**Worker-specific Task Queues**](./worker-specific-task-queues): Use a unique task queue per Worker to have certain Activities only run on that specific Worker. For instance for a file processing Workflow, where one Activity downloads a file and subsequent Activities need to operate on that file. (If multiple Workers were on the same queue, subsequent Activities may get run on different machines that don't have the downloaded file.)
235235

236-
- [**Nexus**](./nexus): Demonstrates how to use the Nexus APIs to facilitate cross namespace calls.
237-
238-
- [**Nexus Cancelation**](./nexus-cancelation): Demonstrates how to cancel a Nexus operation from a caller workflow.
239-
240-
- [**Nexus Context Propagation**](./nexus-context-propagation): Demonstrates how to propagate context through client calls, workflows, and Nexus headers.
241-
242-
243-
244236
### Scenario based examples
245237

246238
- [**Safe Message Handler**](./safe_message_handler): This demonstrates how to safely handle concurrent update and signal requests.
247239

248-
- [**Nexus Messaging**](./nexus-messaging): Demonstrates how send signal, update and query messages through Nexus.
249-
This contains two samples, one sending messages to an existing workflow and a second that creates a workflow through Nexus
250-
and sends messages to it.
251-
252240
- [**DSL Workflow**](./dsl): Demonstrates how to implement a
253241
DSL-based Workflow. This sample contains 2 yaml files that each define a custom "workflow" which instructs the
254242
Temporal Workflow. This is useful if you want to build in a "low code" layer.
@@ -288,6 +276,28 @@ resource waiting for its successful completion
288276
- [**Worker Versioning**](./worker-versioning):
289277
Demonstrates how to use worker versioning to manage workflow code changes.
290278

279+
### Nexus examples
280+
281+
- [**Nexus**](./nexus): Demonstrates how to use the Nexus APIs to facilitate cross namespace calls.
282+
283+
- [**Nexus Cancelation**](./nexus-cancelation): Demonstrates how to cancel a Nexus Operation from a caller Workflow.
284+
285+
- [**Nexus Context Propagation**](./nexus-context-propagation): Demonstrates how to propagate context through client
286+
calls, Workflows, and Nexus headers.
287+
288+
- [**Nexus Messaging**](./nexus-messaging): Demonstrates how to send signal, update and query messages through Nexus.
289+
This contains two samples, one sending messages to an existing Workflow and a second that creates a Workflow through
290+
Nexus and sends messages to it.
291+
292+
- [**Nexus Multiple Arguments**](./nexus-multiple-arguments): Demonstrates how to map a Nexus Operation to a Workflow
293+
that takes multiple arguments.
294+
295+
- [**Standalone Nexus Operations**](./nexus-standalone-operations): Demonstrates how to execute Nexus Operations
296+
directly from a Temporal Client, without a caller Workflow.
297+
298+
- [**Nexus Standalone Activity**](./nexus-standalone-activity): Demonstrates how to back a Nexus Operation with a
299+
Standalone Activity.
300+
291301
### Serverless
292302

293303
- [**Lambda Worker**](./lambda-worker): Demonstrates how to run a Temporal Worker as an
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
## Nexus Operation Backed by a Standalone Activity
2+
3+
> [!WARNING]
4+
> Standalone Nexus Operations and Standalone Activities are experimental and may be subject to backwards-incompatible changes. They require a Temporal server that implements and enables them via the dynamic configs shown below. Use the dev server build at https://github.com/temporalio/cli/releases/tag/v1.7.4-standalone-nexus-operations.
5+
6+
This sample shows how to implement a Nexus Operation whose backing execution is a **Standalone Activity**.
7+
8+
### Sample structure
9+
10+
| File | Purpose |
11+
|--------------------------------------------|------------------------------------------------------------------------------------------------|
12+
| [`service/api.go`](./service/api.go) | Nexus service definition shared by caller and handler |
13+
| [`handler/app.go`](./handler/app.go) | The standalone Activity, and the operation built with `NewTemporalOperation` + `StartActivity` |
14+
| [`worker/main.go`](./worker/main.go) | Worker hosting the Nexus handler and the Activity |
15+
| [`starter/main.go`](./starter/main.go) | Executes the Nexus Operation from client code |
16+
17+
The starter and worker connect to two different namespaces (a "caller" namespace and a "handler" namespace) — this mirrors how Nexus is typically used to cross namespace boundaries. The client is configured via the SDK's [environment configuration](https://docs.temporal.io/develop/environment-configuration) support, which reads `TEMPORAL_NAMESPACE`, `TEMPORAL_ADDRESS`, etc. from the environment (and optionally profiles from `temporal.toml`).
18+
19+
## Run locally against a dev server
20+
21+
1) Start the [Temporal dev server build that supports standalone Nexus Operations](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support) with the required namespaces pre-created and Activity callbacks enabled:
22+
23+
```bash
24+
./temporal server start-dev \
25+
--dynamic-config-value activity.enableCallbacks=true \
26+
--namespace my-caller-namespace \
27+
--namespace my-handler-namespace
28+
```
29+
30+
2) Create a Nexus endpoint that routes to the handler namespace and the worker's task queue:
31+
32+
```bash
33+
./temporal operator nexus endpoint create \
34+
--name my-nexus-endpoint \
35+
--target-namespace my-handler-namespace \
36+
--target-task-queue nexus-handler-queue
37+
```
38+
39+
3) In a second terminal, start the worker in the handler namespace:
40+
41+
```bash
42+
TEMPORAL_NAMESPACE=my-handler-namespace \
43+
go run nexus-standalone-activity/worker/main.go
44+
```
45+
46+
You should see a log line similar to:
47+
48+
```bash
49+
2026/08/18 13:28:43 INFO Started Worker Namespace my-handler-namespace TaskQueue nexus-handler-queue WorkerID 53608
50+
```
51+
52+
4) In a third terminal, run the starter in the caller namespace:
53+
54+
```bash
55+
TEMPORAL_NAMESPACE=my-caller-namespace \
56+
go run nexus-standalone-activity/starter/main.go
57+
```
58+
59+
You should see something similar to:
60+
61+
```bash
62+
2026/08/18 13:29:14 Started Greet operation OperationID greeting-afb1ff21-c842-40f7-ba85-28458d0150a6
63+
2026/08/18 13:29:14 Hello, World!
64+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @@@SNIPSTART samples-go-nexus-standalone-activity-handler
2+
package handler
3+
4+
import (
5+
"context"
6+
"time"
7+
8+
"go.temporal.io/sdk/activity"
9+
"go.temporal.io/sdk/client"
10+
"go.temporal.io/sdk/temporalnexus"
11+
12+
"github.com/temporalio/samples-go/nexus-standalone-activity/service"
13+
)
14+
15+
// CreateGreetingActivity builds the greeting. It runs as a Standalone Activity, so it does not
16+
// need a backing Workflow.
17+
func CreateGreetingActivity(ctx context.Context, input service.GreetingInput) (service.GreetingOutput, error) {
18+
activity.GetLogger(ctx).Info("CreateGreeting", "name", input.Name)
19+
return service.GreetingOutput{Message: "Hello, " + input.Name + "!"}, nil
20+
}
21+
22+
// GreetOperation is the Nexus Operation backed by CreateGreetingActivity. Start dispatches the
23+
// Activity and returns its handle, so the caller's Nexus Operation completes when the Activity does.
24+
var GreetOperation = temporalnexus.MustNewTemporalOperation(
25+
temporalnexus.TemporalOperationOptions[service.GreetingInput, service.GreetingOutput]{
26+
Name: service.GreetOperationName,
27+
Start: func(
28+
ctx context.Context,
29+
nc temporalnexus.NexusClient,
30+
input service.GreetingInput,
31+
options temporalnexus.StartTemporalOperationOptions,
32+
) (temporalnexus.TemporalOperationResult[service.GreetingOutput], error) {
33+
return temporalnexus.StartActivity(ctx, nc, client.StartActivityOptions{
34+
ID: service.GreetingActivityID(input),
35+
StartToCloseTimeout: 10 * time.Second,
36+
}, CreateGreetingActivity, input)
37+
},
38+
},
39+
)
40+
41+
// @@@SNIPEND
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package nexus_standalone_activity_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/nexus-rpc/sdk-go/nexus"
12+
13+
nexuspb "go.temporal.io/api/nexus/v1"
14+
"go.temporal.io/api/operatorservice/v1"
15+
"go.temporal.io/sdk/client"
16+
"go.temporal.io/sdk/testsuite"
17+
"go.temporal.io/sdk/worker"
18+
19+
"github.com/temporalio/samples-go/nexus-standalone-activity/handler"
20+
"github.com/temporalio/samples-go/nexus-standalone-activity/service"
21+
)
22+
23+
const (
24+
taskQueue = "nexus-standalone-activity-test"
25+
endpointName = "nexus-standalone-activity-test-endpoint"
26+
)
27+
28+
func Test_NexusOperationStandaloneActivity_Using_DevServer(t *testing.T) {
29+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
30+
defer cancel()
31+
32+
// Start the dev server with standalone Nexus support and Activity callbacks enabled, which is
33+
// what lets a Standalone Activity complete a Nexus Operation.
34+
server, err := testsuite.StartDevServer(ctx, testsuite.DevServerOptions{
35+
CachedDownload: testsuite.CachedDownload{
36+
Version: "v1.7.4-standalone-nexus-operations",
37+
},
38+
ExtraArgs: []string{
39+
"--dynamic-config-value", "activity.enableCallbacks=true",
40+
},
41+
})
42+
require.NoError(t, err)
43+
defer func() { _ = server.Stop() }()
44+
45+
c := server.Client()
46+
47+
// Create a Nexus endpoint targeting our task queue.
48+
_, err = c.OperatorService().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{
49+
Spec: &nexuspb.EndpointSpec{
50+
Name: endpointName,
51+
Target: &nexuspb.EndpointTarget{
52+
Variant: &nexuspb.EndpointTarget_Worker_{
53+
Worker: &nexuspb.EndpointTarget_Worker{
54+
Namespace: "default",
55+
TaskQueue: taskQueue,
56+
},
57+
},
58+
},
59+
},
60+
})
61+
require.NoError(t, err)
62+
63+
// Register Nexus operations on the worker.
64+
w := worker.New(c, taskQueue, worker.Options{})
65+
66+
svc := nexus.NewService(service.GreetingServiceName)
67+
require.NoError(t, svc.Register(handler.GreetOperation))
68+
w.RegisterNexusService(svc)
69+
w.RegisterActivity(handler.CreateGreetingActivity)
70+
require.NoError(t, w.Start())
71+
defer w.Stop()
72+
73+
// Create a standalone NexusClient.
74+
nexusClient, err := c.NewNexusClient(client.NexusClientOptions{
75+
Endpoint: endpointName,
76+
Service: service.GreetingServiceName,
77+
})
78+
require.NoError(t, err)
79+
80+
handle, err := nexusClient.ExecuteOperation(ctx, service.GreetOperationName, service.GreetingInput{Name: "Test"}, client.StartNexusOperationOptions{
81+
ID: "greeting-" + uuid.NewString(),
82+
ScheduleToCloseTimeout: 10 * time.Second,
83+
})
84+
require.NoError(t, err)
85+
require.NotEmpty(t, handle.GetID())
86+
87+
var result service.GreetingOutput
88+
err = handle.Get(ctx, &result)
89+
require.NoError(t, err)
90+
require.Equal(t, "Hello, Test!", result.Message)
91+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @@@SNIPSTART samples-go-nexus-standalone-activity-service
2+
package service
3+
4+
import "fmt"
5+
6+
// Nexus service definition shared by the caller and the handler. It declares a single operation
7+
// whose backing execution is a Standalone Activity.
8+
9+
const GreetingServiceName = "NexusGreetingService"
10+
11+
const GreetOperationName = "greet"
12+
13+
type GreetingInput struct {
14+
Name string
15+
}
16+
17+
type GreetingOutput struct {
18+
Message string
19+
}
20+
21+
func GreetingActivityID(input GreetingInput) string {
22+
return fmt.Sprintf("greeting-%s", input.Name)
23+
}
24+
25+
// @@@SNIPEND
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// @@@SNIPSTART samples-go-nexus-standalone-activity-starter
2+
package main
3+
4+
import (
5+
"context"
6+
"log"
7+
"time"
8+
9+
"github.com/google/uuid"
10+
11+
"go.temporal.io/sdk/client"
12+
"go.temporal.io/sdk/contrib/envconfig"
13+
14+
"github.com/temporalio/samples-go/nexus-standalone-activity/service"
15+
)
16+
17+
// Executes the Activity-backed Nexus Operation from client code. The operation is standalone: it is
18+
// started directly by this client rather than from within a caller Workflow.
19+
20+
const endpointName = "my-nexus-endpoint"
21+
22+
func main() {
23+
// The client is a heavyweight object that should be created once per process.
24+
c, err := client.Dial(envconfig.MustLoadDefaultClientOptions())
25+
if err != nil {
26+
log.Fatalln("Unable to create client", err)
27+
}
28+
defer c.Close()
29+
30+
// Create a NexusClient bound to the endpoint and service.
31+
// The endpoint must be pre-created on the server (see README).
32+
nexusClient, err := c.NewNexusClient(client.NexusClientOptions{
33+
Endpoint: endpointName,
34+
Service: service.GreetingServiceName,
35+
})
36+
if err != nil {
37+
log.Fatalln("Unable to create Nexus client", err)
38+
}
39+
40+
handle, err := nexusClient.ExecuteOperation(context.Background(), service.GreetOperationName, service.GreetingInput{Name: "World"}, client.StartNexusOperationOptions{
41+
ID: "greeting-" + uuid.NewString(),
42+
ScheduleToCloseTimeout: 10 * time.Second,
43+
})
44+
if err != nil {
45+
log.Fatalln("Unable to execute Greet operation", err)
46+
}
47+
log.Println("Started Greet operation", "OperationID", handle.GetID())
48+
49+
var result service.GreetingOutput
50+
err = handle.Get(context.Background(), &result)
51+
if err != nil {
52+
log.Fatalln("Unable to get Greet operation result", err)
53+
}
54+
log.Println(result.Message)
55+
}
56+
57+
// @@@SNIPEND
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// @@@SNIPSTART samples-go-nexus-standalone-activity-worker
2+
package main
3+
4+
import (
5+
"log"
6+
7+
"github.com/nexus-rpc/sdk-go/nexus"
8+
9+
"go.temporal.io/sdk/client"
10+
"go.temporal.io/sdk/contrib/envconfig"
11+
"go.temporal.io/sdk/worker"
12+
13+
"github.com/temporalio/samples-go/nexus-standalone-activity/handler"
14+
"github.com/temporalio/samples-go/nexus-standalone-activity/service"
15+
)
16+
17+
const taskQueue = "nexus-handler-queue"
18+
19+
func main() {
20+
// The client and worker are heavyweight objects that should be created once per process.
21+
c, err := client.Dial(envconfig.MustLoadDefaultClientOptions())
22+
if err != nil {
23+
log.Fatalln("Unable to create client", err)
24+
}
25+
defer c.Close()
26+
27+
w := worker.New(c, taskQueue, worker.Options{})
28+
29+
svc := nexus.NewService(service.GreetingServiceName)
30+
err = svc.Register(handler.GreetOperation)
31+
if err != nil {
32+
log.Fatalln("Unable to register operations", err)
33+
}
34+
w.RegisterNexusService(svc)
35+
w.RegisterActivity(handler.CreateGreetingActivity)
36+
37+
err = w.Run(worker.InterruptCh())
38+
if err != nil {
39+
log.Fatalln("Unable to start worker", err)
40+
}
41+
}
42+
43+
// @@@SNIPEND

0 commit comments

Comments
 (0)