n8n-io/n8n · error · Error

Deferred tool name "${tool.name}" is reserved

Error message

Deferred tool name "${tool.name}" is reserved

What it means

`DeferredToolManager` exposes two built-in controller tools — `search_tools` and `load_tool` — that let the agent discover and load tools on demand. To avoid name collisions, any tool passed to the constructor whose `name` matches these reserved names throws immediately. The manager cannot function if a deferred tool shadows its own controllers.

Source

Thrown at packages/@n8n/agents/src/runtime/tools/deferred-tool-manager.ts:93

export interface DeferredToolManagerOptions {
	topK?: number;
}

export class DeferredToolManager {
	private readonly toolsByName = new Map<string, BuiltTool>();

	private readonly loadedToolNames = new Set<string>();

	private readonly topK: number;

	private readonly searchTool: BuiltTool;

	private readonly loadTool: BuiltTool;

	constructor(tools: BuiltTool[], options: DeferredToolManagerOptions = {}) {
		for (const tool of tools) {
			if (tool.name === SEARCH_TOOLS_TOOL_NAME || tool.name === LOAD_TOOL_TOOL_NAME) {
				throw new Error(`Deferred tool name "${tool.name}" is reserved`);
			}
			if (this.toolsByName.has(tool.name)) {
				throw new Error(`Duplicate deferred tool name "${tool.name}"`);
			}
			this.toolsByName.set(tool.name, tool);
		}

		this.topK = options.topK ?? DEFAULT_TOP_K;
		this.searchTool = this.createSearchTool();
		this.loadTool = this.createLoadTool();
	}

	get hasTools(): boolean {
		return this.toolsByName.size > 0;
	}

	get totalToolCount(): number {
		return this.toolsByName.size;

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Rename the conflicting tool to something other than `search_tools` or `load_tool`.
  2. If the name comes from an external source (MCP server, tool catalog), map/rename it before passing to the constructor.
  3. Filter or validate tool names against `[SEARCH_TOOLS_TOOL_NAME, LOAD_TOOL_TOOL_NAME]` before constructing the manager.

Example fix

// before:
const mgr = new DeferredToolManager([
  { name: 'search_tools', ... }, // collides with built-in
]);

// after:
const mgr = new DeferredToolManager([
  { name: 'find_integrations', ... }, // renamed
]);
Defensive patterns

Strategy: validation

Validate before calling

import { SEARCH_TOOLS_TOOL_NAME, LOAD_TOOL_TOOL_NAME } from './deferred-tool-manager';

const RESERVED = new Set([SEARCH_TOOLS_TOOL_NAME, LOAD_TOOL_TOOL_NAME]);

function sanitizeToolNames(tools: BuiltTool[]): BuiltTool[] {
  return tools.filter((t) => {
    if (RESERVED.has(t.name)) {
      logger.warn(`Tool name "${t.name}" is reserved, skipping`);
      return false;
    }
    return true;
  });
}

const mgr = new DeferredToolManager(sanitizeToolNames(allTools));

Type guard

function isNotReservedToolName(tool: BuiltTool): boolean {
  return tool.name !== 'search_tools' && tool.name !== 'load_tool';
}

Prevention

When it happens

Trigger: Passing a tool with `name: 'search_tools'` or `name: 'load_tool'` to `new DeferredToolManager([...])`. This happens when a user-defined or integration tool happens to use one of these names.

Common situations: A custom tool or MCP-imported tool was named `search_tools` or `load_tool`. A tool catalog contains a tool with a colliding name. A code generation or naming convention accidentally produced a reserved name.

Related errors


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