n8n-io/n8n · error · Error

Tool name "${tool.name}" is reserved for SDK built-in tools

Error message

Tool name "${tool.name}" is reserved for SDK built-in tools

What it means

Thrown by Agent.assertReservedSdkBuiltInToolName when a registered tool's name is in SDK_RESERVED_BUILTIN_TOOL_NAMES (currently 'delegate_subagent' and 'write_todos') but the tool is not the genuine SDK-built instance. Genuine status is detected by isDelegateSubAgentTool (for delegate_subagent) or isSdkOwnedBuiltInTool (metadata key 'sdkOwnedBuiltinTool' === true).

Source

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

		this.assertToolNameAvailable(tool.name);
		this.assertReservedSdkBuiltInToolName(tool);
	}

	private assertToolNameAvailable(toolName: string): void {
		if (!this.hasRuntimeSkillTool || !RUNTIME_SKILL_TOOL_NAMES.has(toolName)) return;

		throw new Error(`Tool name "${toolName}" is reserved for runtime skills`);
	}

	private assertReservedSdkBuiltInToolName(tool: BuiltTool): void {
		if (!SDK_RESERVED_BUILTIN_TOOL_NAMES.has(tool.name)) return;
		if (isDelegateSubAgentTool(tool)) {
			if (tool.name === DELEGATE_SUB_AGENT_TOOL_NAME) return;
		} else if (isSdkOwnedBuiltInTool(tool)) {
			return;
		}

		throw new Error(`Tool name "${tool.name}" is reserved for SDK built-in tools`);
	}

	private removeRuntimeSkillTools(): void {
		if (!this.hasRuntimeSkillTool) return;

		this.tools = this.tools.filter((tool) => !RUNTIME_SKILL_TOOL_NAMES.has(tool.name));
		this.hasRuntimeSkillTool = false;
	}
}

function resolveInlineSubAgentModelConfig(
	request: DelegateSubAgentRequest,
	options: {
		modelConfig: ModelConfig;
		inlineSubAgentModelsByDifficulty?: Partial<Record<SubAgentTaskDifficulty, ModelConfig>>;
	},
): ModelConfig {
	if (request.difficulty === undefined) {

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Use createDelegateSubAgentTool / the official write-todos factory so the tool carries correct branding and metadata.
  2. Rename your custom tool away from 'delegate_subagent' and 'write_todos'.
  3. If you must wrap an SDK built-in, brand it with withSdkOwnedBuiltInMetadata or pass the already-branded instance.

Example fix

// before
agent.tool({ name: 'write_todos', execute: async () => ... });
// after
agent.tool({ name: 'my-todos', execute: async () => ... });
Defensive patterns

Strategy: validation

Validate before calling

import { SDK_RESERVED_BUILTIN_TOOL_NAMES } from '@n8n/agents';

function assertNotReservedBuiltin(name: string) {
  if (SDK_RESERVED_BUILTIN_TOOL_NAMES.has(name)) {
    throw new Error(`Tool name ${name} collides with SDK built-in; rename or use the factory.`);
  }
}

Type guard

function isReservedBuiltinName(name: string): boolean {
  return name === 'delegate_subagent' || name === 'write_todos';
}

Prevention

When it happens

Trigger: Registering a hand-rolled tool whose name is 'delegate_subagent' or 'write_todos' without the SDK ownership metadata or delegate-tool branding. Triggered from .tool() / deferred tool registration through assertToolRegistrationAllowed.

Common situations: User copies the SDK built-in tool name by accident; a forked/modified tool loses the metadata stamp; constructing a delegate sub-agent tool with a custom factory that bypasses createDelegateSubAgentTool.

Related errors


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