How to Fix 'agent infinite loop during development' in CrewAI (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
agent-infinite-loop-during-developmentcrewaitypescript

What this error actually means

agent infinite loop during development usually means your CrewAI agent kept getting re-invoked without reaching a terminal state. In TypeScript projects, this shows up most often when the agent can’t satisfy its task, keeps calling tools with no exit condition, or your orchestration code re-triggers the same run.

You’ll usually see it during local development when iterating on tools, task prompts, or step callbacks. The stack trace often points at Agent, Task, Crew, or a tool call that never resolves.

The Most Common Cause

The #1 cause is a task prompt that encourages open-ended iteration with no hard stop, combined with an agent that can keep calling tools forever.

Typical pattern:

  • the task asks for “continue until complete”
  • the tool returns partial data
  • the agent retries the same action
  • CrewAI detects repeated steps and throws an infinite loop error

Broken vs fixed

BrokenFixed
No explicit termination criteriaClear output format and stop condition
Tool can be called repeatedly with same inputTool result is bounded and deterministic
Task prompt is vagueTask prompt defines exact deliverable
// ❌ Broken: open-ended task + repeated tool usage
import { Agent, Task, Crew } from "crewai";
import { z } from "zod";

const searchTool = {
  name: "search_policies",
  description: "Search policy docs",
  schema: z.object({ query: z.string() }),
  execute: async ({ query }: { query: string }) => {
    return `Partial results for ${query}`;
  },
};

const agent = new Agent({
  name: "Policy Analyst",
  role: "Analyze policy docs",
  goal: "Keep searching until you are sure you found everything",
  tools: [searchTool],
});

const task = new Task({
  description:
    "Search for every relevant policy and keep refining until complete.",
  expectedOutput: "A comprehensive answer.",
});

const crew = new Crew({
  agents: [agent],
  tasks: [task],
});

await crew.kickoff();
// ✅ Fixed: bounded task + explicit output contract
import { Agent, Task, Crew } from "crewai";
import { z } from "zod";

const searchTool = {
  name: "search_policies",
  description: "Search policy docs",
  schema: z.object({ query: z.string() }),
  execute: async ({ query }: { query: string }) => {
    return JSON.stringify({
      query,
      results: ["Policy A", "Policy B"],
      truncated: false,
    });
  },
};

const agent = new Agent({
  name: "Policy Analyst",
  role: "Analyze policy docs",
  goal: "Return one final answer using at most one tool call per document set",
  tools: [searchTool],
});

const task = new Task({
  description:
    "Return exactly 5 bullet points summarizing relevant policies. Stop after one search pass.",
  expectedOutput:
    "Exactly 5 bullets, each with policy name and one-line summary.",
});

const crew = new Crew({
  agents: [agent],
  tasks: [task],
});

await crew.kickoff();

The fix is not just “make the prompt better.” You need to make termination part of the contract:

  • limit tool calls
  • define exact output shape
  • avoid prompts like “keep going,” “refine,” or “search exhaustively”

Other Possible Causes

1) A tool returns data that causes the agent to retry forever

If a tool response looks incomplete or ambiguous, the model may call it again with slightly modified input.

// Bad
execute: async ({ query }) => `I found something related to ${query}`;

// Better
execute: async ({ query }) =>
  JSON.stringify({ query, hits: ["doc-1", "doc-2"], done: true });

Use structured output. Plain text invites repeated clarification loops.

2) Your stepCallback or logging hook re-triggers execution

A common TypeScript mistake is putting side effects inside callbacks that kick off another crew run.

// Bad
const crew = new Crew({
  agents,
  tasks,
  stepCallback: async (step) => {
    if (step.output.includes("retry")) {
      await crew.kickoff(); // recursion risk
    }
  },
});

Keep callbacks read-only.

// Good
const crew = new Crew({
  agents,
  tasks,
  stepCallback: async (step) => {
    console.log(step.output);
    // no kickoff here
  },
});

3) Your memory/state keeps feeding old unresolved context back in

If you append every intermediate message into memory without pruning, the agent may keep seeing the same unresolved instruction.

// Problematic pattern
memory.push(previousFailedAttempt);
memory.push(previousFailedAttempt);
memory.push(previousFailedAttempt);

Fix by storing only final state or a capped history:

const memory = conversationHistory.slice(-10);

4) Tool schemas are too loose

When a tool accepts any string and returns any shape, the model has no reliable stopping signal.

// Loose schema - risky
schema: z.object({
  input: z.any(),
})

Tighten it:

schema: z.object({
  customerId: z.string().uuid(),
})

Also return predictable JSON with a done flag when appropriate.

How to Debug It

  1. Disable all but one tool

    • Run the crew with a single deterministic tool.
    • If the loop disappears, one of your tools is causing retries.
  2. Log every agent step

    • Print step.input, step.output, and tool arguments.
    • Look for repeated identical calls like:
      • same prompt
      • same tool name
      • same arguments
  3. Remove callbacks and middleware

    • Temporarily strip stepCallback, custom memory adapters, retry wrappers, and tracing hooks.
    • If the error goes away, your orchestration layer is re-entering execution.
  4. Tighten the task contract

    • Replace vague instructions with exact deliverables.
    • Add hard limits:
      • max one search pass
      • max three tool calls
      • exact output format

A good debug trick is to compare the last three steps before failure. If they’re semantically identical, you’re in a retry loop. If they differ only slightly, your prompt is nudging the model toward self-correction without closure.

Prevention

  • Make every task end with a concrete artifact:

    • JSON object
    • fixed bullet count
    • single SQL query result summary
  • Keep tools deterministic and typed:

schema: z.object({
  accountId: z.string().uuid(),
})
  • Never call crew.kickoff() from inside callbacks, tools, or post-processing hooks.

  • Set explicit limits in prompts:

Use at most one tool call. If insufficient data remains after that call, return {"status":"incomplete"}.

If you’re seeing CrewAIError or repeated Agent step traces during local runs, start by fixing the task contract before touching framework internals. In practice, that resolves most agent infinite loop during development cases in TypeScript.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides