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
- Rename your custom tool to anything outside RUNTIME_SKILL_TOOL_NAMES (imported from '@n8n/agents').
- Call .skills() AFTER registering custom tools only if no name collides; otherwise drop skills.
- 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
- Validate tool names against RUNTIME_SKILL_TOOL_NAMES before .tool() when skills are enabled.
- Namespace custom tool names (e.g. 'app_*') to avoid landing on reserved identifiers.
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
- Tool name "${reservedTool.name}" is reserved for runtime ski
- Deferred tool name "${tool.name}" is reserved
- Deferred tool name collision — the following tool names reso
- Tool name "${tool.name}" is reserved for SDK built-in tools
- Duplicate deferred tool name "${tool.name}"
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/9931017728866ff2.
Report an issue: GitHub.