Trace OpenAI Agents JS runs with OpenInference and send spans to Arize AX for LLM observability.
OpenAI Agents JS is OpenAI’s TypeScript framework for building agents — instructions, tools, handoffs, guardrails, and the run function that drives them. Arize AX captures every Agents JS run — agent invocations, tool calls, handoffs, guardrails, and the underlying LLM calls — via the @arizeai/openinference-instrumentation-openai-agents package, which registers as a first-class TracingProcessor against the SDK.
// instrumentation.tsimport { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";import { resourceFromAttributes } from "@opentelemetry/resources";import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";import { SEMRESATTRS_PROJECT_NAME,} from "@arizeai/openinference-semantic-conventions";import { OpenAIAgentsInstrumentation,} from "@arizeai/openinference-instrumentation-openai-agents";import * as agents from "@openai/agents";const projectName = process.env.ARIZE_PROJECT_NAME ?? "openai-agents-js-tracing-example";export const provider = new NodeTracerProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: projectName, [SEMRESATTRS_PROJECT_NAME]: projectName, }), spanProcessors: [ new SimpleSpanProcessor( new OTLPTraceExporter({ url: "https://otlp.arize.com/v1/traces", headers: { "arize-space-id": process.env.ARIZE_SPACE_ID ?? "", "arize-api-key": process.env.ARIZE_API_KEY ?? "", }, }), ), ],});provider.register();const instrumentation = new OpenAIAgentsInstrumentation({ tracerProvider: provider,});instrumentation.manuallyInstrument(agents);console.log("Arize AX tracing initialized for OpenAI Agents JS.");
The instrumentor implements the Agents SDK’s first-class TracingProcessor interface rather than monkey-patching imports, so manuallyInstrument(agents) must receive the same @openai/agents namespace your application code imports. Pass exclusiveProcessor: false to the constructor to run alongside the SDK’s default OpenAI tracing exporter instead of replacing it.
// example.ts// Importing instrumentation first ensures tracing is set up before the// Agents SDK is used.import { provider } from "./instrumentation";import { Agent, run } from "@openai/agents";// The agent reads OPENAI_API_KEY from the environment.const agent = new Agent({ name: "Assistant", instructions: "You are a concise factual assistant.", model: "gpt-5.5",});const result = await run( agent, "Why is the ocean salty? Answer in two sentences.",);console.log(result.finalOutput);// Flush any pending spans before the process exits.await provider.forceFlush();
Arize AX tracing initialized for OpenAI Agents JS.The ocean is salty because rivers continuously dissolve mineral salts from rocks and soil and carry them to the sea, where they accumulate over millions of years. Water leaves the ocean through evaporation but the salts remain, steadily concentrating until reaching today's roughly 3.5% salinity.
Open your Arize AX space and select project openai-agents-js-tracing-example.
You should see a new trace within ~30 seconds with this shape: an Agent workflow root span (AGENT) wraps an Assistant agent span (AGENT) and a response LLM child span (model gpt-5.5) with the prompt, response, and token usage attached. Tool, handoff, and guardrail spans appear when the agent invokes them.
Confirm spans are actually reaching your Arize AX project. Use whichever fits your workflow — the skill and CLI work for any framework; the SDK check is shown for each language.
Arize skill (agent)
AX CLI
SDK
Install the Arize Skills plugin and let your coding agent check for you:
npx skills add Arize-ai/arize-skills
Then prompt your agent:
Use the arize-trace skill to export and analyze recent traces from my project. Confirm spans are arriving, and summarize any errors or latency issues.
Export recent spans for your project — any rows mean traces are landing:
The basic setup above exports every span the tracer provider receives. That’s fine for a standalone Node script — nothing else is emitting through the provider. In Next.js / Vercel / NestJS / any runtime that ships its own OpenTelemetry auto-instrumentation, those instrumentations emit POST / GET spans for every fetch, every page render, every API route, and the Agents SDK spans nest under those HTTP roots inside your AX project. A single chat turn can balloon into 30+ unrelated infra spans.OpenInference-tagged spans all carry an openinference.span.kind attribute that the @arizeai/openinference-instrumentation-openai-agents processor sets — checking for it is enough to drop non-OpenInference spans. Swap SimpleSpanProcessor in instrumentation.ts for a filtering subclass:
import { ReadableSpan, SimpleSpanProcessor,} from "@opentelemetry/sdk-trace-base";import { SemanticConventions } from "@arizeai/openinference-semantic-conventions";function isOpenInferenceSpan(span: ReadableSpan): boolean { return ( typeof span.attributes[SemanticConventions.OPENINFERENCE_SPAN_KIND] === "string" );}class OpenInferenceFilteringSpanProcessor extends SimpleSpanProcessor { onEnd(span: ReadableSpan): void { if (!isOpenInferenceSpan(span)) return; super.onEnd(span); }}// then in spanProcessors:new OpenInferenceFilteringSpanProcessor( new OTLPTraceExporter({ /* ... */ }),);
This is the same predicate @arizeai/openinference-vercel exports as isOpenInferenceSpan — inlined here so a non-Vercel app doesn’t have to depend on a Vercel-specific package.The trade-off: filtering removes the HTTP root spans, which orphans the surviving Agents SDK spans on the Traces tab (no parent to anchor them) — they remain visible on the Spans tab. If you also need a clean trace tree on the Traces tab, swap the filter for a span processor that promotes the SDK’s top-level Agent workflow span to root by clearing its parent ID:
// root-aware-processor.tsimport { Context } from "@opentelemetry/api";import { BatchSpanProcessor, ReadableSpan, Span, SpanExporter,} from "@opentelemetry/sdk-trace-base";import { getSession } from "@arizeai/openinference-core";import { SemanticConventions, SESSION_ID,} from "@arizeai/openinference-semantic-conventions";import { LRUCache } from "lru-cache";// Top-level OpenAI Agents SDK span names. `Agent workflow` is the// implicit root the SDK creates for every `run()` invocation; individual// agent runs surface as spans named after `Agent.name` (e.g. `Assistant`,// `Researcher`). Match on the first whitespace-delimited token so suffixed// run names (e.g. `Agent workflow some-trace-name`) still hit.const ROOT_OI_SPAN_PREFIXES = [ "Agent workflow",];function isRootOISpanByName(spanName: string): boolean { const head = spanName.split(" ")[0]; return ROOT_OI_SPAN_PREFIXES.some( (prefix) => spanName === prefix || spanName.startsWith(prefix + " ") || head === prefix.split(" ")[0], );}function isOpenInferenceSpan(span: ReadableSpan): boolean { return ( typeof span.attributes[SemanticConventions.OPENINFERENCE_SPAN_KIND] === "string" );}interface RootAwareConfig { exporter: SpanExporter; /** LRU size for tracking which traces have a promoted root. */ cacheSize?: number;}/** * Filters non-OpenInference spans (HTTP, fetch, etc.) and promotes the * first `Agent workflow` span in each trace to root by clearing its * parent IDs. Also propagates session ids from context onto every * emitted span. */export class RootAwareOpenInferenceProcessor extends BatchSpanProcessor { private traceIds: LRUCache<string, boolean>; constructor(config: RootAwareConfig) { super(config.exporter); this.traceIds = new LRUCache({ max: config.cacheSize ?? 1000 }); } onStart(span: Span, parentContext: Context): void { const session = getSession(parentContext); if (session?.sessionId) { span.setAttribute(SESSION_ID, session.sessionId); } const traceId = span.spanContext().traceId; if ( isRootOISpanByName(span.name) && !this.traceIds.has(traceId) ) { // parentSpanId is readonly on the public Span type; cast to clear. (span as unknown as { parentSpanId?: string }).parentSpanId = undefined; (span as unknown as { parentSpanContext?: unknown }) .parentSpanContext = undefined; this.traceIds.set(traceId, true); } super.onStart(span, parentContext); } onEnd(span: ReadableSpan): void { if (!isOpenInferenceSpan(span)) return; super.onEnd(span); } shutdown(): Promise<void> { this.traceIds.clear(); return super.shutdown(); }}
Wire it in by replacing the SimpleSpanProcessor (or OpenInferenceFilteringSpanProcessor) in instrumentation.ts:
import { RootAwareOpenInferenceProcessor } from "./root-aware-processor";spanProcessors: [ new RootAwareOpenInferenceProcessor({ exporter: new OTLPTraceExporter({ url: "https://otlp.arize.com/v1/traces", headers: { "arize-space-id": process.env.ARIZE_SPACE_ID ?? "", "arize-api-key": process.env.ARIZE_API_KEY ?? "", }, }), }),],
The recipe needs two additional dependencies: npm install @arizeai/openinference-core lru-cache.
No traces in Arize AX. Confirm ARIZE_SPACE_ID and ARIZE_API_KEY are set in the same shell that runs example.ts. Enable OpenTelemetry debug logs with export OTEL_LOG_LEVEL=debug and re-run.
Agents spans missing.instrumentation.manuallyInstrument(agents) must run before Agent is constructed or run is called. Make sure import { provider } from "./instrumentation" is the first import in your entry point, and that the same @openai/agents namespace passed to manuallyInstrument(...) is the one your application code imports.
401 from OpenAI. Verify OPENAI_API_KEY is set and has access to the default model. Swap model: "gpt-5.5" for a model your key can call.
Spans still going to the default OpenAI tracing exporter. The instrumentor registers exclusively by default. If you need to keep the SDK’s built-in exporter alongside Arize AX, pass new OpenAIAgentsInstrumentation({ tracerProvider: provider, exclusiveProcessor: false }).
Process exits before spans flush. Spans are exported asynchronously (more so when you swap in the RootAwareOpenInferenceProcessor below, which batches); always await provider.forceFlush() (or provider.shutdown()) before the process exits to avoid losing trailing spans.
Tool / handoff / guardrail spans expected but not present. These spans only emit when the agent actually invokes the corresponding feature. The minimal example above has none — add tools, handoffs, or inputGuardrails to the Agent constructor to see them.
AX project is full of GET /, POST /api/chat, or other infra spans. Your runtime’s auto-OTel (Next.js, Vercel, NestJS, …) is piping HTTP / fetch / page-render spans through the global provider. See Span filter above for the OpenInferenceFilteringSpanProcessor recipe (and the RootAwareOpenInferenceProcessor if you also need a clean trace tree on the Traces tab). In Next.js apps an additional defence is to skip provider.register() entirely and pass the provider directly to new OpenAIAgentsInstrumentation({ tracerProvider }) — the instrumentor resolves its tracer off the provider you hand it, so global registration isn’t needed.
Agents SDK spans orphaned on the Traces tab. Expected when the OpenInferenceFilteringSpanProcessor is the only processor — see Span filter for the RootAwareOpenInferenceProcessor recipe that promotes Agent workflow to root.