TL;DR
OpenAI released their Agents SDK for TypeScript with first-class support for tool calling, structured outputs, multi-agent coordination, streaming, and human-in-the-loop approvals. Here is how each piece works.
OpenAI released their Agents SDK for TypeScript, giving JavaScript and TypeScript developers a structured framework for building AI agents. The SDK provides abstractions for the core building blocks: defining agents, equipping them with tools, getting structured outputs, coordinating multiple agents, streaming responses, and adding human approval steps.
The design philosophy is clean. An agent is a class with a name and instructions. A tool is a function with a description and a Zod schema. Running an agent is a single await run() call. The SDK handles the orchestration, tool execution loop, and response parsing.
Install it with:
npm install @openai/agentsYou will need Node.js 20 or higher. The newer versions of Node support .env files natively without needing dotenv.
The simplest agent requires two things: a name and instructions.
import { Agent, run } from "@openai/agents";
const agent = new Agent({
name: "Assistant",
instructions: "You are a helpful assistant that answers questions concisely.",
});
async function main() {
const result = await run(agent, "What is the capital of France?");
console.log(result.finalOutput);
}
main();
That is a complete working agent. The run function sends the prompt to the model, the model responds based on the instructions, and you get the output. No configuration files, no server setup, no complex initialization.
The model defaults to whatever OpenAI's current default is, but you can specify it explicitly if needed. The agent class handles conversation state, tool execution loops, and response formatting.
Agents without tools can only answer from their training data. Tools let them interact with external systems, APIs, and data sources.
A tool definition has four parts: a name, a natural language description (this is what the LLM reads to decide when to use the tool), a Zod schema for the parameters, and a function that executes when the tool is called.
import { Agent, run, tool } from "@openai/agents";
import { z } from "zod";
const getWeather = tool({
name: "get_weather",
description: "Gets the current weather for a specified city",
parameters: z.object({
city: z.string().describe("The name of the city"),
}),
execute: async ({ city }) => {
// In production, call a real weather API here
const weatherData: Record<string, string> = {
"New York": "72°F, Sunny",
"London": "61°F, Cloudy",
"Tokyo": "75°F, Partly Cloudy",
};
return weatherData[city] || "68°F, Sunny";
},
});
const weatherAgent = new Agent({
name: "Weather Agent",
instructions: "You help users check the weather in different cities.",
tools: [getWeather],
});
async function main() {
const result = await run(weatherAgent, "What's the weather in New York?");
console.log(result.finalOutput);
}
main();
The description field is critical. The LLM uses it to determine when to invoke the tool. A vague description leads to unreliable tool selection. Be specific about what the tool does and when it should be used.
The Zod schema serves double duty. It defines the parameter types for the LLM (so it knows what arguments to pass) and it validates the inputs at runtime (so your function receives correctly typed data).
You can attach multiple tools to a single agent by adding them to the tools array. The agent decides which tool to call based on the user's prompt and the tool descriptions.
Structured outputs let you define an exact schema for the agent's response. Instead of free-form text, you get a typed object that maps directly to your application's data structures.
import { Agent, run } from "@openai/agents";
import { z } from "zod";
const productSchema = z.object({
productName: z.string(),
category: z.string(),
price: z.number(),
features: z.array(z.string()),
pros: z.array(z.string()),
cons: z.array(z.string()),
rating: z.number(),
recommendation: z.string(),
});
const analystAgent = new Agent({
name: "Product Analyst",
instructions:
"You analyze products and provide detailed, structured breakdowns.",
outputType: productSchema,
});
async function main() {
const result = await run(
analystAgent,
"Analyze the iPhone 15 Pro and provide a detailed breakdown."
);
console.log(JSON.stringify(result.finalOutput, null, 2));
}
main();
The outputType parameter tells the SDK to enforce the schema on the model's response. You are guaranteed to get an object matching your Zod schema. The shape is deterministic. The values can still reflect the model's judgment (and potentially hallucinate), but the structure is locked.
This is different from JSON mode, which only guarantees valid JSON without any schema enforcement. Structured outputs guarantee both valid JSON and adherence to your specific schema.
The practical application is straightforward. If you have a UI with specific fields for product name, price, features, and rating, structured outputs let you pipe model responses directly into your component props without parsing or transformation.
Get the weekly deep dive
Tutorials on Claude Code, AI agents, and dev tools - delivered free every week.
Tools and structured outputs work together naturally. Use tools to gather real-time data, then format the results into a consistent schema.
import { Agent, run, tool } from "@openai/agents";
import { z } from "zod";
import FirecrawlApp from "@mendable/firecrawl-js";
const researchSchema = z.object({
topic: z.string(),
keyFindings: z.array(z.string()),
sources: z.array(z.object({ title: z.string(), url: z.string() })),
trends: z.array(z.string()),
recommendations: z.array(z.string()),
lastUpdated: z.string(),
});
const firecrawl = new FirecrawlApp({
apiKey: process.env.FIRECRAWL_API_KEY,
});
const webSearch = tool({
name: "web_search",
description: "Searches the web for information on a given query",
parameters: z.object({
query: z.string().describe("The search query"),
}),
execute: async ({ query }) => {
const results = await firecrawl.search(query, {
limit: 5,
scrapeOptions: { formats: ["markdown"] },
});
return results.data
.map(
(r: any) =>
`Title: ${r.metadata?.title}\nURL: ${r.url}\nContent: ${r.markdown?.slice(0, 1000)}`
)
.join("\n\n");
},
});
const researchAgent = new Agent({
name: "Research Agent",
instructions:
"Research topics thoroughly using web search and provide structured analysis.",
tools: [webSearch],
outputType: researchSchema,
});
async function main() {
const result = await run(
researchAgent,
"Research recent developments in large language models"
);
console.log(JSON.stringify(result.finalOutput, null, 2));
}
main();
The agent searches the web using Firecrawl, processes the results, and formats everything into the research schema. You get typed data with sources, findings, and trends, ready to display in a UI or store in a database.
Single agents hit a ceiling. Complex tasks benefit from specialized agents that each handle one part of the workflow, coordinated by a manager agent.
import { Agent, run, tool } from "@openai/agents";
import { z } from "zod";
const searchTool = tool({
name: "search",
description: "Search the web for information",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => {
// Web search implementation
return `Search results for: ${query}`;
},
});
const dataCollector = new Agent({
name: "Data Collector",
instructions: "Collect data from web searches. Be thorough and factual.",
tools: [searchTool],
handoffDescription:
"Hand off to this agent when you need to collect data from web searches.",
});
const analyst = new Agent({
name: "Analyst",
instructions:
"Analyze collected data. Identify patterns, trends, and key insights.",
handoffDescription:
"Hand off to this agent when you need to analyze collected data.",
});
const coordinator = new Agent({
name: "Research Coordinator",
instructions: `Coordinate research projects:
1. Understand what the user wants
2. Hand off to the Data Collector to gather information
3. Hand off to the Analyst to analyze findings
4. Provide a final summary`,
agents: [dataCollector, analyst],
});
async function main() {
const result = await run(
coordinator,
"Research the current state of AI coding tools and their adoption"
);
console.log(result.finalOutput);
}
main();
The handoffDescription field works like a tool description. It tells the coordinator when to delegate to each specialist agent. The coordinator reads the user's request, decides which agent should handle each step, and orchestrates the full workflow.
This pattern maps directly to how teams work. A front-end agent, back-end agent, and QA agent could collaborate on code generation. A researcher, writer, and editor could produce content. The coordinator manages the handoffs based on the instructions you give it.
For chat interfaces and real-time applications, streaming eliminates the wait for complete responses.
import { Agent, run } from "@openai/agents";
const agent = new Agent({
name: "Streaming Assistant",
instructions: "Provide detailed, helpful responses.",
});
async function main() {
const stream = await run(agent, "Explain how transformers work in AI", {
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk);
}
}
main();
The third argument to run accepts a configuration object where stream: true switches to streaming mode. Instead of waiting for the complete response, you get chunks as the model generates them.
In a web application, you would pipe these chunks to a Server-Sent Events endpoint or a WebSocket connection. The SDK handles the streaming protocol. You handle the delivery to the client.
Not every agent action should execute automatically. The SDK includes a built-in approval mechanism for tools that need human review before firing.
import { Agent, run, tool } from "@openai/agents";
import { z } from "zod";
import readline from "readline";
const publishContent = tool({
name: "publish_content",
description: "Publishes content to the website",
parameters: z.object({
title: z.string(),
content: z.string(),
}),
needsApproval: true,
execute: async ({ title, content }) => {
// Publish to CMS, database, etc.
return `Published: "${title}"`;
},
});
const publisher = new Agent({
name: "Content Publisher",
instructions: "Help users create and publish blog content.",
tools: [publishContent],
});
async function main() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const result = await run(
publisher,
"Publish a blog post titled 'Introduction to AI Agents'"
);
if (result.interruptions?.length) {
for (const interruption of result.interruptions) {
const answer = await new Promise<string>((resolve) => {
rl.question(
`Approve action "${interruption.toolName}"? (yes/no): `,
resolve
);
});
interruption.state = answer === "yes" ? "approved" : "rejected";
}
// Re-run with the approval decisions
const finalResult = await run(publisher, result);
console.log(finalResult.finalOutput);
}
rl.close();
}
main();
Setting needsApproval: true on a tool causes the agent to pause execution when it tries to call that tool. The result object includes an interruptions array with details about what the agent wants to do. Your code reviews the request, sets the state to approved or rejected, and resumes execution.
This pattern is essential for production agents. An agent that drafts emails should not send them without review. An agent that modifies a database should not execute writes without confirmation. An agent that publishes content should not go live without approval.
The approval mechanism is clean because it fits into the same run function. There is no separate approval API or webhook system. The same code path handles both the initial run and the resumed run after approval.
Before this SDK, building agents in TypeScript meant either using LangChain.js (which many developers found overly abstracted) or wiring up tool-calling loops manually with the base OpenAI client library. The Agents SDK sits between those extremes: enough structure to handle the common patterns, not so much abstraction that you lose visibility into what is happening.
The key patterns covered by the SDK - tool calling, structured outputs, multi-agent handoffs, streaming, and human approval - represent the fundamental building blocks of most agent applications. If you can compose these five patterns, you can build sophisticated AI workflows without reaching for additional frameworks.
The SDK also includes real-time and voice capabilities for applications that need them, though those are separate from the core agent patterns covered here.
For TypeScript developers already building with OpenAI's models, the Agents SDK is the official way to move from simple chat completions to agent-based architectures. The learning curve is gentle if you are comfortable with async/await patterns and Zod schemas. And because it uses the same OpenAI API key and models you are already paying for, there is no additional infrastructure to set up.
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.
Lightweight Python framework for multi-agent systems. Agent handoffs, tool use, guardrails, tracing. Successor to the ex...
View ToolMulti-agent orchestration framework built on the OpenAI Agents SDK. Define agent roles, typed tools, and directional com...
View Tool
New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.
The TypeScript toolkit for building AI apps. Unified API across OpenAI, Anthropic, Google. Streaming, tool calling, stru...
Step-by-step guide to building an MCP server in TypeScript - from project setup to tool definitions, resource handling, testing, and deployment.
AI AgentsConfigure Claude Code for maximum productivity -- CLAUDE.md, sub-agents, MCP servers, and autonomous workflows.
AI AgentsWhat MCP servers are, how they work, and how to build your own in 5 minutes.
AI Agents
Getting Started with OpenAI's New TypeScript Agents SDK: A Comprehensive Guide OpenAI has recently unveiled their Agents SDK within TypeScript, and this video provides a detailed walkthrough...

In this video, I guide you through setting up the new OpenAI real-time API, which promises new interactive possibilities for developers with its web socket-based architecture. You will learn...

In this video, I dive deep into the Groq Inference API, which I've found to be the fastest inference API out there. I share my insights on the various approaches to leveraging this API, focusing...
Production-tested patterns for orchestrating AI agent teams - from fan-out parallelism to hierarchical delegation. Cover...

Agents forget everything between sessions. Here are the patterns that fix that: CLAUDE.md persistence, RAG retrieval, co...

AI agents fail in ways traditional debugging cannot catch. Here are the tools and patterns for finding and fixing broken...