Skip to main content
If you’re using agentex agents on Temporal, you don’t need this page — agentex provides its own SGP tracing integration through its tracing processors. This guide is for users writing their own Temporal workflows or activities that emit SGP spans directly.
Temporal workflows are deterministic: workflow code is replayed from history on every worker, so it cannot perform network IO, generate non-deterministic UUIDs, or read system time directly. This makes integrating any tracing SDK with Temporal a question of where the tracing calls live. Activities, by contrast, are regular Python functions running on a worker. They are free to do network IO, so the SGP Tracing SDK works inside an activity exactly like in any other process. This guide covers the two patterns that come up in practice. Use this when “one activity = one span” gives you the granularity you need. It is the simplest and matches how most Temporal applications are observed.
  • Activities call tracing.create_span(...) directly using the SDK’s context manager.
  • The caller of the workflow creates the parent span and passes trace_id and parent_span_id as workflow input.
  • The workflow forwards those IDs to each activity. Each activity span is rooted under the caller’s parent.
The caller starts the workflow inside an existing span, passing the IDs through workflow input:
Use workflow.uuid4() (Temporal’s deterministic UUID helper), not uuid.uuid4(), when generating IDs inside workflow code. Standard library uuid.uuid4() is non-deterministic and will break workflow replay.
The SDK’s background queue worker is a daemon thread inside the activity worker process, not the workflow. It is fine to use the default tracing.init() configuration. The queue flushes on size or cadence triggers and on worker shutdown.

Pattern 2: Buffer in workflow, flush via activity (advanced)

Use this when you want span boundaries that mirror workflow steps rather than activity invocations, or when activity calls are too coarse-grained to capture the hierarchy you want. The shape:
  • The workflow buffers lightweight span dicts (the SDK’s Item TypedDict) in workflow state. No network IO, no SDK calls.
  • A dedicated flush_spans activity receives the buffered list and writes them via client.spans.upsert_batch(items=...).
  • The flush activity runs:
    • when the buffer hits a size threshold,
    • on a periodic timer,
    • before the workflow completes or calls continue_as_new.
Item is a TypedDict, so values are plain dict at runtime. That means no asdict(...) conversion before the flush activity, and Temporal’s default data converter serializes them as JSON cleanly.
A non-retryable failure (any error class listed in non_retryable_error_types) drops the buffered batch. The data is gone — there is no second chance for those spans. Choose the non-retryable list deliberately: list only errors where retrying truly cannot succeed (bad input, auth) and let everything else retry.

Why upsert_batch and not create

  • Idempotency. Temporal retries activities on transient failures. upsert_batch is keyed on the client-supplied span id, so a retry that partially succeeded will re-converge to the right state. client.spans.create is not safe under retry because a partial success leaves orphan rows.
  • Throughput. One HTTP call carries many spans. The batch endpoint accepts up to 1000 items per request.
Keep batches at 200 to 500 items per flush in practice. That keeps payloads under 1 MB and HTTP timeouts comfortable, and gives you headroom under the 1000-item server cap.

Span integrity discipline

The server rejects the whole batch if any child’s trace_id does not match its parent’s. This is easy to trip if parent IDs are minted in workflow code but child IDs are minted in an activity without the trace context piped through. Always pass trace_id (and parent_span_id where relevant) explicitly when crossing the workflow/activity boundary.

Distributed tracing across workflows

If one workflow starts another (child workflow, or an external start_workflow call), propagate trace_id and the originating span’s id via workflow input or memo, and use them in the child workflow as parent_id for its root span. The same propagation rule from the Distributed Tracing page applies; Temporal’s workflow-input mechanism is a clean place to carry it.