n8n-io/n8n · error · Error

Tool name "${toolName}" is reserved for runtime skills

Error message

Tool name "${toolName}" is reserved for runtime skills

What it means

Thrown by Agent.assertToolNameAvailable when a user-registered tool collides with a name in RUNTIME_SKILL_TOOL_NAMES (currently the skill-load tool). The SDK reserves those names for runtime skill tooling injected by .skills(). The check fires only when the agent already has a runtime skill tool registered (hasRuntimeSkillTool === true).

Source

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

					...(pendingSuspend.length > 0 ? { pendingSuspend } : {}),
					getState: () => resultStream.getState(),
				};
				return generateResultToDelegateSubAgentOutput(request.taskPath, result);
			} finally {
				await childRuntime.dispose();
			}
		};
	}

	private assertToolRegistrationAllowed(tool: BuiltTool): void {
		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;

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Rename your custom tool to anything outside RUNTIME_SKILL_TOOL_NAMES (imported from '@n8n/agents').
  2. Call .skills() AFTER registering custom tools only if no name collides; otherwise drop skills.
  3. If you genuinely need that name, do not enable .skills() on this agent.

Example fix

// before
agent.skills(source).tool({ name: SKILL_LOAD_TOOL_NAME, ... });
// after
agent.skills(source).tool({ name: 'my-skill-loader', ... });
Defensive patterns

Strategy: validation

Validate before calling

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

function assertToolNameFree(toolName: string) {
  if (RUNTIME_SKILL_TOOL_NAMES.has(toolName)) {
    throw new Error(`Refusing to register reserved skill tool name: ${toolName}`);
  }
}

Type guard

import { RUNTIME_SKILL_TOOL_NAMES } from '@n8n/agents';
function isReservedSkillToolName(name: string): boolean {
  return RUNTIME_SKILL_TOOL_NAMES.has(name);
}

Prevention

When it happens

Trigger: Calling agent.skills(source) first (which injects the skill-load tool), then agent.tool(myTool) where myTool.name equals the skill-load tool name. Also reachable via assertToolRegistrationAllowed running on a deferred tool that shares the name.

Common situations: Copying a tool name from SDK source (e.g. importing SKILL_LOAD_TOOL_NAME and reusing it); merging two agent configs where one enabled skills and the other registered a clashing tool; renaming a custom tool to something starting with the skill-load identifier.

Related errors


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