n8n-io/n8n · error · Error
Tool "${this.name}" requires a handler
Error message
Tool "${this.name}" requires a handler What it means
Tool.build() requires a handler function — the code that actually executes when the LLM invokes the tool. Without it the tool is metadata-only and cannot run. Set it via .handler(async (input, ctx) => {...}). A tool with a name, description, and input schema but no handler is incomplete and rejected at build.
Source
Thrown at packages/@n8n/agents/src/sdk/tool.ts:367
/**
* Validate configuration and produce a `BuiltTool`.
*
* @throws if name, description, input schema, or handler is missing.
* @throws if suspend is declared without resume or vice versa.
*/
build(): BuiltTool {
if (!this.name) {
throw new Error('Tool name is required');
}
if (!this.desc) {
throw new Error(`Tool "${this.name}" requires a description`);
}
if (!this.inputSchema) {
throw new Error(`Tool "${this.name}" requires an input schema`);
}
if (!this.handlerFn) {
throw new Error(`Tool "${this.name}" requires a handler`);
}
const hasSuspend = this.suspendSchemaValue !== undefined;
const hasResume = this.resumeSchemaValue !== undefined;
if (hasSuspend && !hasResume) {
throw new Error(`Tool "${this.name}" has .suspend() but missing .resume()`);
}
if (hasResume && !hasSuspend) {
throw new Error(`Tool "${this.name}" has .resume() but missing .suspend()`);
}
const hasApproval =
(this.requireApprovalValue ?? false) || this.needsApprovalFnValue !== undefined;
if (hasApproval && (hasSuspend || hasResume)) {
throw new Error(
`Tool "${this.name}" cannot use both approval (.requireApproval/.needsApprovalFn) and suspend/resume (.suspend/.resume)`,
);View on GitHub (pinned to 5ac6606e81)
Solutions
- Add .handler(async (input, ctx) => { ... }) to the builder chain.
- If the tool is metadata-only (e.g. for the JSON-config flow), call .describe() instead of letting it reach .build() via an agent.
- When attaching handlers conditionally, assert the handler reference is a function before calling .handler().
Example fix
// before
const t = new Tool('echo')
.description('Echo input')
.input(z.object({ msg: z.string() }));
agent.tool(t); // throws: requires a handler
// after
const t = new Tool('echo')
.description('Echo input')
.input(z.object({ msg: z.string() }))
.handler(async ({ msg }) => ({ echo: msg })); Defensive patterns
Strategy: type-guard
Validate before calling
function attachHandler(tool: Tool, fn: unknown) {
if (typeof fn !== 'function') {
throw new Error('Tool handler must be a function');
}
return tool.handler(fn as any);
} Type guard
function isHandler(fn: unknown): fn is (...args: any[]) => Promise<unknown> {
return typeof fn === 'function';
} Prevention
- Keep handler attachment adjacent to the tool definition in source so it isn't dropped during refactor.
- If splitting definition from handler, use a checklist or test that exercises agent.tool(...) to surface missing handlers early.
- For descriptor-only flows, call .describe() not .build(); for executable tools, always chain .handler().
When it happens
Trigger: Constructing a Tool, setting description and input, but forgetting .handler(...), then building. Also possible if .handler() is called with undefined due to an unfilled variable.
Common situations: Building a tool descriptor for inspection but accidentally passing it to an agent; refactoring a handler out into a variable that ends up undefined; splitting tool definition from handler attachment and missing the second step.
Related errors
- Tool name is required
- Tool "${this.name}" requires a description
- Tool "${this.name}" requires an input schema
- Tool "${this.name}" has .suspend() but missing .resume()
- Tool "${this.name}" has .resume() but missing .suspend()
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/0fef1db169353ed5.
Report an issue: GitHub.