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 tenant IDs, auth tokens, or other request-scoped metadata.
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
- Register a context propagator on the Client via
ContextPropagatorsin ClientOptions - Inject - On outbound calls, the SDK calls
Inject(fromcontext.Context) orInjectFromWorkflow(fromworkflow.Context) to serialize values into Temporal headers - Extract - On inbound calls, the SDK calls
Extract(intocontext.Context) orExtractToWorkflow(intoworkflow.Context) to deserialize headers back into the context - 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):
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:
// 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
}
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
- Passing Context with Temporal - A conceptual guide to middleware and walkthrough of building a context propagator in Go