n8n-io/n8n · error · Error

Static tool name collision — the following tool names resolv

Error message

Static tool name collision — the following tool names resolve to duplicates: ${staticCollisions.join(', ')}

What it means

Thrown by Agent.build() when findDuplicateToolNames(finalStaticTools) returns a non-empty list, meaning two or more statically-registered tools share the same name. Tool names must be unique within an agent because the LLM uses them to select which tool to call, and the runtime dispatches by name — duplicates create ambiguity.

Source

Thrown at packages/@n8n/agents/src/sdk/agent.ts:1001

			finalDeferredTools.some((t) => t.suspendSchema);
		const mcpNeedsCheckpoint = this.mcpClients.some((c) => c.declaresApproval());
		if ((staticNeedsCheckpoint || mcpNeedsCheckpoint) && !this.checkpointStore) {
			throw new Error(
				`Agent "${this.name}" has tools requiring approval or suspend/resume but no checkpoint storage. ` +
					"Add .checkpoint('memory') for in-process storage, " +
					'or pass a persistent store (e.g. LibSQLStore, PgStore).',
			);
		}

		// Resolve tools from all MCP clients.
		const mcpToolLists = await Promise.all(this.mcpClients.map(async (c) => await c.listTools()));
		const mcpTools = ensureUniqueMcpToolNames(mcpToolLists.flat());
		const mcpConnectionFailures = this.getMcpConnectionFailures();

		// Detect collisions between direct, deferred, and MCP tools.
		const staticCollisions = findDuplicateToolNames(finalStaticTools);
		if (staticCollisions.length > 0) {
			throw new Error(
				`Static tool name collision — the following tool names resolve to duplicates: ${staticCollisions.join(', ')}`,
			);
		}

		const staticNames = new Set(finalStaticTools.map((t) => t.name));
		const reservedDeferredToolNames = new Set([
			SEARCH_TOOLS_TOOL_NAME,
			LOAD_TOOL_TOOL_NAME,
			...RUNTIME_SKILL_TOOL_NAMES,
		]);
		const deferredNames = new Set<string>();
		const deferredCollisions: string[] = [];
		for (const tool of finalDeferredTools) {
			if (
				staticNames.has(tool.name) ||
				reservedDeferredToolNames.has(tool.name) ||
				deferredNames.has(tool.name)
			) {

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Inspect the error message which lists the colliding names, then rename the duplicate tools.
  2. Use a namespace prefix when combining tools from multiple sources (e.g. 'github_search' vs 'web_search').
  3. Dedupe the tool array before passing it to .tools() using a Set or Map keyed by name.

Example fix

// before
agent.tools([
  { name: 'search', ... },
  { name: 'search', ... }, // collision
]);
// after
agent.tools([
  { name: 'web_search', ... },
  { name: 'db_search', ... },
]);
Defensive patterns

Strategy: validation

Validate before calling

function findDuplicates(tools: { name: string }[]): string[] {
  const seen = new Set<string>();
  const dupes = new Set<string>();
  for (const t of tools) {
    if (seen.has(t.name)) dupes.add(t.name);
    seen.add(t.name);
  }
  return [...dupes];
}
const dupes = findDuplicates(myTools);
if (dupes.length > 0) {
  // rename before passing to .tools()
}

Prevention

When it happens

Trigger: Calling agent.tools([toolA, toolB]) where toolA.name === toolB.name. Or calling .tools() twice with tools that have overlapping names. Also possible when importing tool arrays from different modules that happen to share a name.

Common situations: Two different MCP servers or tool factories producing tools with generic names like 'search' or 'get'. Copying a tool and forgetting to rename it. Registering both a custom tool and a library-provided tool with the same name. Dynamic tool generation that does not enforce uniqueness.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/7cb71590d04822c3. Report an issue: GitHub.