max_iter: capping an agent's loop
max_iter is the cap on an agent's tool loop; after that many rounds CrewAI removes the tools and asks for a final answer.
Last updated: 28 Sep, 2026 · CrewAI 1.15
The tool-calls lesson's loop ends when the model answers in words. Nothing forces it to. This model asks for the same lookup every time it has tools, however many results it has already seen.
View the code here
from crewai.tools import tool
ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order's shipping status by its id, such as A17."""
status = ORDERS.get(order_id)
return f"{order_id} {status}." if status else f"{order_id} is not an order we have."
import json
import os
import re
from crewai import BaseLLM
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"
class ShopLLM(BaseLLM):
script: list = []
def supports_function_calling(self):
return True
def call(self, messages, tools=None, **kwargs):
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
if self.script:
return self.script.pop(0)
names = [t["function"]["name"] for t in tools or []]
return self.decide(messages, names)
def decide(self, messages, tools):
last = messages[-1]
if last["role"] == "tool":
return last["content"]
text = last["content"]
orders = re.findall(r"\b[A-Z]\d+\b", text)
wanted = "refund_order" if "refund" in text.lower() else "lookup_order"
matches = [name for name in tools if name.endswith(wanted)]
if orders and matches:
args = json.dumps({"order_id": orders[0]})
return [{"id": f"call_{orders[0]}", "type": "function",
"function": {"name": matches[0], "arguments": args}}]
if orders:
return f"I have no way to look up {orders[0]} yet."
return "Hello. Which order is this about?"
The clerk from the tool-calls lesson
from crewai import Agent, Crew, Task
from shop_llm import ShopLLM
from tools import lookup_order
clerk = Agent(
role="Order clerk",
goal="Find the status of customers' orders",
backstory="You can look up any order in the shop's system.",
llm=ShopLLM(model="shop"),
tools=[lookup_order],
)
task = Task(description="Answer the customer: {question}",
expected_output="The order's status in one sentence.", agent=clerk)
crew = Crew(agents=[clerk], tasks=[task])A model that never stops asking
from shop_llm import ShopLLM
class Stuck(ShopLLM):
calls: int = 0
def call(self, messages, tools=None, **kwargs):
self.calls += 1
print("model call", self.calls, "with tools" if tools else "without tools")
if tools:
return self.decide(messages[:2], ["lookup_order"])
return "A17 shipped on 3 March. Sorry for the wait."Stuck passes only the first two messages to decide, so it never sees a tool result and asks again. It prints each call, and answers in words only when it is given no tools.
Reading the default cap
from crewai import Agent
from shop_llm import ShopLLM
clerk = Agent(role="Order clerk", goal="Find orders",
backstory="You look orders up.", llm=ShopLLM(model="shop"))
print(clerk.max_iter)25
This reads the cap from the installed version rather than stating it, because the number drifts between releases. Whatever it prints, it is far more rounds than a normal run needs, so the next example sets a small cap to see the stop.
Capping the loop with max_iter
clerk.llm = Stuck(model="shop")
clerk.max_iter = 3
result = crew.kickoff(inputs={"question": "Where is my order A17?"})
print(result.raw)model call 1 with tools model call 2 with tools model call 3 with tools model call 4 without tools A17 shipped on 3 March. Sorry for the wait.
Three calls came with tools and made three lookups. On the fourth, CrewAI sent no tools and a message asking for a best final answer now, and the model gave one. The default cap is large, larger than a well-behaved run needs, so this example sets it low to make the stop easy to see; a stuck hosted model would otherwise make that many paid calls before stopping.
How a tool loop ends
| Ending | What happens |
|---|---|
| the model answers in words | the loop stops with that answer |
| max_iter reached | tools removed, one last call, an answer |
| a model hook aborts | the run stops with an error (the model-hooks lesson) |
When to set a low cap
- While building, so a mistake in the model stops in a few rounds, not many.
- On untrusted input, where a prompt might drive the loop on and on.
- To hold down cost, since each round is another paid call to a hosted model.
Related
- Previous: Tool failures mid-run
- Next: Tool hooks: approving a refund
- Set
max_iterto 1 and count the model calls. - Print
clerk.llm.callsafter the run. - Remove the
if toolsbranch and see where the run ends.
Little by little, you're building something great.