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/agentsandzodinstalled (npm i @openai/agents zod) -
OPENAI_API_KEYset 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:
-
get_customer- plan tier, open ticket count -
get_invoice- amount, status, payment IDs -
search_knowledge_base- duplicate-charge and refund policy -
create_support_ticketorescalate_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 case | Suggested cap |
|---|---|
| Single tool, then answer | 2 |
| Chat with occasional tool use | 3–5 |
| Task agents (triage, research) | 8–15 |
| Long autonomous workflows | 15–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
newItemsor stream events for tool usage -
Tool errors - return structured errors from
executeinstead 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.
Top comments (0)