traceparent header with the request. If your agent extracts that header and uses it as the parent context for its own spans, all the agent’s tracing — LLM calls, tool calls, retrieval steps — nests under the experiment-run trace in the Arize UI.
This page covers what your agent needs to do.
What you need to do
Two things, in order:- Extract
traceparentfrom the incoming request and use it as the parent context for the top-level span you create. - Use
register_with_routing()andset_routing_context()so spans land in the right space and project per-request, instead of the static one configured at app startup.
How Arize propagates context
Every agent-experiment request includes these headers:traceparentis W3C standard — links your spans to a specific Arize-side parent.baggagecarries the space_id and project_name Arize wants your spans routed to. Both fields are also available in the request body asarize_metadata.space_idandarize_metadata.project_name.
Extracting the parent context
OpenTelemetry’spropagate.extract() handles this in one line. Pass the incoming request headers, get back a Context you attach before creating your top-level span.
Don’t pass
context=parent_ctx directly to start_as_current_span() if you’re also using set_routing_context() (next section). Doing so overrides the routing context, and Arize will drop the span. Always attach() first, layer routing on top, then start the span with no explicit context override.Per-request space and project routing
The standardarize.otel.register() call locks your TracerProvider to a single space and project at app startup. That’s wrong for an agent endpoint that one space’s experiment runner might call, then another space’s might call a minute later.
Use register_with_routing() instead — it leaves routing unset at startup, and you decide per-request via set_routing_context():
/invoke handler, resolve the space and project from the request, and wrap the span creation:
Both
space_id and project_name must be set inside set_routing_context(), or arize-otel drops the span entirely. If either is missing in arize_metadata, fall back to env defaults or use the request’s baggage header — but never call set_routing_context() with None.What the final trace looks like
When both pieces are in place, the trace tree in Arize looks like:Subprocess agents (Claude Agent SDK, OpenAI Agents SDK CLI mode, etc.)
If your agent runs the LLM calls in a subprocess (the Claude Agent SDK’s bundled CLI, for example), the auto-instrumentor in your parent Python process won’t see those API calls — they happen in another process. Two options:- Accept it. Your CHAIN + TOOL spans still capture the orchestration layer, which is usually what you care about for experiments. Per-turn LLM details just won’t be present.
- Switch to in-process LLM calls. Replace the SDK with direct
anthropic/openaiSDK calls and your own loop. You lose the SDK’s harness, but full LLM tracing works.
Where routing values come from
Arize sends the same routing information in three places. Use whichever fits your code:| Source | Where | Notes |
|---|---|---|
| Request body | arize_metadata.space_id, arize_metadata.project_name | Easiest — parse the JSON body. |
| Baggage header | baggage: arize.space_id=...,arize.project_name=... | Use OTel’s baggage API to read. |
| Custom headers | x-arize-experiment-id, x-arize-experiment-run-id | For experiment/run linkage, not space routing. |
arize_metadata first, then fall back to baggage, then fall back to env defaults. The example above shows the recommended priority order.
Validating context propagation
After deploying your traced agent, run one experiment row and check both sides:- In the Experiments view, open the run and copy the
trace_idfrom the experiment span. - In the Traces view, search for that
trace_id. You should see bothagent.experiment.run(from Arize) andrun_agent(from your agent), withrun_agent’s parent_id pointing atagent.experiment.run. - Tool spans should be children of
run_agent. If they appear as direct children ofagent.experiment.runinstead, your CHAIN span isn’t getting exported — usually a routing context issue (see warning above).
Common pitfalls
Spans appear with a different trace_id than the experiment run
Spans appear with a different trace_id than the experiment run
You’re not extracting
traceparent from the incoming request. Add the propagate.extract() step shown above.CHAIN span shows up but tool spans don't (or vice versa)
CHAIN span shows up but tool spans don't (or vice versa)
You’re starting spans both inside and outside the
set_routing_context() block. Move all span creation inside it.The CHAIN span is missing, leaving tool spans 'orphaned'
The CHAIN span is missing, leaving tool spans 'orphaned'
You probably passed
context=parent_ctx directly to start_as_current_span() and used set_routing_context(). The explicit context= override drops the routing attributes from the new span, and arize-otel filters it out. Fix: use attach() to make parent_ctx current first, then start the span normally.`Failed to export traces, StatusCode.UNAVAILABLE`
`Failed to export traces, StatusCode.UNAVAILABLE`
Next
Running agent experiments
Pick a dataset, launch a run, and compare across config variants.