CrewAICrewAI 1.15 · Python 3.10 to 3.13
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
35 small wins to finish your pathNext lesson →

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.

Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
tools.py
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."
shop_llm.py
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

python
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

python
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

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

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

Example
clerk.llm = Stuck(model="shop")
clerk.max_iter = 3
result = crew.kickoff(inputs={"question": "Where is my order A17?"})
print(result.raw)

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

EndingWhat happens
the model answers in wordsthe loop stops with that answer
max_iter reachedtools removed, one last call, an answer
a model hook abortsthe 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.
Watch out
The default cap is large, so a stuck hosted model makes many paid calls before it stops. Set max_iter low while you build and raise it once the agent behaves.
Try it yourself
  • Set max_iter to 1 and count the model calls.
  • Print clerk.llm.calls after the run.
  • Remove the if tools branch and see where the run ends.

Little by little, you're building something great.