Skip to main content
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 stable agentId, agentName, role, and sessionId so 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 zod

Configure Export

Set the Catalyst endpoint and token before your app starts. setup() reads these in every example below. Generate a token at API Keys.
export CATALYST_OTLP_ENDPOINT="https://telemetry.inference.net"
export CATALYST_OTLP_TOKEN="<your-token>"
export CATALYST_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:
  1. 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 RETRIEVER or CHAIN span yourself.
  2. Sub-steps inside a tool. The SDK emits one TOOL span per tool invocation. If that tool’s execute does 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.