---
title: "Temporal Tracing"
description: "Trace Temporal workflow and activity spans with Judgment."
sidebar:
  label: "Temporal"
seo:
  title: "Temporal Tracing with Judgment | Integration Docs"
  description: "Trace Temporal spans with Judgment to connect application behavior to production AI agent monitoring and evaluation."
---

Trace a Temporal app by making each activity execution its own trace, grouped
by the workflow ID as the session. Temporal's tracing interceptor is optional;
the final section explains when it is useful.

Read [Durable work](/documentation/tracing/durable-work) for the full
pattern: app roots, sessions, activity boundaries, waits, errors, and flushes.

{/* @test-flow
  id: tracing-task-temporal
  lang: python
  env: JUDGMENT_API_KEY, JUDGMENT_ORG_ID
*/}

**uv**

```bash
uv add judgeval temporalio
```

**pip**

```bash
pip install judgeval temporalio
```

## Activity roots

Initialize the tracer at import time in every process (the producer service
and each worker) before any activity, client, or app object is built. Then
run each activity through an observed helper. Flush after that helper returns,
when its root has ended:

```python title="app/tracing.py"
import os

from judgeval import Tracer

Tracer.init(project_name=os.environ["JUDGMENT_PROJECT_NAME"])
```

```python title="activities.py"
from judgeval import Tracer
from temporalio import activity

import app.tracing  # initializes tracing before the activity is defined


@Tracer.observe(span_type="agent", span_name="app.execute_step")
async def _execute_traced_step(req: StepRequest) -> StepResult:
    info = activity.info()
    Tracer.set_session_id(info.workflow_id)
    Tracer.set_attributes({"app.activity.attempt": info.attempt})
    return await run_step(req)   # model and tool calls happen here


@activity.defn
async def execute_step(req: StepRequest) -> StepResult:
    try:
        return await _execute_traced_step(req)
    finally:
        Tracer.force_flush()
```

Wrap the model client once (`Tracer.wrap(...)`) so model calls appear as
children inside whichever activity root is active. Trace submit and approval
endpoints only when they do app work, such as starting or resuming the run in
their own process. Leave them untraced if they only enqueue or acknowledge a
request. Status polls do not represent agent work, and
workflow code should not create spans because Temporal replays it.

This produces one trace per activity attempt, connected model and tool
children, and a session that groups the whole job across queue and approval
waits. Retries appear as separate traces with distinct attempt numbers.

## Temporal's tracing interceptor (optional)

Temporal's tracing interceptor carries the caller's trace context into later
work. An activity that runs after a queue or approval wait can then attach
under a submit trace that already ended, and its spans can duplicate the
activity roots above. Use the activity roots above as the default Judgment
unit of work. Add the interceptor only when you need its transport spans, then
verify that it does not duplicate activity roots or parent delayed work under a
submit trace that has already ended.

```python title="temporal_tracing.py"
from temporalio.client import Client
from temporalio.contrib.opentelemetry import TracingInterceptor

from judgeval import JudgmentTracerProvider, Tracer

Tracer.init(project_name="temporal_app")
JudgmentTracerProvider.install_as_global_tracer_provider()

client = await Client.connect("localhost:7233", interceptors=[TracingInterceptor()])
```

`TracingInterceptor` comes from `temporalio[opentelemetry]`. If Temporal spans
appear as their own roots for work an activity root already covers, or later
activities attach under an ended submit root, remove the interceptor.
