Skip to main content

Context Propagation - Go SDK

View Markdown

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 tenant identifiers, auth tokens, or other request-scoped metadata.

tip

For OpenTelemetry tracing, use the OpenTelemetry v2 integration. The plugin handles OpenTelemetry trace-context propagation automatically, so you do not need a custom Temporal context propagator for trace identifiers.

How it works

  1. Register a context propagator on the Client via ContextPropagators in ClientOptions
  2. Inject - On outbound calls, the SDK calls Inject (from context.Context) or InjectFromWorkflow (from workflow.Context) to serialize values into Temporal headers
  3. Extract - On inbound calls, the SDK calls Extract (into context.Context) or ExtractToWorkflow (into workflow.Context) to deserialize headers back into the context
  4. Access - Your Workflow and Activity code reads values from the context as usual

Implement a context propagator

A context propagator implements the ContextPropagator interface:

type ContextPropagator interface {
// Inject writes values from a Go context.Context into headers (Client/Activity side)
Inject(context.Context, HeaderWriter) error
// Extract reads headers into a Go context.Context (Client/Activity side)
Extract(context.Context, HeaderReader) (context.Context, error)
// InjectFromWorkflow writes values from a workflow.Context into headers
InjectFromWorkflow(Context, HeaderWriter) error
// ExtractToWorkflow reads headers into a workflow.Context
ExtractToWorkflow(Context, HeaderReader) (Context, error)
}

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).

Here is a propagator that carries a custom key-value pair from the Client to Workflows and Activities (from the context propagation sample):

ctxpropagation/propagator.go

type (
// contextKey is an unexported type used as key for items stored in the
// Context object
contextKey struct{}

// propagator implements the custom context propagator
propagator struct{}

// Values is a struct holding values
Values struct {
Key string `json:"key"`
Value string `json:"value"`
}
)

// PropagateKey is the key used to store the value in the Context object
var PropagateKey = contextKey{}

// HeaderKey is the key used by the propagator to pass values through the
// Temporal server headers
const HeaderKey = "custom-header"

// NewContextPropagator returns a context propagator that propagates a set of
// string key-value pairs across a workflow
func NewContextPropagator() workflow.ContextPropagator {
return &propagator{}
}

// Inject injects values from context into headers for propagation
func (s *propagator) Inject(ctx context.Context, writer workflow.HeaderWriter) error {
value := ctx.Value(PropagateKey)
payload, err := converter.GetDefaultDataConverter().ToPayload(value)
if err != nil {
return err
}
writer.Set(HeaderKey, payload)
return nil
}

// InjectFromWorkflow injects values from context into headers for propagation
func (s *propagator) InjectFromWorkflow(ctx workflow.Context, writer workflow.HeaderWriter) error {
value := ctx.Value(PropagateKey)
payload, err := converter.GetDefaultDataConverter().ToPayload(value)
if err != nil {
return err
}
writer.Set(HeaderKey, payload)
return nil
}

// Extract extracts values from headers and puts them into context
func (s *propagator) Extract(ctx context.Context, reader workflow.HeaderReader) (context.Context, error) {
if value, ok := reader.Get(HeaderKey); ok {
var values Values
if err := converter.GetDefaultDataConverter().FromPayload(value, &values); err != nil {
return ctx, nil
}
ctx = context.WithValue(ctx, PropagateKey, values)
}

return ctx, nil
}

Register the propagator and set context values

Register the propagator on the Client. Then set context values before starting a Workflow:

ctxpropagation/starter/main.go

// The client is a heavyweight object that should be created once per process.
c, err := client.Dial(client.Options{
HostPort: client.DefaultHostPort,
Interceptors: []interceptor.ClientInterceptor{tracingInterceptor},
ContextPropagators: []workflow.ContextPropagator{ctxpropagation.NewContextPropagator()},
})
if err != nil {
log.Fatalln("Unable to create client", err)
}
defer c.Close()

workflowID := "ctx-propagation_" + uuid.New()
workflowOptions := client.StartWorkflowOptions{
ID: workflowID,
TaskQueue: "ctx-propagation",
}

ctx := context.Background()
ctx = context.WithValue(ctx, ctxpropagation.PropagateKey, &ctxpropagation.Values{Key: "test", Value: "tested"})

we, err := c.ExecuteWorkflow(ctx, workflowOptions, ctxpropagation.CtxPropWorkflow)

You can also register context propagators through a Plugin if you are building a reusable library.

Access propagated values

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:

ctxpropagation/workflow.go

// CtxPropWorkflow workflow definition
func CtxPropWorkflow(ctx workflow.Context) (err error) {
ao := workflow.ActivityOptions{
StartToCloseTimeout: 2 * time.Second, // such a short timeout to make sample fail over very fast
}
ctx = workflow.WithActivityOptions(ctx, ao)

if val := ctx.Value(PropagateKey); val != nil {
vals := val.(Values)
workflow.GetLogger(ctx).Info("custom context propagated to workflow", vals.Key, vals.Value)
}

var values Values
if err = workflow.ExecuteActivity(ctx, SampleActivity).Get(ctx, &values); err != nil {
workflow.GetLogger(ctx).Error("Workflow failed.", "Error", err)
return err
}
workflow.GetLogger(ctx).Info("context propagated to activity", values.Key, values.Value)
workflow.GetLogger(ctx).Info("Workflow completed.")
return nil
}

ctxpropagation/activities.go

func SampleActivity(ctx context.Context) (*Values, error) {
if val := ctx.Value(PropagateKey); val != nil {
vals := val.(Values)
return &vals, nil
}
return nil, nil
}

You can configure multiple context propagators on a single Client, each responsible for its own set of keys.

Context propagation over Nexus

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).

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.

Further reading