> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://respan.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://respan.ai/docs/_mcp/server.

# Temporal (tracing)

> Trace Temporal Python workflows, activities, signals, queries, and updates with Respan.

[Temporal](https://temporal.io/) 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.

#### Set up Respan

Create an account at [platform.respan.ai](https://platform.respan.ai) and grab an [API key](https://platform.respan.ai/platform/gateway/api-keys).

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

#### Example projects

* [Complete Python example suite](https://github.com/respanai/respan-example-projects/tree/main/python/tracing/temporal)
* [Workflow and remote activity](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/01_workflow_and_activity.py)
* [Signals and queries](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/02_signals_queries_and_updates.py)
* [Child workflow, local activity, and expected error](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/03_child_local_activity_and_error.py)
* [Workflow update](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/04_workflow_update.py)

## Setup

#### Install packages

```bash
pip install respan-ai respan-instrumentation-temporal temporalio
```

#### Set environment variables

```bash
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()`.

#### 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`**

```python Python
import asyncio
from datetime import timedelta

from respan import Respan
from respan_instrumentation_temporal import TemporalInstrumentor
from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.worker import Worker


@activity.defn
async def compose_greeting(name: str) -> str:
    return f"Hello, {name}!"


@workflow.defn
class GreetingWorkflow:
    @workflow.run
    async def run(self, name: str) -> str:
        return await workflow.execute_activity(
            compose_greeting,
            name,
            start_to_close_timeout=timedelta(seconds=10),
        )


async def main() -> None:
    instrumentor = TemporalInstrumentor()
    respan = Respan(instrumentations=[instrumentor])

    try:
        # The Respan interceptor is injected automatically here.
        client = await Client.connect("localhost:7233")

        async with Worker(
            client,
            task_queue="greeting-task-queue",
            workflows=[GreetingWorkflow],
            activities=[compose_greeting],
        ):
            result = await client.execute_workflow(
                GreetingWorkflow.run,
                "Ada",
                id="greeting-workflow-1",
                task_queue="greeting-task-queue",
            )
            print(result)
    finally:
        respan.shutdown()


asyncio.run(main())
```

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

#### View your trace

Open the [Traces page](https://platform.respan.ai/platform/traces) 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.

```python
from respan import Respan
from respan_instrumentation_temporal import TemporalInstrumentor
from temporalio.client import Client

instrumentor = TemporalInstrumentor()
respan = Respan(instrumentations=[instrumentor])

client = 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`**

```python Python
from respan import Respan
from respan_instrumentation_temporal import TemporalInstrumentor
from temporalio.testing import WorkflowEnvironment

instrumentor = TemporalInstrumentor()
respan = Respan(instrumentations=[instrumentor])

environment = await WorkflowEnvironment.start_time_skipping(
    interceptors=[instrumentor.interceptor]
)

try:
    client = environment.client
    # Create a Worker with this client, then start or execute workflows.
finally:
    await environment.shutdown()
    respan.shutdown()
```

The [Temporal example suite](https://github.com/respanai/respan-example-projects/tree/main/python/tracing/temporal) 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.

| Area               | Captured operations                                                                     |
| ------------------ | --------------------------------------------------------------------------------------- |
| Workflow lifecycle | Client workflow start and signal-with-start, workflow run, and workflow completion      |
| Activities         | Activity scheduling plus remote and local activity execution                            |
| Messages           | Client signals and queries, workflow signal handlers, and workflow query handlers       |
| Updates            | Client workflow-update start, update-with-start, update validation, and update handling |
| Child workflows    | Child-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

| Parameter                      | Type   | Default | Description                                                                                                                  |
| ------------------------------ | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `capture_content`              | `bool` | `True`  | Capture workflow/activity arguments and Temporal identifiers when the intercepted operation exposes them.                    |
| `always_create_workflow_spans` | `bool` | `False` | Create workflow-side spans even when no client-created parent span is available. This can produce orphan spans after replay. |
| `max_attribute_chars`          | `int`  | `16000` | Maximum serialized size for canonical input and output attributes. Values below `512` are raised to `512`.                   |

### Respan options

| Parameter             | Type           | Default | Description                                                                |
| --------------------- | -------------- | ------- | -------------------------------------------------------------------------- |
| `api_key`             | `str \| None`  | `None`  | Falls back to the `RESPAN_API_KEY` environment variable.                   |
| `base_url`            | `str \| None`  | `None`  | Falls back to the `RESPAN_BASE_URL` environment variable.                  |
| `instrumentations`    | `list`         | `[]`    | Plugin instrumentations to activate, for example `TemporalInstrumentor()`. |
| `customer_identifier` | `str \| None`  | `None`  | Default customer identifier for all spans.                                 |
| `metadata`            | `dict \| None` | `None`  | Default metadata attached to all spans.                                    |
| `environment`         | `str \| None`  | `None`  | Environment 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:

```python
from respan import Respan
from respan_instrumentation_temporal import TemporalInstrumentor

respan = Respan(
    instrumentations=[TemporalInstrumentor(capture_content=False)]
)
```

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:

```python
instrumentor = TemporalInstrumentor()
respan = Respan(instrumentations=[instrumentor])

try:
    client = await Client.connect("localhost:7233")
    # Run workers and workflows.
finally:
    respan.shutdown()
```

## More examples

* [Workflow and remote activity](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/01_workflow_and_activity.py)
* [Signals and queries](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/02_signals_queries_and_updates.py)
* [Child workflow, local activity, content disabled, and expected failure](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/03_child_local_activity_and_error.py)
* [Workflow update and signal completion](https://github.com/respanai/respan-example-projects/blob/main/python/tracing/temporal/04_workflow_update.py)