Skip to content

Running agents

Agents do nothing by themselves – you run them with the Runner class or the run() utility.

Simple run
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:

Simple run
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 runner
const 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.

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:

  1. Call the current agent’s model with the current input.
  2. 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.
  3. Throw MaxTurnsExceededError once maxTurns is reached.

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.

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:

OptionDefaultDescription
streamfalseIf true the call returns a StreamedRunResult and emits events as they arrive from the model.
contextContext object forwarded to every tool / guardrail / handoff. Learn more in the context guide.
maxTurns10Safety limit – throws MaxTurnsExceededError when reached.
signalAbortSignal for cancellation.
sessionSession persistence implementation. See the sessions guide.
sessionInputCallbackCustom merge logic for session history and new input; runs before the model call. See sessions.
callModelInputFilterHook to edit the model input (items + optional instructions) just before calling the model. See Call model input filter.
tracingPer-run tracing configuration overrides (for example, export API key).
conversationIdReuse a server-side conversation (OpenAI Responses API + Conversations API only).
previousResponseIdContinue from the previous Responses API call without creating a conversation (OpenAI Responses API only).

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.

If you are creating your own Runner instance, you can pass in a RunConfig object to configure the runner.

FieldTypePurpose
modelstring | ModelForce a specific model for all agents in the run.
modelProviderModelProviderResolves model names – defaults to the OpenAI provider.
modelSettingsModelSettingsGlobal tuning parameters that override per‑agent settings.
handoffInputFilterHandoffInputFilterMutates input items when performing handoffs (if the handoff itself doesn’t already define one).
inputGuardrailsInputGuardrail[]Guardrails applied to the initial user input.
outputGuardrailsOutputGuardrail[]Guardrails applied to the final output.
tracingDisabledbooleanDisable OpenAI Tracing completely.
traceIncludeSensitiveDatabooleanExclude LLM/tool inputs & outputs from traces while still emitting spans.
workflowNamestringAppears in the Traces dashboard – helps group related runs.
traceId / groupIdstringManually specify the trace or group ID instead of letting the SDK generate one.
traceMetadataRecord<string, string>Arbitrary metadata to attach to every span.
tracingTracingConfigPer-run tracing overrides (for example, export API key).
sessionInputCallbackSessionInputCallbackDefault history merge strategy for all runs on this runner.
callModelInputFilterCallModelInputFilterGlobal hook to edit model inputs before each model call.

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.

Example of carrying over the conversation history
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.

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.

Reusing a server conversation
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.

Chaining with previousResponseId
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);

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).

The SDK throws a small set of errors you can catch:

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:

Guardrail execution error
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 state will not re-trigger input guardrails.
  • Output guardrails run after the model response, so you can reuse the saved state from a GuardrailExecutionError to 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 retry
Guardrail execution failed (output): Error: Output guardrail failed to complete: Error: Output guardrail crashed.
Output guardrail tripped after retry with saved state