|
| 1 | +--- |
| 2 | +id: context-propagation |
| 3 | +title: Context Propagation - Go SDK |
| 4 | +sidebar_label: Context Propagation |
| 5 | +toc_max_heading_level: 4 |
| 6 | +keywords: |
| 7 | + - context propagation |
| 8 | + - context propagators |
| 9 | + - go sdk |
| 10 | + - headers |
| 11 | + - tracing |
| 12 | +tags: |
| 13 | + - Context Propagation |
| 14 | + - Go SDK |
| 15 | + - Temporal SDKs |
| 16 | +description: How to propagate custom key-value data across Workflow, Activity, and Child Workflow boundaries using the Temporal Go SDK. |
| 17 | +--- |
| 18 | + |
| 19 | +Context propagation lets you pass custom key-value data from a Client to Workflows, and from Workflows to Activities and Child Workflows, without threading it through every function signature. Common use cases include propagating tracing IDs, tenant IDs, auth tokens, or other request-scoped metadata. |
| 20 | + |
| 21 | +{/* TODO: Link to /encyclopedia/context-propagation once that page lands */} |
| 22 | + |
| 23 | +:::tip |
| 24 | + |
| 25 | +If you want to propagate tracing context, check if there is a [built-in tracing interceptor](/develop/go/observability#tracing) for your library before building a custom context propagator. |
| 26 | + |
| 27 | +::: |
| 28 | + |
| 29 | +## How it works |
| 30 | + |
| 31 | +1. **Register** a context propagator on the Client via `ContextPropagators` in [ClientOptions](https://pkg.go.dev/go.temporal.io/sdk/internal#ClientOptions) |
| 32 | +2. **Inject** - On outbound calls, the SDK calls `Inject` (from `context.Context`) or `InjectFromWorkflow` (from `workflow.Context`) to serialize values into Temporal headers |
| 33 | +3. **Extract** - On inbound calls, the SDK calls `Extract` (into `context.Context`) or `ExtractToWorkflow` (into `workflow.Context`) to deserialize headers back into the context |
| 34 | +4. **Access** - Your Workflow and Activity code reads values from the context as usual |
| 35 | + |
| 36 | +## Implement a context propagator |
| 37 | + |
| 38 | +A context propagator implements the [`ContextPropagator`](https://pkg.go.dev/go.temporal.io/sdk/workflow#ContextPropagator) interface: |
| 39 | + |
| 40 | +```go |
| 41 | +type ContextPropagator interface { |
| 42 | + // Inject writes values from a Go context.Context into headers (Client/Activity side) |
| 43 | + Inject(context.Context, HeaderWriter) error |
| 44 | + // Extract reads headers into a Go context.Context (Client/Activity side) |
| 45 | + Extract(context.Context, HeaderReader) (context.Context, error) |
| 46 | + // InjectFromWorkflow writes values from a workflow.Context into headers |
| 47 | + InjectFromWorkflow(Context, HeaderWriter) error |
| 48 | + // ExtractToWorkflow reads headers into a workflow.Context |
| 49 | + ExtractToWorkflow(Context, HeaderReader) (Context, error) |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +There are two pairs of methods because Go uses `context.Context` in non-Workflow code (Client, Activities) and `workflow.Context` inside Workflows. You must implement all four methods for values to propagate across every boundary (Client → Workflow → Activity/Child Workflow). |
| 54 | + |
| 55 | +Here is a propagator that carries a custom key-value pair from the Client to Workflows and Activities (from the [context propagation sample](https://github.com/temporalio/samples-go/tree/main/ctxpropagation)): |
| 56 | + |
| 57 | + |
| 58 | +<!--SNIPSTART samples-go-ctx-propagation-propagator--> |
| 59 | +[ctxpropagation/propagator.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/propagator.go) |
| 60 | +```go |
| 61 | +type ( |
| 62 | + // contextKey is an unexported type used as key for items stored in the |
| 63 | + // Context object |
| 64 | + contextKey struct{} |
| 65 | + |
| 66 | + // propagator implements the custom context propagator |
| 67 | + propagator struct{} |
| 68 | + |
| 69 | + // Values is a struct holding values |
| 70 | + Values struct { |
| 71 | + Key string `json:"key"` |
| 72 | + Value string `json:"value"` |
| 73 | + } |
| 74 | +) |
| 75 | + |
| 76 | +// PropagateKey is the key used to store the value in the Context object |
| 77 | +var PropagateKey = contextKey{} |
| 78 | + |
| 79 | +// HeaderKey is the key used by the propagator to pass values through the |
| 80 | +// Temporal server headers |
| 81 | +const HeaderKey = "custom-header" |
| 82 | + |
| 83 | +// NewContextPropagator returns a context propagator that propagates a set of |
| 84 | +// string key-value pairs across a workflow |
| 85 | +func NewContextPropagator() workflow.ContextPropagator { |
| 86 | + return &propagator{} |
| 87 | +} |
| 88 | + |
| 89 | +// Inject injects values from context into headers for propagation |
| 90 | +func (s *propagator) Inject(ctx context.Context, writer workflow.HeaderWriter) error { |
| 91 | + value := ctx.Value(PropagateKey) |
| 92 | + payload, err := converter.GetDefaultDataConverter().ToPayload(value) |
| 93 | + if err != nil { |
| 94 | + return err |
| 95 | + } |
| 96 | + writer.Set(HeaderKey, payload) |
| 97 | + return nil |
| 98 | +} |
| 99 | + |
| 100 | +// InjectFromWorkflow injects values from context into headers for propagation |
| 101 | +func (s *propagator) InjectFromWorkflow(ctx workflow.Context, writer workflow.HeaderWriter) error { |
| 102 | + value := ctx.Value(PropagateKey) |
| 103 | + payload, err := converter.GetDefaultDataConverter().ToPayload(value) |
| 104 | + if err != nil { |
| 105 | + return err |
| 106 | + } |
| 107 | + writer.Set(HeaderKey, payload) |
| 108 | + return nil |
| 109 | +} |
| 110 | + |
| 111 | +// Extract extracts values from headers and puts them into context |
| 112 | +func (s *propagator) Extract(ctx context.Context, reader workflow.HeaderReader) (context.Context, error) { |
| 113 | + if value, ok := reader.Get(HeaderKey); ok { |
| 114 | + var values Values |
| 115 | + if err := converter.GetDefaultDataConverter().FromPayload(value, &values); err != nil { |
| 116 | + return ctx, nil |
| 117 | + } |
| 118 | + ctx = context.WithValue(ctx, PropagateKey, values) |
| 119 | + } |
| 120 | + |
| 121 | + return ctx, nil |
| 122 | +} |
| 123 | +``` |
| 124 | +<!--SNIPEND--> |
| 125 | + |
| 126 | +## Register the propagator and set context values |
| 127 | + |
| 128 | +Register the propagator on the Client. Then set context values before starting a Workflow: |
| 129 | + |
| 130 | +<!--SNIPSTART samples-go-ctx-propagation-starter--> |
| 131 | +[ctxpropagation/starter/main.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/starter/main.go) |
| 132 | +```go |
| 133 | +// The client is a heavyweight object that should be created once per process. |
| 134 | +c, err := client.Dial(client.Options{ |
| 135 | + HostPort: client.DefaultHostPort, |
| 136 | + Interceptors: []interceptor.ClientInterceptor{tracingInterceptor}, |
| 137 | + ContextPropagators: []workflow.ContextPropagator{ctxpropagation.NewContextPropagator()}, |
| 138 | +}) |
| 139 | +if err != nil { |
| 140 | + log.Fatalln("Unable to create client", err) |
| 141 | +} |
| 142 | +defer c.Close() |
| 143 | + |
| 144 | +workflowID := "ctx-propagation_" + uuid.New() |
| 145 | +workflowOptions := client.StartWorkflowOptions{ |
| 146 | + ID: workflowID, |
| 147 | + TaskQueue: "ctx-propagation", |
| 148 | +} |
| 149 | + |
| 150 | +ctx := context.Background() |
| 151 | +ctx = context.WithValue(ctx, ctxpropagation.PropagateKey, &ctxpropagation.Values{Key: "test", Value: "tested"}) |
| 152 | + |
| 153 | +we, err := c.ExecuteWorkflow(ctx, workflowOptions, ctxpropagation.CtxPropWorkflow) |
| 154 | +``` |
| 155 | +<!--SNIPEND--> |
| 156 | + |
| 157 | +You can also register context propagators through a [Plugin](/develop/plugins-guide) if you are building a reusable library. |
| 158 | + |
| 159 | +## Access propagated values |
| 160 | + |
| 161 | +In your Workflow, the propagated values are available on the `workflow.Context`. When the Workflow starts an Activity, the SDK automatically propagates the same values: |
| 162 | + |
| 163 | +<!--SNIPSTART samples-go-ctx-propagation-workflow--> |
| 164 | +[ctxpropagation/workflow.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/workflow.go) |
| 165 | +```go |
| 166 | +// CtxPropWorkflow workflow definition |
| 167 | +func CtxPropWorkflow(ctx workflow.Context) (err error) { |
| 168 | + ao := workflow.ActivityOptions{ |
| 169 | + StartToCloseTimeout: 2 * time.Second, // such a short timeout to make sample fail over very fast |
| 170 | + } |
| 171 | + ctx = workflow.WithActivityOptions(ctx, ao) |
| 172 | + |
| 173 | + if val := ctx.Value(PropagateKey); val != nil { |
| 174 | + vals := val.(Values) |
| 175 | + workflow.GetLogger(ctx).Info("custom context propagated to workflow", vals.Key, vals.Value) |
| 176 | + } |
| 177 | + |
| 178 | + var values Values |
| 179 | + if err = workflow.ExecuteActivity(ctx, SampleActivity).Get(ctx, &values); err != nil { |
| 180 | + workflow.GetLogger(ctx).Error("Workflow failed.", "Error", err) |
| 181 | + return err |
| 182 | + } |
| 183 | + workflow.GetLogger(ctx).Info("context propagated to activity", values.Key, values.Value) |
| 184 | + workflow.GetLogger(ctx).Info("Workflow completed.") |
| 185 | + return nil |
| 186 | +} |
| 187 | +``` |
| 188 | +<!--SNIPEND--> |
| 189 | + |
| 190 | +<!--SNIPSTART samples-go-ctx-propagation-activity--> |
| 191 | +[ctxpropagation/activities.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/activities.go) |
| 192 | +```go |
| 193 | +func SampleActivity(ctx context.Context) (*Values, error) { |
| 194 | + if val := ctx.Value(PropagateKey); val != nil { |
| 195 | + vals := val.(Values) |
| 196 | + return &vals, nil |
| 197 | + } |
| 198 | + return nil, nil |
| 199 | +} |
| 200 | +``` |
| 201 | +<!--SNIPEND--> |
| 202 | + |
| 203 | +You can configure multiple context propagators on a single Client, each responsible for its own set of keys. |
| 204 | + |
| 205 | +## Context propagation over Nexus |
| 206 | + |
| 207 | +Nexus does not use the `ContextPropagator` interface. It relies on a Temporal-agnostic protocol with its own header format (`nexus.Header`, a wrapper around `map[string]string`). |
| 208 | + |
| 209 | +To propagate context over Nexus Operation calls, use interceptors to explicitly serialize and deserialize context into the Nexus header. See the [Nexus Context Propagation sample](https://github.com/temporalio/samples-go/tree/main/nexus-context-propagation). |
| 210 | + |
| 211 | +## Further reading |
| 212 | + |
| 213 | +- [Passing Context with Temporal](https://spiralscout.com/blog/passing-context-with-temporal) - A conceptual guide to middleware and walkthrough of building a context propagator in Go |
0 commit comments