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

  1. Add .handler(async (input, ctx) => { ... }) to the builder chain.
  2. If the tool is metadata-only (e.g. for the JSON-config flow), call .describe() instead of letting it reach .build() via an agent.
  3. 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

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


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