Running agents
Agents do nothing by themselves – you run them with the Runner class or the run() utility.
import { Agent, run } from '@openai/agents';
const agent = new Agent({ name: 'Assistant', instructions: 'You are a helpful assistant',});
const result = await run( agent, 'Write a haiku about recursion in programming.',);console.log(result.finalOutput);
// Code within the code,// Functions calling themselves,// Infinite loop's dance.When you don’t need a custom runner, you can also use the run() utility, which runs a singleton default Runner instance.
Alternatively, you can create your own runner instance:
import { Agent, Runner } from '@openai/agents';
const agent = new Agent({ name: 'Assistant', instructions: 'You are a helpful assistant',});
// You can pass custom configuration to the runnerconst runner = new Runner();
const result = await runner.run( agent, 'Write a haiku about recursion in programming.',);console.log(result.finalOutput);
// Code within the code,// Functions calling themselves,// Infinite loop's dance.After running your agent, you will receive a result object that contains the final output and the full history of the run.
Runner lifecycle and configuration
Section titled “Runner lifecycle and configuration”The agent loop
Section titled “The agent loop”When you use the run method in Runner, you pass in a starting agent and input. The input can either be a string (which is considered a user message), or a list of input items, which are the items in the OpenAI Responses API.
The runner then runs a loop:
- Call the current agent’s model with the current input.
- Inspect the LLM response.
- Final output → return.
- Handoff → switch to the new agent, keep the accumulated conversation history, go to 1.
- Tool calls → execute tools, append their results to the conversation, go to 1.
- Throw
MaxTurnsExceededErroroncemaxTurnsis reached.
Runner lifecycle
Section titled “Runner lifecycle”Create a Runner when your app starts and reuse it across requests. The instance
stores global configuration such as model provider and tracing options. Only
create another Runner if you need a completely different setup. For simple
scripts you can also call run() which uses a default runner internally.
Run arguments
Section titled “Run arguments”The input to the run() method is an initial agent to start the run on, input for the run and a set of options.
The input can either be a string (which is considered a user message), or a list of input items, or a RunState object in case you are building a human-in-the-loop agent.
The additional options are:
| Option | Default | Description |
|---|---|---|
stream | false | If true the call returns a StreamedRunResult and emits events as they arrive from the model. |
context | – | Context object forwarded to every tool / guardrail / handoff. Learn more in the context guide. |
maxTurns | 10 | Safety limit – throws MaxTurnsExceededError when reached. |
signal | – | AbortSignal for cancellation. |
session | – | Session persistence implementation. See the sessions guide. |
sessionInputCallback | – | Custom merge logic for session history and new input; runs before the model call. See sessions. |
callModelInputFilter | – | Hook to edit the model input (items + optional instructions) just before calling the model. See Call model input filter. |
toolErrorFormatter | – | Hook to customize tool approval rejection messages returned to the model. See Tool error formatter. |
reasoningItemIdPolicy | – | Controls whether reasoning-item ids are preserved or omitted when prior run items are turned back into model input. See Reasoning item ID policy. |
tracing | – | Per-run tracing configuration overrides (for example, export API key). |
errorHandlers | – | Handlers for supported runtime errors (currently maxTurns). See Error handlers. |
conversationId | – | Reuse a server-side conversation (OpenAI Responses API + Conversations API only). |
previousResponseId | – | Continue from the previous Responses API call without creating a conversation (OpenAI Responses API only). |
Streaming
Section titled “Streaming”Streaming allows you to additionally receive streaming events as the LLM runs. Once the stream is started, the StreamedRunResult will contain the complete information about the run, including all the new outputs produces. You can iterate over the streaming events using a for await loop. Read more in the streaming guide.
Run config
Section titled “Run config”If you are creating your own Runner instance, you can pass in a RunConfig object to configure the runner.
| Field | Type | Purpose |
|---|---|---|
model | string | Model | Force a specific model for all agents in the run. |
modelProvider | ModelProvider | Resolves model names – defaults to the OpenAI provider. |
modelSettings | ModelSettings | Global tuning parameters that override per‑agent settings. |
handoffInputFilter | HandoffInputFilter | Mutates input items when performing handoffs (if the handoff itself doesn’t already define one). |
inputGuardrails | InputGuardrail[] | Guardrails applied to the initial user input. |
outputGuardrails | OutputGuardrail[] | Guardrails applied to the final output. |
tracingDisabled | boolean | Disable OpenAI Tracing completely. |
traceIncludeSensitiveData | boolean | Exclude LLM/tool inputs & outputs from traces while still emitting spans. |
workflowName | string | Appears in the Traces dashboard – helps group related runs. |
traceId / groupId | string | Manually specify the trace or group ID instead of letting the SDK generate one. |
traceMetadata | Record<string, string> | Arbitrary metadata to attach to every span. |
tracing | TracingConfig | Per-run tracing overrides (for example, export API key). |
sessionInputCallback | SessionInputCallback | Default history merge strategy for all runs on this runner. |
callModelInputFilter | CallModelInputFilter | Global hook to edit model inputs before each model call. |
toolErrorFormatter | ToolErrorFormatter | Global hook to customize tool approval rejection messages returned to the model. |
reasoningItemIdPolicy | ReasoningItemIdPolicy | Default policy for preserving or omitting reasoning-item ids when replaying generated items into later model calls. |
State and conversation management
Section titled “State and conversation management”Conversations / chat threads
Section titled “Conversations / chat threads”Each call to runner.run() (or run() utility) represents one turn in your application-level conversation. You choose how much of the RunResult you show the end‑user – sometimes only finalOutput, other times every generated item.
import { Agent, run } from '@openai/agents';import type { AgentInputItem } from '@openai/agents';
let thread: AgentInputItem[] = [];
const agent = new Agent({ name: 'Assistant',});
async function userSays(text: string) { const result = await run( agent, thread.concat({ role: 'user', content: text }), );
thread = result.history; // Carry over history + newly generated items return result.finalOutput;}
await userSays('What city is the Golden Gate Bridge in?');// -> "San Francisco"
await userSays('What state is it in?');// -> "California"See the chat example for an interactive version.
Server-managed conversations
Section titled “Server-managed conversations”You can let the OpenAI Responses API persist conversation history for you instead of sending your entire local transcript on every turn. This is useful when you are coordinating long conversations or multiple services. See the Conversation state guide for details.
OpenAI exposes two ways to reuse server-side state:
1. conversationId for an entire conversation
Section titled “1. conversationId for an entire conversation”You can create a conversation once using Conversations API and then reuse its ID for every turn. The SDK automatically includes only the newly generated items.
import { Agent, run } from '@openai/agents';import { OpenAI } from 'openai';
const agent = new Agent({ name: 'Assistant', instructions: 'Reply very concisely.',});
async function main() { // Create a server-managed conversation: const client = new OpenAI(); const { id: conversationId } = await client.conversations.create({});
const first = await run(agent, 'What city is the Golden Gate Bridge in?', { conversationId, }); console.log(first.finalOutput); // -> "San Francisco"
const second = await run(agent, 'What state is it in?', { conversationId }); console.log(second.finalOutput); // -> "California"}
main().catch(console.error);2. previousResponseId to continue from the last turn
Section titled “2. previousResponseId to continue from the last turn”If you want to start only with Responses API anyway, you can chain each request using the ID returned from the previous response. This keeps the context alive across turns without creating a full conversation resource.
import { Agent, run } from '@openai/agents';
const agent = new Agent({ name: 'Assistant', instructions: 'Reply very concisely.',});
async function main() { const first = await run(agent, 'What city is the Golden Gate Bridge in?'); console.log(first.finalOutput); // -> "San Francisco"
const previousResponseId = first.lastResponseId; const second = await run(agent, 'What state is it in?', { previousResponseId, }); console.log(second.finalOutput); // -> "California"}
main().catch(console.error);Hooks and customization
Section titled “Hooks and customization”Call model input filter
Section titled “Call model input filter”Use callModelInputFilter to edit the model input right before the model is called. This hook receives the current agent, context, and the combined input items (including session history when present). Return the updated input array and optional instructions to redact sensitive data, drop old messages, or inject additional system guidance.
Set it per run in runner.run(..., { callModelInputFilter }) or as a default in the Runner config (callModelInputFilter in RunConfig).
Tool error formatter
Section titled “Tool error formatter”Use toolErrorFormatter to customize approval-rejection messages that are sent back to the model when a tool call is rejected. This lets you return domain-specific wording (for example, compliance guidance) instead of the SDK default message.
The formatter can be set per-run (runner.run(..., { toolErrorFormatter })) or globally in RunConfig (toolErrorFormatter in new Runner(...)).
The formatter currently runs for approval_rejected events and receives:
kind(currently always'approval_rejected')toolType('function','computer','shell', or'apply_patch')toolNamecallIddefaultMessage(the SDK fallback message)runContext
Return a string to override the message, or return undefined to keep the SDK default. If the formatter throws (or returns a non-string value), the SDK logs a warning and falls back to the default approval-rejection message.
Reasoning item ID policy
Section titled “Reasoning item ID policy”Use reasoningItemIdPolicy to control whether reasoning items keep their id fields when the SDK converts previously generated run items back into AgentInputItem[] for later model input.
This affects places where the SDK replays generated model items as input, such as:
-
follow-up model calls inside the same run (for example, after tool execution),
-
subsequent turns that reuse generated items as input/history,
-
resumed runs from a saved
RunState, -
derived result views like
result.history/result.output(which are model-input-shaped arrays). -
'preserve'(default) keeps reasoning-item IDs. -
'omit'strips theidfield from reasoning items before they are sent back as input. -
Non-reasoning items are unaffected.
What this does not change:
- raw model responses (
result.rawResponses), - run items (
result.newItems), - the model’s current-turn output as returned by the provider.
In other words, the policy applies when the SDK builds the next input from prior generated items.
You can set the policy per run (runner.run(..., { reasoningItemIdPolicy: 'omit' })) or as the runner default (new Runner({ reasoningItemIdPolicy: 'omit', ... })). When resuming from a saved RunState, the previously resolved policy is reused unless you override it.
Interaction with callModelInputFilter
Section titled “Interaction with callModelInputFilter”reasoningItemIdPolicy is applied before callModelInputFilter. If you need custom behavior, callModelInputFilter can still inspect the prepared input and re-introduce or remove reasoning IDs manually before the model call.
When to use 'omit'
Section titled “When to use 'omit'”Use 'omit' when you want replayed reasoning items to be normalized without IDs (for example, to keep forwarded/replayed model inputs simpler or to match integration requirements in your app pipeline).
It is also a useful troubleshooting option if your backend/provider rejects replayed reasoning items with request validation errors (for example, HTTP 400 errors related to reasoning item IDs in follow-up inputs). In those cases, stripping replayed reasoning IDs with 'omit' can avoid sending IDs that the backend treats as invalid for a new request.
Keep 'preserve' if you want the SDK to carry reasoning-item IDs through replayed inputs and your integration accepts them.
Errors and recovery
Section titled “Errors and recovery”Error handlers
Section titled “Error handlers”Use errorHandlers to convert supported runtime errors into a final output instead of throwing. Currently, only maxTurns is supported.
errorHandlers.maxTurnshandles only max-turn errors.errorHandlers.defaultis used as a fallback for supported kinds.- Handlers receive
{ error, context, runData }and can return{ finalOutput, includeInHistory? }.
Exceptions
Section titled “Exceptions”The SDK throws a small set of errors you can catch:
MaxTurnsExceededError–maxTurnsreached.ModelBehaviorError– model produced invalid output (e.g. malformed JSON, unknown tool).InputGuardrailTripwireTriggered/OutputGuardrailTripwireTriggered– guardrail violations.ToolInputGuardrailTripwireTriggered/ToolOutputGuardrailTripwireTriggered– tool guardrail violations.GuardrailExecutionError– guardrails failed to complete.ToolTimeoutError– a function tool exceededtimeoutMsand usedtimeoutBehavior: 'raise_exception'.ToolCallError– function tool execution failed for non-timeout errors.UserError– any error thrown based on configuration or user input.
All extend the base AgentsError class, which could provide the state property to access the current run state.
Here is an example code that handles GuardrailExecutionError. Because input guardrails only run on the first user input, the example restarts the run with the original input and context. It also shows reusing the saved state to retry output guardrails without calling the model again:
import { Agent, GuardrailExecutionError, InputGuardrail, InputGuardrailTripwireTriggered, OutputGuardrail, OutputGuardrailTripwireTriggered, run,} from '@openai/agents';import { z } from 'zod';
// Shared guardrail agent to avoid re-creating it on every fallback run.const guardrailAgent = new Agent({ name: 'Guardrail check', instructions: 'Check if the user is asking you to do their math homework.', outputType: z.object({ isMathHomework: z.boolean(), reasoning: z.string(), }),});
async function main() { const input = 'Hello, can you help me solve for x: 2x + 3 = 11?'; const context = { customerId: '12345' };
// Input guardrail example
const unstableInputGuardrail: InputGuardrail = { name: 'Math Homework Guardrail (unstable)', execute: async () => { throw new Error('Something is wrong!'); }, };
const fallbackInputGuardrail: InputGuardrail = { name: 'Math Homework Guardrail (fallback)', execute: async ({ input, context }) => { const result = await run(guardrailAgent, input, { context }); const isMathHomework = result.finalOutput?.isMathHomework ?? /solve for x|math homework/i.test(JSON.stringify(input)); return { outputInfo: result.finalOutput, tripwireTriggered: isMathHomework, }; }, };
const agent = new Agent({ name: 'Customer support agent', instructions: 'You are a customer support agent. You help customers with their questions.', inputGuardrails: [unstableInputGuardrail], });
try { // Input guardrails only run on the first turn of a run, so retries must start a fresh run. await run(agent, input, { context }); } catch (e) { if (e instanceof GuardrailExecutionError) { console.error(`Guardrail execution failed (input): ${e}`); try { agent.inputGuardrails = [fallbackInputGuardrail]; // Retry from scratch with the original input and context. await run(agent, input, { context }); } catch (ee) { if (ee instanceof InputGuardrailTripwireTriggered) { console.log('Math homework input guardrail tripped on retry'); } else { throw ee; } } } else { throw e; } }
// Output guardrail example
const replyOutputSchema = z.object({ reply: z.string() });
const unstableOutputGuardrail: OutputGuardrail<typeof replyOutputSchema> = { name: 'Answer review (unstable)', execute: async () => { throw new Error('Output guardrail crashed.'); }, };
const fallbackOutputGuardrail: OutputGuardrail<typeof replyOutputSchema> = { name: 'Answer review (fallback)', execute: async ({ agentOutput }) => { const outputText = typeof agentOutput === 'string' ? agentOutput : (agentOutput?.reply ?? JSON.stringify(agentOutput)); const flagged = /math homework|solve for x|x =/i.test(outputText); return { outputInfo: { flaggedOutput: outputText }, tripwireTriggered: flagged, }; }, };
const agent2 = new Agent<unknown, typeof replyOutputSchema>({ name: 'Customer support agent (output check)', instructions: 'You are a customer support agent. Answer briefly.', outputType: replyOutputSchema, outputGuardrails: [unstableOutputGuardrail], });
try { await run(agent2, input, { context }); } catch (e) { if (e instanceof GuardrailExecutionError && e.state) { console.error(`Guardrail execution failed (output): ${e}`); try { agent2.outputGuardrails = [fallbackOutputGuardrail]; // Output guardrails can be retried using the saved state without another model call. await run(agent2, e.state); } catch (ee) { if (ee instanceof OutputGuardrailTripwireTriggered) { console.log('Output guardrail tripped after retry with saved state'); } else { throw ee; } } } else { throw e; } }}
main().catch(console.error);Input vs. output retries:
- Input guardrails run only on the very first user input of a run, so you must start a fresh run with the same input/context to retry them—passing a saved
statewill not re-trigger input guardrails. - Output guardrails run after the model response, so you can reuse the saved
statefrom aGuardrailExecutionErrorto rerun output guardrails without another model call.
When you run the above example, you will see the following output:
Guardrail execution failed (input): Error: Input guardrail failed to complete: Error: Something is wrong!Math homework input guardrail tripped on retryGuardrail execution failed (output): Error: Output guardrail failed to complete: Error: Output guardrail crashed.Output guardrail tripped after retry with saved stateNext steps
Section titled “Next steps”- Learn how to configure models.
- Provide your agents with tools.
- Add guardrails or tracing for production readiness.