Capping a run with max_turns
max_turns is a limit on how many model and tool cycles a single run may take before the SDK stops it with an error.
Last updated: 28 Sep, 2026 · openai-agents 0.22.3
A run loops: the model answers, and if it calls a tool the SDK runs the tool and calls the model again. Each model call is one turn. If the model never gives a final answer the loop would run forever, so max_turns caps it and raises MaxTurnsExceeded. The default cap is 10.
The max_turns argument and MaxTurnsExceeded
from agents import Runner, MaxTurnsExceeded
try:
Runner.run_sync(agent, "go", max_turns=3) # stop after 3 turns
except MaxTurnsExceeded as e:
print("stopped:", e)A model that always calls a tool
To force the cap the stand-in returns a tool call every time and never a final message, so the loop can only end at the cap.
class LoopModel(Model):
async def get_response(self, system_instructions, input, model_settings, tools,
output_schema, handoffs, tracing, **k):
call = ResponseFunctionToolCall(id="fc", call_id="c1", name="ping",
arguments="{}", type="function_call")
return ModelResponse(output=[call], usage=Usage(), response_id=None)A tool for it to call
The model asks for a tool named ping, so the agent needs one. It returns a value each time, which sends the loop back to the model.
from agents import function_tool
@function_tool
def ping() -> str:
"A tool that always returns pong."
return "pong"Running with a small cap and catching the error
Give the agent the tool and the looping model, run with max_turns=3, and catch the exception the cap raises.
agent = Agent(name="Looper", instructions="x", tools=[ping], model=LoopModel())
try:
Runner.run_sync(agent, "go", max_turns=3)
except MaxTurnsExceeded as e:
print("stopped:", e)A loop stopped at three turns
The whole program in one file. The model always calls the tool, so the run can only end when it hits the cap.
from agents import Agent, Runner, function_tool, set_tracing_disabled, MaxTurnsExceeded
from agents.models.interface import Model
from agents.items import ModelResponse
from agents.usage import Usage
from openai.types.responses import ResponseFunctionToolCall
set_tracing_disabled(True)
@function_tool
def ping() -> str:
"A tool that always returns pong."
return "pong"
class LoopModel(Model):
async def get_response(self, system_instructions, input, model_settings, tools,
output_schema, handoffs, tracing, **k):
call = ResponseFunctionToolCall(id="fc", call_id="c1", name="ping",
arguments="{}", type="function_call")
return ModelResponse(output=[call], usage=Usage(), response_id=None)
async def stream_response(self, *a, **k):
raise NotImplementedError
agent = Agent(name="Looper", instructions="x", tools=[ping], model=LoopModel())
try:
Runner.run_sync(agent, "go", max_turns=3)
except MaxTurnsExceeded as e:
print("stopped:", e)Why the run raised MaxTurnsExceeded
- Each turn the model returns a tool call, the SDK runs
ping, and it calls the model again. - No turn ever returns a final message, so the loop cannot end on its own.
- At the third turn the run reaches
max_turns=3and raisesMaxTurnsExceeded, whose message names the limit that was hit.
Default cap vs a small cap
| max_turns | Effect on this looping model | When to use it |
|---|---|---|
10 (default) | Loops ten times, then raises | Normal runs that end well before ten |
3 | Loops three times, then raises | Testing a limit or bounding a risky loop |
When to lower max_turns
- Bounding an agent that might keep calling tools without settling on an answer.
- Failing fast in a test instead of waiting for the default ten cycles.
- Protecting a run from a tool loop that has no natural end.
max_turns is 10. A model that always calls a tool never returns a final answer, so it will always hit the cap. Give the model a path to a final message when you want the run to finish on its own.Related
- Previous: Shaping the model with ModelSettings
- Next: The desk agent and its tools
- Reference: Running agents: exceptions
- Raise
max_turnsto 5 and confirm the message names the new limit. - Make
LoopModelreturn a final message on the second call and watch the run finish. - Remove the
tryblock and read the raisedMaxTurnsExceeded.
This is what real progress feels like.