Temporal (tracing)

Temporal is a durable execution platform for long-running applications. Respan instruments the Temporal Python SDK through Temporal’s official replay-aware OpenTelemetry interceptor, preserving Temporal context propagation and replay behavior while emitting canonical Respan workflow and task spans.

Create an account at platform.respan.ai and grab an API key.

Run npx @respan/cli setup to set up with your coding agent.

Setup

1

Install packages

$pip install respan-ai respan-instrumentation-temporal temporalio
2

Set environment variables

$export RESPAN_API_KEY="YOUR_RESPAN_API_KEY"

Set RESPAN_BASE_URL only when exporting to a custom Respan endpoint. The Temporal client address is configured separately when you call Client.connect().

3

Initialize and run

Initialize Respan before connecting the Temporal client. TemporalInstrumentor automatically appends its interceptor to Client.connect(), and workers created from that client inherit the interceptor.

Python
1import asyncio
2from datetime import timedelta
3
4from respan import Respan
5from respan_instrumentation_temporal import TemporalInstrumentor
6from temporalio import activity, workflow
7from temporalio.client import Client
8from temporalio.worker import Worker
9
10
11@activity.defn
12async def compose_greeting(name: str) -> str:
13 return f"Hello, {name}!"
14
15
16@workflow.defn
17class GreetingWorkflow:
18 @workflow.run
19 async def run(self, name: str) -> str:
20 return await workflow.execute_activity(
21 compose_greeting,
22 name,
23 start_to_close_timeout=timedelta(seconds=10),
24 )
25
26
27async def main() -> None:
28 instrumentor = TemporalInstrumentor()
29 respan = Respan(instrumentations=[instrumentor])
30
31 try:
32 # The Respan interceptor is injected automatically here.
33 client = await Client.connect("localhost:7233")
34
35 async with Worker(
36 client,
37 task_queue="greeting-task-queue",
38 workflows=[GreetingWorkflow],
39 activities=[compose_greeting],
40 ):
41 result = await client.execute_workflow(
42 GreetingWorkflow.run,
43 "Ada",
44 id="greeting-workflow-1",
45 task_queue="greeting-task-queue",
46 )
47 print(result)
48 finally:
49 respan.shutdown()
50
51
52asyncio.run(main())

Run a Temporal development server or point Client.connect() at your Temporal deployment before starting this example.

4

View your trace

Open the Traces page to see the workflow lifecycle, activity execution, and Temporal client operations in one trace tree.

How automatic injection works

Passing TemporalInstrumentor() to Respan activates a patch on temporalio.client.Client.connect. Every client connected after initialization receives instrumentor.interceptor; workers created with that client use the same interceptor and preserve trace context across Temporal boundaries.

1from respan import Respan
2from respan_instrumentation_temporal import TemporalInstrumentor
3from temporalio.client import Client
4
5instrumentor = TemporalInstrumentor()
6respan = Respan(instrumentations=[instrumentor])
7
8client = await Client.connect("localhost:7233")

If the interceptors argument already contains a Temporal TracingInterceptor, Respan does not append a second tracing interceptor. This prevents duplicate spans.

Initialize Respan before the first Client.connect() call. Clients created before activation are not retrofitted.

Explicit interceptor wiring

Temporal test environments and custom client factories expose an explicit interceptors list. Use the same interceptor instance through instrumentor.interceptor:

Python
1from respan import Respan
2from respan_instrumentation_temporal import TemporalInstrumentor
3from temporalio.testing import WorkflowEnvironment
4
5instrumentor = TemporalInstrumentor()
6respan = Respan(instrumentations=[instrumentor])
7
8environment = await WorkflowEnvironment.start_time_skipping(
9 interceptors=[instrumentor.interceptor]
10)
11
12try:
13 client = environment.client
14 # Create a Worker with this client, then start or execute workflows.
15finally:
16 await environment.shutdown()
17 respan.shutdown()

The Temporal example suite uses this wiring with Temporal’s official time-skipping test server, so it does not require an external Temporal cluster. The first run downloads the official test-server binary.

Captured operations

The instrumentor adapts the operations emitted by Temporal’s official Python tracing interceptor.

AreaCaptured operations
Workflow lifecycleClient workflow start and signal-with-start, workflow run, and workflow completion
ActivitiesActivity scheduling plus remote and local activity execution
MessagesClient signals and queries, workflow signal handlers, and workflow query handlers
UpdatesClient workflow-update start, update-with-start, update validation, and update handling
Child workflowsChild-workflow start plus child and external workflow signals

Operation names include the workflow, activity, signal, query, or update type when Temporal exposes it. Workflow lifecycle operations are classified as Respan workflow spans; the other Temporal operations are classified as task spans.

Workflow-side spans follow Temporal’s replay safeguards. By default, the official interceptor creates them only when an overarching client span is present. See always_create_workflow_spans below if workflows are started from schedules, the CLI, or another source without that parent context.

Configuration

TemporalInstrumentor options

ParameterTypeDefaultDescription
capture_contentboolTrueCapture workflow/activity arguments and Temporal identifiers when the intercepted operation exposes them.
always_create_workflow_spansboolFalseCreate workflow-side spans even when no client-created parent span is available. This can produce orphan spans after replay.
max_attribute_charsint16000Maximum serialized size for canonical input and output attributes. Values below 512 are raised to 512.

Respan options

ParameterTypeDefaultDescription
api_keystr | NoneNoneFalls back to the RESPAN_API_KEY environment variable.
base_urlstr | NoneNoneFalls back to the RESPAN_BASE_URL environment variable.
instrumentationslist[]Plugin instrumentations to activate, for example TemporalInstrumentor().
customer_identifierstr | NoneNoneDefault customer identifier for all spans.
metadatadict | NoneNoneDefault metadata attached to all spans.
environmentstr | NoneNoneEnvironment tag, for example "production".

Content controls

Content capture is enabled by default. It can include operation arguments and fields such as workflow or activity type, workflow ID, update ID, and task queue when Temporal exposes those values to the interceptor. Temporal headers and RPC metadata are never captured.

Disable content capture when arguments or identifiers can contain sensitive data:

1from respan import Respan
2from respan_instrumentation_temporal import TemporalInstrumentor
3
4respan = Respan(
5 instrumentations=[TemporalInstrumentor(capture_content=False)]
6)

With content capture disabled, spans retain the stable operation and workflow/activity type needed to inspect the trace, but omit arguments and Temporal IDs. Error details are reduced to the exception class rather than the exception message.

Errors and lifecycle

Successful Temporal operations receive backend-visible success status. Failed operations receive OpenTelemetry error status together with status_code and error.message, and their canonical output records an error result. When capture_content=False, the error message contains only the exception class.

activate() and deactivate() are idempotent and safe across multiple instrumentor instances. The Client.connect() patch remains active until the last active Temporal instrumentor is deactivated. Existing clients retain the interceptor they received at connection time.

When Respan owns the instrumentor lifecycle, call respan.shutdown() during application shutdown to flush pending telemetry and deactivate instrumentation:

1instrumentor = TemporalInstrumentor()
2respan = Respan(instrumentations=[instrumentor])
3
4try:
5 client = await Client.connect("localhost:7233")
6 # Run workers and workflows.
7finally:
8 respan.shutdown()

More examples