homeresume
 
   

Building AI agents with OpenAI Agents SDK

Published June 12, 2026Last updated June 12, 20267 min read

The OpenAI Agents SDK (@openai/agents) is OpenAI's official framework for agentic apps in TypeScript. It provides a small set of primitives: Agent, tools, handoffs, guardrails, and a run loop managed by run().

This post builds the same support triage agent as the Building AI agents with Vercel AI SDK post - lookup customers and invoices, search a knowledge base, then create a ticket or escalate - but uses the OpenAI SDK instead of the Vercel tool loop.

For lower-level API access, see the OpenAI Responses API post. For the Vercel AI SDK alternative (generateText, stopWhen, stepCountIs), see the Vercel AI SDK agents post.

Prerequisites

  • OpenAI account
  • Generated API key
  • Enabled billing
  • Node.js version 26
  • @openai/agents and zod installed (npm i @openai/agents zod)
  • OPENAI_API_KEY set in the environment

Mental model - turns and the agent loop

A turn is one model generation. In that turn the model either:

  • returns final output (the run ends), or
  • returns tool calls (the SDK executes them and starts another turn with the results), or
  • requests a handoff to another agent (control switches, history is preserved, loop continues)

Typical flow for the support triage agent: user question → model calls lookup tools (get_customer, get_invoice, search_knowledge_base) → model creates a ticket or escalates → final answer.

maxTurns: 8 means “stop after 8 turns” (eight model generations), not eight individual tool calls. A single turn can include multiple parallel tool calls.

When you omit maxTurns, the SDK defaults to 10 as a safety cap.

Support triage scenario

Example prompt:

Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?

A realistic chain:

  1. get_customer - plan tier, open ticket count
  2. get_invoice - amount, status, payment IDs
  3. search_knowledge_base - duplicate-charge and refund policy
  4. create_support_ticket or escalate_to_human - write action or escalation

The demo uses in-memory fixtures (customers, invoices, knowledge-base articles) so scripts run without a database.

Defining multiple tools

Register tools with tool() and Zod parameters. Clear description values help the model pick the right tool.

import { tool } from '@openai/agents';
import { z } from 'zod';
const getCustomer = tool({
name: 'get_customer',
description: 'Look up a customer account by ID',
parameters: z.object({
customerId: z.string().describe('Customer ID, e.g. cus_1042'),
}),
execute: async ({ customerId }) => {
const customer = customers.find((item) => item.id === customerId);
if (!customer) {
return { found: false, customerId, error: 'Customer not found' };
}
return { found: true, customer };
},
});
const getInvoice = tool({
name: 'get_invoice',
description: 'Look up an invoice by ID, including payment IDs and status',
parameters: z.object({
invoiceId: z.string().describe('Invoice ID, e.g. inv_8891'),
}),
execute: async ({ invoiceId }) => {
const invoice = invoices.find((item) => item.id === invoiceId);
if (!invoice) {
return { found: false, invoiceId, error: 'Invoice not found' };
}
return { found: true, invoice };
},
});
const searchKnowledgeBase = tool({
name: 'search_knowledge_base',
description: 'Search internal support articles by keyword',
parameters: z.object({
query: z.string().describe('Search terms, e.g. duplicate charge refund'),
}),
execute: async ({ query }) => {
// keyword match against mocked articles
return { query, articles: matches };
},
});

Add write tools for outcomes:

const createSupportTicket = tool({
name: 'create_support_ticket',
description: 'Create a support ticket after gathering customer and policy context',
parameters: z.object({
customerId: z.string(),
subject: z.string().min(3),
priority: z.enum(['low', 'medium', 'high']),
summary: z.string().min(10),
}),
execute: async (input) => {
const ticket = createTicket(input);
return { created: true, ticket };
},
});
const escalateToHuman = tool({
name: 'escalate_to_human',
description: 'Escalate when policy requires manual review',
parameters: z.object({
customerId: z.string(),
reason: z.string().min(10),
urgency: z.enum(['normal', 'high']),
}),
execute: async (input) => ({
escalated: true,
queue: input.urgency === 'high' ? 'billing-urgent' : 'billing-standard',
...input,
}),
});

Return structured objects from execute. The SDK serializes them as tool results for the next turn. Return explicit errors (for example { found: false, error: '...' }) so the model can recover instead of throwing.

Running an agent

Define an Agent with instructions and tools, then call run():

import { Agent, run } from '@openai/agents';
const agent = new Agent({
name: 'Support Triage',
model: 'gpt-5.5',
instructions: `You are a billing support triage agent.
- Look up customer and invoice before recommending refunds.
- Search the knowledge base for policy guidance.
- Create a ticket when you can resolve within policy.
- Call escalate_to_human when manual review is required.`,
tools: [
getCustomer,
getInvoice,
searchKnowledgeBase,
createSupportTicket,
escalateToHuman,
],
});
const result = await run(
agent,
'Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?',
{ maxTurns: 8 },
);
console.log(result.finalOutput);

Use a model that supports tool calling.

maxTurns - cap the number of turns

maxTurns(n) stops once the run reaches n turns. Use it on every production agent to prevent runaway loops and unbounded API cost. When the cap is exceeded, the SDK throws MaxTurnsExceededError.

Use caseSuggested cap
Single tool, then answer2
Chat with occasional tool use3–5
Task agents (triage, research)8–15
Long autonomous workflows15–20 (with monitoring)

Tight vs relaxed cap on the same prompt:

import { Agent, run } from '@openai/agents';
// Stops after 3 turns even if the model still wants more context
const tight = await run(agent, prompt, { maxTurns: 3 });
// Allows a fuller investigation chain
const relaxed = await run(agent, prompt, { maxTurns: 8 });

Inspecting runs

The newItems array on the result contains tool calls, tool outputs, and messages from the run. Use it for debugging:

const result = await run(agent, prompt, { maxTurns: 8 });
for (const item of result.newItems) {
if (item.type === 'tool_call_item') {
console.log('tool:', item.rawItem.name, item.rawItem.arguments);
}
if (item.type === 'tool_call_output_item') {
console.log('output:', item.output);
}
}
console.log('lastAgent:', result.lastAgent.name);
console.log('answer:', result.finalOutput);

The SDK emits traces automatically. Set workflowName on a custom Runner to group related runs in the OpenAI Traces dashboard.

Handoffs

For multi-agent workflows, define specialist agents and wire them with Agent.create() and handoffs. The triage agent in this post stays single-agent, but handoffs are the SDK's way to delegate between agents (similar to routing a case to a billing specialist):

import { Agent } from '@openai/agents';
const billingAgent = new Agent({
name: 'Billing Specialist',
instructions: 'Handle refund and duplicate-charge cases.',
tools: [getInvoice, searchKnowledgeBase, createSupportTicket],
});
const triageAgent = Agent.create({
name: 'Triage',
instructions: 'Route billing cases to the billing specialist when needed.',
handoffs: [billingAgent],
});

After a run, check result.lastAgent to see which agent produced the final output.

Streaming

Pass stream: true to receive events as the run progresses:

import { Agent, run } from '@openai/agents';
const stream = await run(agent, prompt, { maxTurns: 8, stream: true });
process.stdout.write('Answer: ');
for await (const event of stream) {
if (event.type === 'raw_model_stream_event' && event.data.type === 'output_text_delta') {
process.stdout.write(event.data.delta);
}
if (event.type === 'run_item_stream_event' && event.name === 'tool_called') {
console.error(`\nTool: ${event.item.rawItem.name}`);
}
}
await stream.completed;
console.log('\nDone:', stream.finalOutput);

Text streams incrementally. Tool calls appear as run_item_stream_event events between text segments.

Production notes

  • Always set maxTurns - do not rely on the default cap without monitoring
  • Cost - each turn is another model call; inspect newItems or stream events for tool usage
  • Tool errors - return structured errors from execute instead of throwing when the model should retry or escalate
  • Instructions - keep policy rules in instructions, not only in the user prompt
  • Tracing - use the OpenAI Traces dashboard to debug multi-turn runs
  • Alternatives - hosted tools (web search, code interpreter), MCP servers, and sandbox agents are covered in the official docs

Demo

Runnable scripts for each section live in the openai-agents-sdk-demo folder. Get access via code demos.