n8n-io/n8n · error · Error
Tool "${this.name}" has .suspend() but missing .resume()
Error message
Tool "${this.name}" has .suspend() but missing .resume() What it means
Tool.build() enforces that suspend and resume schemas are declared together. .suspend(schema) defines the payload the tool yields when pausing for external input; .resume(schema) defines the payload accepted when resuming. A suspend without a resume leaves the tool unable to accept resume input, so the agent loop could never continue it — rejected at build to surface the mismatch early.
Source
Thrown at packages/@n8n/agents/src/sdk/tool.ts:374
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)`,
);
}
const built: BuiltTool = {
name: this.name,
description: this.desc,
systemInstruction: this.systemInstructionText,
suspendSchema: this.suspendSchemaValue,View on GitHub (pinned to 5ac6606e81)
Solutions
- Add a matching .resume(z.object({...})) call defining the payload your resume path will send back.
- If you only need approval-style interruption (not a custom resume payload), remove .suspend() and use .requireApproval(true) instead — the approval schemas are wired automatically.
- Review the agent's resume entry point to ensure the resume schema matches what it will actually deliver.
Example fix
// before
const t = new Tool('ask_user')
.description('Ask the user a question')
.input(z.object({ question: z.string() }))
.suspend(z.object({ question: z.string() }))
.handler(...); // missing .resume()
// after
const t = new Tool('ask_user')
.description('Ask the user a question')
.input(z.object({ question: z.string() }))
.suspend(z.object({ question: z.string() }))
.resume(z.object({ answer: z.string() }))
.handler(...); Defensive patterns
Strategy: validation
Validate before calling
function interruptibleTool(name: string, suspendSchema: unknown, resumeSchema: unknown) {
if ((suspendSchema && !resumeSchema) || (resumeSchema && !suspendSchema)) {
throw new Error('suspend and resume schemas must be provided together');
}
const t = new Tool(name);
if (suspendSchema) t.suspend(suspendSchema as any);
if (resumeSchema) t.resume(resumeSchema as any);
return t;
} Type guard
function isPairedSuspendResume(suspend?: unknown, resume?: unknown): boolean {
return (suspend !== undefined) === (resume !== undefined);
} Prevention
- Always declare suspend and resume schemas as a pair in the same edit.
- Add a unit test that exercises the full suspend/resume cycle so a missing half fails CI.
- Prefer .requireApproval(true) for simple approve/deny flows — its schemas are auto-paired.
When it happens
Trigger: Calling .suspend(z.object({...})) without a matching .resume(...), then building. The check fires after name/description/input/handler validation and before the approval-combination check.
Common situations: Adding a suspend step to an existing tool and forgetting the resume schema; copy-pasting half of a human-in-the-loop pattern; refactoring that drops one of the pair.
Related errors
- Tool "${this.name}" has .resume() but missing .suspend()
- Tool "${this.name}" cannot use both approval (.requireApprov
- Tool name is required
- Tool "${this.name}" requires a description
- Tool "${this.name}" requires an input schema
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/b2b47f7474e32d6d.
Report an issue: GitHub.