Skip to main content

Context Propagation - Go SDK

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.

tip

If you want to propagate tracing context, check if there is a built-in tracing interceptor for your library before building a custom context propagator.

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