How to Fix 'duplicate tool calls' in CrewAI (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-callscrewaitypescript

If you’re seeing duplicate tool calls in CrewAI TypeScript, the agent is trying to invoke the same tool more than once in a single run path, or your app is registering the same tool multiple times. In practice, this usually shows up when an agent loops, a tool is attached twice, or your orchestration layer retries a step without clearing state.

The error often looks like this:

Error: duplicate tool calls detected for tool "search_tool"

or in some setups:

CrewAIError: duplicate tool calls are not allowed

The Most Common Cause

The #1 cause is attaching the same Tool instance more than once, or passing duplicate tool definitions into an agent/crew. In CrewAI TypeScript, this happens a lot when tools are created in one module and then spread into both the agent and the crew config.

Broken vs fixed pattern

BrokenFixed
Same tool instance registered twiceRegister the tool once
Tool array built from duplicated sourcesDeduplicate before passing to the agent
// ❌ Broken: same tool ends up in the agent twice
import { Agent } from "crewai";
import { SerperDevTool } from "./tools/SerperDevTool";

const searchTool = new SerperDevTool();

export const analyst = new Agent({
  name: "Analyst",
  role: "Research analyst",
  goal: "Find relevant company data",
  tools: [searchTool, searchTool], // duplicate
});
// ✅ Fixed: one instance, one registration
import { Agent } from "crewai";
import { SerperDevTool } from "./tools/SerperDevTool";

const searchTool = new SerperDevTool();

export const analyst = new Agent({
  name: "Analyst",
  role: "Research analyst",
  goal: "Find relevant company data",
  tools: [searchTool],
});

A more realistic version is when you merge arrays:

// ❌ Broken
const baseTools = [searchTool];
const researchTools = [searchTool, dbTool];

const tools = [...baseTools, ...researchTools]; // duplicates searchTool
// ✅ Fixed
const tools = [...new Map(
  [...baseTools, ...researchTools].map((tool) => [tool.name, tool])
).values()];

If your tools are class instances, dedupe by name or another stable identifier. Don’t rely on object identity if you recreate instances across modules.

Other Possible Causes

1) The agent is looping and calling the same tool repeatedly

This happens when the model keeps receiving incomplete output or your task prompt encourages repeated retrieval.

// ❌ Too open-ended; can trigger repeated calls
const agent = new Agent({
  role: "Researcher",
  goal: "Keep searching until you know everything",
  tools: [searchTool],
});
// ✅ Constrain the task and stop conditions
const agent = new Agent({
  role: "Researcher",
  goal: "Find exactly three sources about X",
  tools: [searchTool],
  maxIterations: 3,
});

2) Your retry wrapper replays the same step with preserved state

If you wrap crew.kickoff() in a retry library and reuse the same context object, you can accidentally replay prior tool calls.

// ❌ Reuses state across retries
await retry(async () => {
  return await crew.kickoff(sharedContext);
});
// ✅ Create fresh inputs per attempt
await retry(async () => {
  const context = { requestId: crypto.randomUUID() };
  return await crew.kickoff(context);
});

3) You register tools both globally and per-agent

Some codebases build a shared crew config and also attach tools directly to each agent. That duplicates execution paths.

// ❌ Tool appears in both places
const crew = new Crew({
  agents: [analyst],
  tools: [searchTool],
});

analyst.tools = [searchTool];
// ✅ Pick one registration point
const crew = new Crew({
  agents: [analyst],
});

analyst.tools = [searchTool];

4) Two different imports create two different instances of the same tool

This is common with factory functions or when a module is imported through different paths.

// ❌ Same logical tool created twice
import { makeSearchTool } from "./tools/search";
import { makeSearchTool as makeSearchToolAlt } from "../src/tools/search";

const t1 = makeSearchTool();
const t2 = makeSearchToolAlt();

If both produce name === "search_tool", CrewAI may treat them as duplicates.

// ✅ Export a singleton or central factory usage
import { searchTool } from "./tools/search-singleton";

agent.tools = [searchTool];

How to Debug It

  1. Log every tool name before kickoff

    console.log(agent.tools?.map((t) => t.name));
    

    If you see duplicates like ["search_tool", "search_tool"], that’s your issue.

  2. Check whether the same instance is added twice Compare references where possible:

    console.log(agent.tools?.[0] === agent.tools?.[1]);
    
  3. Inspect retries and callbacks If you use a retry library, middleware, or event handler around Crew.kickoff(), temporarily disable it and rerun. If the error disappears, your wrapper is replaying work.

  4. Reduce to one agent and one tool Remove all but one Agent, one Task, and one Tool. If it works in isolation, add pieces back until duplication returns.

Prevention

  • Keep tool registration in one place only.
  • Use singleton exports for shared tools instead of creating new instances in multiple files.
  • Add a startup assertion:
    const names = agent.tools?.map((t) => t.name) ?? [];
    if (new Set(names).size !== names.length) {
      throw new Error(`Duplicate tools detected: ${names.join(", ")}`);
      }
    
  • Set sane limits like maxIterations so an agent can’t loop forever calling the same tool.

If you’re debugging this in production code, start by printing tool.name arrays at every boundary where agents are assembled. In CrewAI TypeScript, this error is usually not about the model being “wrong”; it’s about duplicated wiring in your app.


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