Traces
OpenAI Agents Traces
Trace OpenAI Agents runs, tool calls, handoffs, and nested OpenAI model calls.
Pair OpenAI Agents instrumentation with OpenAI instrumentation so nested model
calls are captured. Once setup() runs (pass both modules in TypeScript;
Python auto-detects installed SDKs), the agent run, its tool calls, handoffs,
and nested model calls are all captured automatically. Wrap the run in
agentSpan() / agent_span() when you want stable identity and session
grouping.
Three layers, in increasing order of effort:
setup()traces the whole run automatically (pass the SDK modules in TypeScript; Python auto-detects). Enough on its own.agentSpan()/agent_span()adds a stableagentId,agentName,role, andsessionIdso the Agents dashboard groups runs by conversation, plus your high-level input and output.manualSpan()/manual_span()captures steps the SDK never sees: your own retrieval, routing, or sub-steps inside a tool.
For the full breakdown of when to reach for each, see How Tracing Fits Together.
Install
bun add @inference/tracing openai @openai/agents zodConfigure Export
Set the Inference Tracing endpoint and token before your app starts. setup() reads
these in every example below. Generate a token at
API Keys.
export INFERENCE_OTLP_ENDPOINT="https://telemetry.inference.net"
export INFERENCE_API_KEY="<your-token>"
export INFERENCE_SERVICE_NAME="support-agent"Minimal Setup
Pass the SDK modules to setup() in TypeScript, or let it auto-detect in
Python, then run your agent. The run, any tool calls, and the nested model calls
are all captured with no further code. This is everything you need for a single
one-shot agent.
import { setup } from "@inference/tracing";
import * as agents from "@openai/agents";
import { Agent, run } from "@openai/agents";
import OpenAI from "openai";
const tracing = await setup({
modules: { openai: OpenAI, openaiAgents: agents },
});
const supportAgent = new Agent({
name: "SupportAgent",
instructions: "Help customers with order questions.",
model: "gpt-4o-mini",
});
// No agentSpan needed: the run and its nested model calls are captured
// automatically.
const result = await run(supportAgent, "Where is order ABC-123?");
console.log(result.finalOutput);
await tracing.shutdown();Group Under An Agent
Wrap the run in agentSpan() to give it stable identity and session grouping.
This is the recommended shape for anything beyond a one-shot script. Pass
userId to record user.id so you can filter traces by user on the dashboard,
just like sessionId. For arbitrary keys, attach any custom attributes you want
to filter traces by inside the callback (organization.id, chat.id,
order.id). See
Adding Custom Attributes.
import { agentSpan, setup } from "@inference/tracing";
import * as agents from "@openai/agents";
import { Agent, run, tool } from "@openai/agents";
import OpenAI from "openai";
import { z } from "zod";
const tracing = await setup({
modules: { openai: OpenAI, openaiAgents: agents },
});
const lookupOrder = tool({
name: "lookup_order",
description: "Look up an order by ID.",
parameters: z.object({ orderId: z.string() }),
execute: async ({ orderId }) =>
JSON.stringify({ orderId, status: "shipped", total: 42.5 }),
});
const supportAgent = new Agent({
name: "SupportAgent",
instructions: "Use tools to help customers with order questions.",
tools: [lookupOrder],
model: "gpt-4o-mini",
});
const userMessage = "Where is order ABC-123?";
await agentSpan(
{
agentId: "support-agent",
agentName: "Support Agent",
spanName: "support-agent.run",
sessionId: "conversation-order-abc-123",
userId: "user_8675309",
role: "support",
system: "openai",
},
async (span) => {
span.setInput(userMessage);
// For arbitrary keys, attach whatever your app keys on. Any attribute is
// filterable from the dashboard and CLI, e.g.
// `inf span list --metadata "organization.id=org_42"`.
span.setAttribute("organization.id", "org_42");
const result = await run(supportAgent, userMessage, { maxTurns: 4 });
span.setOutput(String(result.finalOutput ?? ""));
},
);
await tracing.shutdown();Multi-Agent Handoff
Wrap the triage request once. The trace tree groups the triage agent, specialist agent, nested model calls, and tools under the customer request.
import { Agent, handoff, run, tool } from "@openai/agents";
const issueRefund = tool({
name: "issue_refund",
description: "Issue a refund for an order.",
parameters: z.object({ orderId: z.string(), amount: z.number() }),
execute: async ({ orderId, amount }) =>
JSON.stringify({ ok: true, orderId, refundId: "RFD-2201", amount }),
});
const refundsAgent = new Agent({
name: "RefundsAgent",
instructions: "Handle refund requests and use issue_refund.",
tools: [issueRefund],
model: "gpt-4o-mini",
});
const billingAgent = new Agent({
name: "BillingAgent",
instructions: "Answer billing questions. Do not issue refunds.",
model: "gpt-4o-mini",
});
const triageAgent = new Agent({
name: "TriageAgent",
instructions: "Route refund requests to RefundsAgent.",
handoffs: [handoff(refundsAgent), handoff(billingAgent)],
model: "gpt-4o-mini",
});
await agentSpan(
{
agentId: "triage-agent",
agentName: "Triage Agent",
spanName: "triage-agent.run",
sessionId: "conversation-refund-abc-123",
role: "triage",
system: "openai",
},
async (span) => {
const input = "I need a refund for order ABC-123, total $42.50.";
span.setInput(input);
const result = await run(triageAgent, input, { maxTurns: 8 });
span.setOutput(String(result.finalOutput ?? ""));
},
);Use one stable ID for the outer customer-facing run. If you also emit separate
AGENT spans for handoff specialists, give each specialist its own stable
agent.id, such as refunds-agent or billing-agent.
When To Add Manual Spans
The Agents SDK only captures the work it runs for you: the model calls, and the
tools you register with tool(). Anything you do yourself inside the
agentSpan callback is invisible to it. You do not wrap the SDK's own tool
calls in manualSpan(), those are already captured, and doing so would just
produce a duplicate span. You reach for manualSpan() for the steps the SDK
never sees.
Two common cases:
- A step around the run. You fetch context from a vector store before the
run, or validate and reshape the output after it. Neither is an SDK call, so
author a
RETRIEVERorCHAINspan yourself. - Sub-steps inside a tool. The SDK emits one TOOL span per tool
invocation. If that tool's
executedoes something you want broken out (a vector search, a downstream API call, a transform), wrap it in a child span. It nests under the SDK's TOOL span automatically.
import { manualSpan, SpanKindValues } from "@inference/tracing";
// Case 2: a sub-step inside a registered tool. The SDK already opened a TOOL
// span for this call; the RETRIEVER span below nests inside it.
const lookupOrder = tool({
name: "lookup_order",
description: "Look up an order by ID.",
parameters: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => {
const docs = await manualSpan(
{
spanName: "order_docs.search",
spanKind: SpanKindValues.RETRIEVER,
input: { orderId, k: 4 },
},
async (span) => {
const results = await vectorStore.search(orderId, { k: 4 });
span.setOutput(results.map((d) => ({ id: d.id, score: d.score })));
return results;
},
);
return JSON.stringify({ orderId, status: "shipped", context: docs.length });
},
});
await agentSpan(
{
agentId: "support-agent",
agentName: "Support Agent",
spanName: "support-agent.run",
sessionId: "conversation-order-abc-123",
role: "support",
system: "openai",
},
async (span) => {
const userMessage = "Where is order ABC-123?";
span.setInput(userMessage);
// Case 1: a pre-run step the SDK never sees. You classify the request
// yourself before handing it to the agent.
const route = await manualSpan(
{
spanName: "router.classify",
spanKind: SpanKindValues.CHAIN,
input: { userMessage },
},
async (chain) => {
const decision = classifyRequest(userMessage);
chain.setOutput(decision);
return decision;
},
);
const result = await run(supportAgent, userMessage, { maxTurns: 4 });
span.setOutput(String(result.finalOutput ?? ""));
},
);The rule of thumb: if the Agents SDK runs the code (a model call, a registered
tool), it is captured for you. If you run the code (retrieval, routing,
validation, or a sub-step inside a tool), wrap it in manualSpan. See
Manual Spans for the full set of span
kinds and the handle API.
Flushing
Every example above ends with await tracing.shutdown(), which flushes batched
spans before the process exits. Long-lived servers and serverless or edge
runtimes need a different flush strategy, since the process does not exit after
each run. See
Flushing and process lifecycle
for all three shapes.
Next Steps
Manual spans
Author TOOL, CHAIN, and RETRIEVER spans for work the SDK does not capture.
Agent identity
Pick stable agent.id and session.id values for the Agents dashboard.
Production agent example
A production-shaped agent with custom tool spans, end to end.
Attributes reference
Every Attr.* constant and SpanKindValues value the SDK emits.