CrewAI infinite loop debugging starts with accepting that crews rarely hang without a cause. The framework’s agents iterate until they hit an internal stop condition, and when that condition is missing or contradictory, they cycle through delegation, tool calls, or self-reflection forever. This guide lays out an ordered path to find and break those cycles in production.
1. Reproduce with verbose logging
You cannot fix a loop you cannot see. Run the crew with verbose=True on both agents and crew, and capture stdout to a file. CrewAI prints each agent’s thought, action, and observation. A loop shows up as repeated substrings in the log within seconds.
from crewai import Agent, Task, Crew
agent = Agent(
role="Analyst",
goal="Summarize the report",
backstory="Precise and terse.",
verbose=True,
)
task = Task(
description="Summarize the attached text in 2 sentences.",
expected_output="Two-sentence summary.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task], verbose=True)
crew.kickoff()
Pipe output: python crew.py 2>&1 | tee crew.log. Grep for the agent role name. If you see the same Action: line more than five times, you have a loop.
2. Classify the loop type
Three patterns cover nearly all CrewAI hangs.
Delegation loops
Two agents with allow_delegation=True ping-pong a task. Agent A delegates to B, B delegates back to A because its goal is ambiguous. The crew never terminates because neither agent marks the task done.
Self-reflection loops
A single agent hits its internal ReAct loop. It keeps generating Thought: and Action: blocks without emitting a final answer. This is the default behavior when max_iter is high and the prompt lacks a stop instruction.
Tool-call loops
An agent calls a tool that returns a soft error (“try again”) or an empty string. The agent interprets that as incomplete and calls again with identical arguments.
3. Cap iterations and enforce timeouts
CrewAI exposes max_iter on the Agent to bound self-reflection. Set it to a low value during debugging—three is enough to see intent without spinning.
agent = Agent(
role="Analyst",
goal="Summarize the report",
backstory="Precise and terse.",
max_iter=3,
allow_delegation=False,
verbose=True,
)
max_iter does not cap wall-clock time. Wrap kickoff_async in an asyncio timeout so a stuck crew cannot block your service.
import asyncio
async def run_with_timeout(crew, seconds=30):
return await asyncio.wait_for(crew.kickoff_async(), timeout=seconds)
try:
result = asyncio.run(run_with_timeout(crew, 30))
except asyncio.TimeoutError:
# emit metric, alert, fallback
print("Crew timed out — loop suspected")
Tradeoff: a timeout kills legitimate long tasks. Set it from historical p95 latency of the same crew, not a guess.
4. Kill delegation loops with explicit boundaries
If you see cross-agent delegation, disable it unless you specifically need hierarchical control.
manager = Agent(role="Manager", goal="Coordinate", backstory="...", allow_delegation=False)
worker = Agent(role="Worker", goal="Execute", backstory="...", allow_delegation=False)
When you do need delegation, give the manager a hard exit condition in its goal: “Delegate subtasks, then call finish with the consolidated answer. Never return a task to a previous agent.” CrewAI respects natural language stop cues only as well as the underlying LLM does, so pair this with max_iter.
5. Fix the prompt contract
Most self-reflection loops come from underspecified expected_output. The agent does not know what “done” looks like. Make it unambiguous.
task = Task(
description=(
"Read the input. Output exactly one JSON object with keys 'summary' "
"(string) and 'confidence' (float 0-1). Do not output any other text. "
"If unsure, set confidence to 0.0. STOP after the JSON."
),
expected_output="Single JSON object, no prose.",
agent=agent,
)
Add a literal STOP token in the description. Many LLMs trained on ReAct traces treat STOP as termination.
6. Guard tool calls with sentinels
Tool-call loops vanish when the tool returns a decisive result. Instead of raising an exception or returning "", return a structured sentinel the agent is told to respect.
def search(query: str) -> str:
res = backend.search(query)
if not res:
return "RESULT_EMPTY: no matches, do not retry with same query."
return res
In the agent’s backstory, state: “If a tool returns RESULT_EMPTY, change the query or finish.” Without that instruction, the model treats empty as transient.
7. Detect state repetition externally
CrewAI has no built-in cycle detector across tasks. Add one by hashing the last agent message after each step using a lightweight callback or by polling the log.
import hashlib
class LoopGuard:
def __init__(self, max_repeats=3):
self.seen = {}
self.max_repeats = max_repeats
def check(self, state: str):
h = hashlib.md5(state.encode()).hexdigest()
self.seen[h] = self.seen.get(h, 0) + 1
if self.seen[h] > self.max_repeats:
raise RuntimeError("State repeated — aborting crew")
Feed it the concatenated agent.last_message after each kickoff iteration if you drive the crew manually in a loop, or parse streaming logs in a separate thread.
8. Handle LLM provider degradation
A subtle source of loops is the agent receiving truncated or error responses and retrying. If your crew uses a single provider and that provider rate-limits, the SDK may hang or return garbage that the agent loops on. An OpenAI-compatible gateway with automatic fallback when a provider is degraded—such as n4n.ai—removes the retry storm at the network layer, but you still need the client-side caps above because a fallback model can also produce ambiguous output.
Set max_rpm on the Crew to avoid hammering any provider:
crew = Crew(agents=[agent], tasks=[task], verbose=True, max_rpm=10)
9. Validate with a minimal crew
Before redeploying, strip the crew to one agent and one task that previously looped. Confirm it terminates within max_iter and timeout. Then add agents back one at a time. This isolates whether the loop was in a single agent’s reasoning or in cross-agent interaction.
minimal = Crew(agents=[agent], tasks=[task], verbose=True, max_rpm=5)
minimal.kickoff()
Common pitfalls and tradeoffs
- Setting
max_itertoo low. Agents may return partial work. Use 3–5 for debugging, 10–15 for production if logs show clean termination. - Disabling delegation globally. You lose hierarchical planning. Disable per-agent where the loop occurs, not blanket.
- Timeouts as a crutch. A timeout hides the bug; it does not fix the prompt. Treat every timeout as a failed test.
- Over-specifying
expected_output. If you demand a rigid format the model rarely produces, you create a new loop. Match the format to the model’s strengths (JSON for Claude, loose bullets for GPT-style). - Ignoring tool latency. A slow tool looks like a hang. Measure tool call duration separately from agent reasoning.
CrewAI infinite loop debugging is mostly about making termination explicit: cap iterations, bound time, forbid needless delegation, and give the model a clear “done” shape. Do that, and the cycles disappear.