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

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-calls-in-productioncrewaitypescript

What the error means

duplicate tool calls in production usually means your agent executed the same tool invocation more than once for a single task. In CrewAI TypeScript, this typically shows up when the agent retries, the task is re-run by your app logic, or your tool layer is not idempotent.

In practice, you’ll see this when a tool has side effects: writing to a database, creating tickets, sending emails, or calling an external API that does not tolerate duplicate requests.

The Most Common Cause

The #1 cause is calling kickoff() or run() more than once for the same request path, often because the code sits inside a retry wrapper, an HTTP handler that gets re-entered, or a React/serverless function that executes twice.

Here’s the broken pattern:

BrokenFixed
Calls crew execution inside a retried blockRuns once and deduplicates by request ID
No idempotency keyUses stable task/request identity
Side-effect tool can fire twiceTool checks whether work already happened
// BROKEN
import { Crew, Agent, Task } from "@crew-ai/crewai";

const agent = new Agent({
  role: "Ops Agent",
  goal: "Create a support ticket",
  backstory: "Handles customer escalations",
  tools: [createTicketTool],
});

const task = new Task({
  description: "Create a ticket for order #12345",
  agent,
});

export async function handler(req: Request) {
  const crew = new Crew({ agents: [agent], tasks: [task] });

  // This wrapper retries on any error.
  // If the first attempt partially succeeds, the tool may be called again.
  return await retry(async () => {
    return await crew.kickoff();
  });
}
// FIXED
import { Crew, Agent, Task } from "@crew-ai/crewai";

const processedRequests = new Set<string>();

export async function handler(req: Request) {
  const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();

  if (processedRequests.has(requestId)) {
    return new Response(JSON.stringify({ status: "duplicate_ignored" }), {
      status: 200,
    });
  }

  processedRequests.add(requestId);

  const agent = new Agent({
    role: "Ops Agent",
    goal: "Create a support ticket",
    backstory: "Handles customer escalations",
    tools: [createTicketTool],
  });

  const task = new Task({
    description: `Create a ticket for order #12345. Request ID: ${requestId}`,
    agent,
  });

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

  return new Response(JSON.stringify(result), { status: 200 });
}

If you’re seeing something like:

  • Error: duplicate tool calls in production
  • CrewAIError: Tool invocation already processed
  • AgentExecutor attempted to call tool twice for the same step

then start by checking whether your app is triggering the same execution path twice.

Other Possible Causes

1) Non-idempotent tool implementation

If your tool creates records without checking whether they already exist, any retry becomes a duplicate write.

// BAD
export const createTicketTool = {
  name: "create_ticket",
  execute: async ({ orderId }: { orderId: string }) => {
    return db.tickets.insert({
      orderId,
      status: "open",
    });
  },
};
// GOOD
export const createTicketTool = {
  name: "create_ticket",
  execute: async ({ orderId }: { orderId: string }) => {
    const existing = await db.tickets.findFirst({ where: { orderId } });
    if (existing) return existing;

    return db.tickets.insert({
      orderId,
      status: "open",
    });
  },
};

2) Duplicate event delivery from your queue/webhook

SQS, Pub/Sub, Stripe webhooks, and most job systems are at-least-once delivery by default. If you feed each event straight into CrewAI, duplicates are expected unless you dedupe upstream.

// BAD
queue.on("message", async (msg) => {
  await crew.kickoff();
});
// GOOD
queue.on("message", async (msg) => {
  if (await dedupeStore.has(msg.id)) return;
  await dedupeStore.put(msg.id);

  await crew.kickoff();
});

3) Multiple agent steps referencing the same tool output

Sometimes the model asks for the same tool twice because your prompt is vague or your task allows repeated execution.

const task = new Task({
  description:
    "Check account status and update CRM if needed. Do whatever is necessary.",
});

Make it explicit:

const task = new Task({
  description:
    "Call account_status exactly once. If CRM update is required, perform it once only after confirming status.",
});

4) Shared mutable state across concurrent requests

If you store “current tool call” in module scope, concurrent requests can collide and look like duplicates.

// BAD
let lastToolCallId: string | null = null;

export async function run() {
  lastToolCallId = crypto.randomUUID();
  return crew.kickoff();
}

Prefer request-scoped state:

export async function run() {
  const context = { requestId: crypto.randomUUID() };
  return crew.kickoff({ context });
}

How to Debug It

  1. Log every tool invocation with a request ID

    • Include requestId, toolName, args, and timestamp.
    • If you see the same requestId twice, your app is re-entering execution.
    • If you see different requestIds with identical args, your queue/webhook is redelivering.
  2. Check whether the duplicate happens before or after side effects

    • If DB rows or tickets already exist before the error surfaces, your tool needs idempotency.
    • If nothing was written yet, look at retries around crew.kickoff() or HTTP handlers.
  3. Disable retries temporarily

    • Remove wrapper retries around kickoff().
    • Turn off platform retries in serverless functions and queue consumers.
    • Re-run once and see if the error disappears.
  4. Trace one full execution path

    • Add logs in:
      • webhook/queue handler
      • Crew construction
      • task creation
      • each custom tool’s execute()
    • You want to answer one question: who called the tool twice?

Prevention

  • Make every side-effecting tool idempotent.
  • Use stable request IDs or idempotency keys from entrypoint to database row.
  • Keep CrewAI execution single-shot per request; do not wrap kickoff() in blind retries.
  • Deduplicate events before they enter your agent pipeline.
  • Write prompts that constrain tools clearly:
    • “call once”
    • “do not repeat”
    • “skip if already processed”

If you’re building this for production banking or insurance workflows, treat tools like transaction handlers. The agent can be probabilistic; your side effects cannot be.


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