Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ These samples demonstrate some common control flow patterns using Temporal's Go

- [**Nexus Context Propagation**](./nexus-context-propagation): Demonstrates how to propagate context through client calls, workflows, and Nexus headers.

- [**Nexus Activity Operation**](./nexus-activity-operation): Demonstrates how to back a Nexus operation with an activity using `temporalnexus.NewTemporalOperation` and invoke it via the standalone `client.NexusClient`.



### Scenario based examples
Expand Down
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ go 1.25.0

replace github.com/cactus/go-statsd-client => github.com/cactus/go-statsd-client/v5 v5.0.0

// Use local sdk-go (branch NEXUS-382) which provides temporalnexus.NewTemporalOperation.
// Remove this directive once a released SDK version includes the API.
replace go.temporal.io/sdk => ../sdk-go

require (
github.com/aws/aws-sdk-go-v2 v1.41.7
github.com/aws/aws-sdk-go-v2/config v1.32.17
Expand All @@ -25,7 +29,7 @@ require (
go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.temporal.io/api v1.62.12
go.temporal.io/sdk v1.44.0
go.temporal.io/sdk v1.44.2-0.20260617163130-90858533dd27
go.temporal.io/sdk/contrib/aws/lambdaworker v0.1.1
go.temporal.io/sdk/contrib/aws/lambdaworker/otel v0.1.1
go.temporal.io/sdk/contrib/aws/s3driver v0.2.0
Expand Down Expand Up @@ -159,7 +163,7 @@ require (
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.41.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
4,691 changes: 4,671 additions & 20 deletions go.sum

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions nexus-activity-operation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# nexus-activity-operation

This sample shows how to author a Nexus operation **backed by an activity** using the generic
`temporalnexus.NewTemporalOperation` constructor, and how to invoke it directly using the
standalone `client.NexusClient` (no caller workflow required).

Activity-backed Nexus operations let the handler execute a single, long running, side-effecting call —
an API request, a database write, a compute step — without paying the cost of a workflow
execution. Temporal's retry, timeout, and cancellation semantics still apply via standard
activity options.

`NewTemporalOperation` is the unified constructor for Temporal-backed Nexus operations. The
`Start` callback receives a `NexusClient` scoped to the invocation and returns a
`TemporalOperationResult` produced by `temporalnexus.StartActivity` (or `StartUntypedActivity`
when the activity doesn't follow the single-input / single-output signature). The same
constructor can back operations with workflows (`StartWorkflow` / `StartUntypedWorkflow`) and
expose `CancelWorkflowRun` / `CancelActivityExecution` hooks for customizing cancel behavior
per token type.

The starter uses `client.NewNexusClient` + `ExecuteOperation` — the standalone Nexus API for
invoking operations from non-workflow code. For invocations from inside a workflow, use
`workflow.NewNexusClient` (see the [`nexus`](../nexus) sample).

This sample defines a single operation:

- `say-hello` — an asynchronous operation that schedules `HelloHandlerActivity` via
`temporalnexus.StartActivity` and returns an async result.

### Sample directory structure

- [service](./service) - shared service definition
- [handler](./handler) - operations and worker, defined with `NewTemporalOperation`
- [starter](./starter) - standalone Nexus client that invokes the operation
- [options](./options) - command line argument parsing utility

## Getting started locally

### Get `temporal` CLI to enable local development

Follow the [docs site](https://learn.temporal.io/getting_started/go/dev_environment/#set-up-a-local-temporal-service-for-development-with-temporal-cli)
to install Temporal CLI (v1.3.0 or later recommended).

### Spin up environment

#### Start temporal server

Activity-backed Nexus operations are currently gated behind a server dynamic config flags. For this sample please start the dev server with these three enabled:

```
temporal server start-dev \
--dynamic-config-value frontend.activityAPIsEnabled=true \
--dynamic-config-value nexusoperation.enableStandalone=true \
--dynamic-config-value activity.enableCallbacks=true
```

- `frontend.activityAPIsEnabled` exposes the Standalone Activity client APIs that the
handler uses to schedule the backing activity.
- `nexusoperation.enableStandalone` enables Standalone Nexus operations.
- `activity.enableCallbacks` enables callbacks on activities so the activity completion
can be delivered back through the Nexus operation token.

In a separate terminal:

#### Create caller and target namespaces

```
temporal operator namespace create --namespace my-target-namespace
temporal operator namespace create --namespace my-caller-namespace
```

#### Create Nexus endpoint

> NOTE: this must be run in the `nexus-activity-operation` sample directory.

```
temporal operator nexus endpoint create \
--name my-nexus-endpoint-name \
--target-namespace my-target-namespace \
--target-task-queue my-handler-task-queue \
--description-file ./service/description.md
```

### Run the handler worker

```
cd handler
go run ./worker \
-target-host localhost:7233 \
-namespace my-target-namespace
```

### Invoke the operation

```
cd starter
go run . \
-target-host localhost:7233 \
-namespace my-caller-namespace
```

### Output

```
Hello result: ¡Hola! Nexus 👋
```
55 changes: 55 additions & 0 deletions nexus-activity-operation/handler/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package handler

import (
"context"
"fmt"
"strings"
"time"

"go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporalnexus"

"github.com/temporalio/samples-go/nexus-activity-operation/service"
)

// HelloOperation is an asynchronous Nexus operation backed by a standalone activity execution.
// The Start callback uses temporalnexus.StartActivity to schedule HelloHandlerActivity and
// returns an async result whose operation token resolves when the activity completes.
//
// Activity-backed operations skip the cost of a workflow execution when the underlying work is
// a single side-effecting call (an API request, a database write, a compute step). They retain
// Temporal's retry, timeout, and cancellation semantics via standard activity options.
var HelloOperation = temporalnexus.MustNewTemporalOperation(temporalnexus.TemporalOperationOptions[service.HelloInput, service.HelloOutput]{
Name: service.HelloOperationName,
Start: func(ctx context.Context, nc temporalnexus.NexusClient, input service.HelloInput, _ temporalnexus.StartTemporalOperationOptions) (temporalnexus.TemporalOperationResult[service.HelloOutput], error) {
return temporalnexus.StartActivity(ctx, nc, client.StartActivityOptions{
ID: helloActivityID(input),
StartToCloseTimeout: 30 * time.Second,
// TaskQueue defaults to the task queue this operation is handled on.
}, HelloHandlerActivity, input)
},
})

// helloActivityID returns a deterministic activity ID derived from the input. The Nexus start
// request may be retried, and a non-deterministic ID would embed a fresh value into the
// operation token on each retry.
func helloActivityID(input service.HelloInput) string {
name := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(input.Name), " ", "-"))
return fmt.Sprintf("hello-%s-%s", input.Language, name)
}

func HelloHandlerActivity(_ context.Context, input service.HelloInput) (service.HelloOutput, error) {
switch input.Language {
case service.EN:
return service.HelloOutput{Message: "Hello " + input.Name + " 👋"}, nil
case service.FR:
return service.HelloOutput{Message: "Bonjour " + input.Name + " 👋"}, nil
case service.DE:
return service.HelloOutput{Message: "Hallo " + input.Name + " 👋"}, nil
case service.ES:
return service.HelloOutput{Message: "¡Hola! " + input.Name + " 👋"}, nil
case service.TR:
return service.HelloOutput{Message: "Merhaba " + input.Name + " 👋"}, nil
}
return service.HelloOutput{}, fmt.Errorf("unsupported language %q", input.Language)
}
42 changes: 42 additions & 0 deletions nexus-activity-operation/handler/worker/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"log"
"os"

"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"

"github.com/nexus-rpc/sdk-go/nexus"
"github.com/temporalio/samples-go/nexus-activity-operation/handler"
"github.com/temporalio/samples-go/nexus-activity-operation/options"
"github.com/temporalio/samples-go/nexus-activity-operation/service"
)

const (
taskQueue = "my-handler-task-queue"
)

func main() {
clientOptions, err := options.ParseClientOptionFlags(os.Args[1:])
if err != nil {
log.Fatalf("Invalid arguments: %v", err)
}
c, err := client.Dial(clientOptions)
if err != nil {
log.Fatalln("Unable to create client", err)
}
defer c.Close()

w := worker.New(c, taskQueue, worker.Options{})
svc := nexus.NewService(service.HelloServiceName)
if err := svc.Register(handler.HelloOperation); err != nil {
log.Fatalln("Unable to register operations", err)
}
w.RegisterNexusService(svc)
w.RegisterActivity(handler.HelloHandlerActivity)

if err := w.Run(worker.InterruptCh()); err != nil {
log.Fatalln("Unable to start worker", err)
}
}
95 changes: 95 additions & 0 deletions nexus-activity-operation/options/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package options

import (
"context"
"flag"
"fmt"
"os"

"crypto/tls"
"crypto/x509"

"go.temporal.io/sdk/client"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

// ParseClientOptionFlags parses the given arguments into client options. In
// some cases a failure will be returned as an error, in others the process may
// exit with help info.
func ParseClientOptionFlags(args []string) (client.Options, error) {
set := flag.NewFlagSet("nexus-activity-operation-sample", flag.ExitOnError)
targetHost := set.String("target-host", "localhost:7233", "Host:port for the Temporal service")
namespace := set.String("namespace", "default", "Namespace to connect to")
serverRootCACert := set.String("server-root-ca-cert", "", "Optional path to root server CA cert")
clientCert := set.String("client-cert", "", "Optional path to client cert, mutually exclusive with API key")
clientKey := set.String("client-key", "", "Optional path to client key, mutually exclusive with API key")
serverName := set.String("server-name", "", "Server name to use for verifying the server's certificate")
insecureSkipVerify := set.Bool("insecure-skip-verify", false, "Skip verification of the server's certificate and host name")
apiKey := set.String("api-key", "", "Optional API key, mutually exclusive with cert/key")

if err := set.Parse(args); err != nil {
return client.Options{}, fmt.Errorf("failed parsing args: %w", err)
}
if *clientCert != "" && *clientKey == "" || *clientCert == "" && *clientKey != "" {
return client.Options{}, fmt.Errorf("either both or neither of -client-key and -client-cert are required")
}
if *clientCert != "" && *apiKey != "" {
return client.Options{}, fmt.Errorf("either -client-cert and -client-key or -api-key are required, not both")
}

var connectionOptions client.ConnectionOptions
var credentials client.Credentials
if *clientCert != "" {
cert, err := tls.LoadX509KeyPair(*clientCert, *clientKey)
if err != nil {
return client.Options{}, fmt.Errorf("failed loading client cert and key: %w", err)
}

var serverCAPool *x509.CertPool
if *serverRootCACert != "" {
serverCAPool = x509.NewCertPool()
b, err := os.ReadFile(*serverRootCACert)
if err != nil {
return client.Options{}, fmt.Errorf("failed reading server CA: %w", err)
} else if !serverCAPool.AppendCertsFromPEM(b) {
return client.Options{}, fmt.Errorf("server CA PEM file invalid")
}
}

connectionOptions = client.ConnectionOptions{
TLS: &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: serverCAPool,
ServerName: *serverName,
InsecureSkipVerify: *insecureSkipVerify,
},
}
} else if *apiKey != "" {
connectionOptions = client.ConnectionOptions{
TLS: &tls.Config{},
DialOptions: []grpc.DialOption{
grpc.WithUnaryInterceptor(
func(ctx context.Context, method string, req any, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return invoker(
metadata.AppendToOutgoingContext(ctx, "temporal-namespace", *namespace),
method,
req,
reply,
cc,
opts...,
)
},
),
},
}
credentials = client.NewAPIKeyStaticCredentials(*apiKey)
}

return client.Options{
HostPort: *targetHost,
Namespace: *namespace,
ConnectionOptions: connectionOptions,
Credentials: credentials,
}, nil
}
25 changes: 25 additions & 0 deletions nexus-activity-operation/service/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package service

const HelloServiceName = "my-hello-service"

const HelloOperationName = "say-hello"

type Language string

const (
EN Language = "en"
FR Language = "fr"
DE Language = "de"
ES Language = "es"
TR Language = "tr"
)

type HelloInput struct {
Name string
Language Language
}

type HelloOutput struct {
Message string
}

4 changes: 4 additions & 0 deletions nexus-activity-operation/service/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Service: [my-hello-service](https://github.com/temporalio/samples-go/blob/main/nexus-activity-operation/service/api.go)
- operation: `say-hello`

See https://github.com/temporalio/samples-go/blob/main/nexus-activity-operation/service/api.go for Input / Output types.
Loading