How to Fix 'duplicate tool calls in production' in CrewAI (TypeScript)
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:
| Broken | Fixed |
|---|---|
| Calls crew execution inside a retried block | Runs once and deduplicates by request ID |
| No idempotency key | Uses stable task/request identity |
| Side-effect tool can fire twice | Tool 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
- •
Log every tool invocation with a request ID
- •Include
requestId,toolName,args, and timestamp. - •If you see the same
requestIdtwice, your app is re-entering execution. - •If you see different
requestIds with identical args, your queue/webhook is redelivering.
- •Include
- •
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.
- •
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.
- •Remove wrapper retries around
- •
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?
- •Add logs in:
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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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