n8n-io/n8n · error · Error
Tool " " requires an input schema
Error message
Tool "${this.name}" requires an input schema What it means
Tool.build() requires an input schema because the runtime needs it to validate the LLM's tool-call arguments, generate the model-facing JSON schema, and type the handler's first argument. Set it via .input(zodSchema) or .input(jsonSchema). Without it, the agent loop cannot serialize the tool for the model.
Solutions
- Add .input(z.object({...})) with a Zod schema describing the tool's arguments.
- For tools that take no meaningful input, pass z.object({}).describe('no arguments') rather than skipping .input().
- If loading schemas dynamically, assert the schema is defined before calling .input().
Example fix
// before
const t = new Tool('ping').description('Health check').handler(async () => 'ok');
agent.tool(t); // throws: requires an input schema
// after
const t = new Tool('ping')
.description('Health check')
.input(z.object({}).describe('no arguments'))
.handler(async () => 'ok'); Defensive patterns
Strategy: type-guard
Validate before calling
import { isZodSchema } from '@n8n/agents/utils/zod'; // or local util
function attachInput(tool: Tool, schema: unknown) {
if (!isZodSchema(schema) && !(schema && typeof schema === 'object' && 'type' in schema)) {
throw new Error('Input schema must be a Zod schema or JSON Schema object');
}
return tool.input(schema as any);
} Type guard
function isInputSchema(value: unknown): value is import('zod').ZodType | import('json-schema').JSONSchema7 {
if (!value) return false;
// Zod check
if (typeof value === 'object' && '_def' in value && typeof (value as any)._def === 'object') return true;
// JSON Schema check
if (typeof value === 'object' && 'type' in value) return true;
return false;
} Prevention
- Always pair a handler with its input schema in the same builder block so they aren't separated.
- For no-arg tools, pass z.object({}) explicitly rather than skipping .input().
- When loading schemas from external sources, assert they parse as Zod or JSON Schema before attaching.
When it happens
Trigger: Calling new Tool('name').description('...').handler(...) and omitting .input(...), then building. Also triggered when .input() is called with undefined due to a variable that wasn't set.
Common situations: Forgetting .input() in a quick prototype; passing an undefined schema variable; assuming the handler will accept raw args without schema validation.
Related errors
- Invalid discovery test case
- Tool name is required
- Tool " " cannot use both approval…
- Tool " " has .resume() but missing .suspend()
- Tool " " has .suspend() but missing .resume()
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/eadcf646ca9ba9b1.
Report an issue: GitHub.
Appendix: source
Thrown at packages/@n8n/agents/src/sdk/tool.ts:364
this.providerOptionsValue = { ...this.providerOptionsValue, ...options };
return this;
}
/**
* 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)) {View on GitHub (pinned to 5ac6606e81)