{"record":{"id":"b2b47f7474e32d6d","repo":"n8n-io/n8n","slug":"tool-this-name-has-suspend-but-missing-re","errorCode":null,"errorMessage":"Tool \"${this.name}\" has .suspend() but missing .resume()","messagePattern":"Tool \"(.+?)\" has \\.suspend\\(\\) but missing \\.resume\\(\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/sdk/tool.ts","lineNumber":374,"sourceCode":"\tbuild(): BuiltTool {\n\t\tif (!this.name) {\n\t\t\tthrow new Error('Tool name is required');\n\t\t}\n\t\tif (!this.desc) {\n\t\t\tthrow new Error(`Tool \"${this.name}\" requires a description`);\n\t\t}\n\t\tif (!this.inputSchema) {\n\t\t\tthrow new Error(`Tool \"${this.name}\" requires an input schema`);\n\t\t}\n\t\tif (!this.handlerFn) {\n\t\t\tthrow new Error(`Tool \"${this.name}\" requires a handler`);\n\t\t}\n\n\t\tconst hasSuspend = this.suspendSchemaValue !== undefined;\n\t\tconst hasResume = this.resumeSchemaValue !== undefined;\n\n\t\tif (hasSuspend && !hasResume) {\n\t\t\tthrow new Error(`Tool \"${this.name}\" has .suspend() but missing .resume()`);\n\t\t}\n\t\tif (hasResume && !hasSuspend) {\n\t\t\tthrow new Error(`Tool \"${this.name}\" has .resume() but missing .suspend()`);\n\t\t}\n\n\t\tconst hasApproval =\n\t\t\t(this.requireApprovalValue ?? false) || this.needsApprovalFnValue !== undefined;\n\t\tif (hasApproval && (hasSuspend || hasResume)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Tool \"${this.name}\" cannot use both approval (.requireApproval/.needsApprovalFn) and suspend/resume (.suspend/.resume)`,\n\t\t\t);\n\t\t}\n\n\t\tconst built: BuiltTool = {\n\t\t\tname: this.name,\n\t\t\tdescription: this.desc,\n\t\t\tsystemInstruction: this.systemInstructionText,\n\t\t\tsuspendSchema: this.suspendSchemaValue,","sourceCodeStart":356,"sourceCodeEnd":392,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/sdk/tool.ts#L356-L392","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst t = new Tool('ask_user')\n  .description('Ask the user a question')\n  .input(z.object({ question: z.string() }))\n  .suspend(z.object({ question: z.string() }))\n  .handler(...); // missing .resume()\n\n// after\nconst t = new Tool('ask_user')\n  .description('Ask the user a question')\n  .input(z.object({ question: z.string() }))\n  .suspend(z.object({ question: z.string() }))\n  .resume(z.object({ answer: z.string() }))\n  .handler(...);","handlingStrategy":"validation","validationCode":"function interruptibleTool(name: string, suspendSchema: unknown, resumeSchema: unknown) {\n  if ((suspendSchema && !resumeSchema) || (resumeSchema && !suspendSchema)) {\n    throw new Error('suspend and resume schemas must be provided together');\n  }\n  const t = new Tool(name);\n  if (suspendSchema) t.suspend(suspendSchema as any);\n  if (resumeSchema) t.resume(resumeSchema as any);\n  return t;\n}","typeGuard":"function isPairedSuspendResume(suspend?: unknown, resume?: unknown): boolean {\n  return (suspend !== undefined) === (resume !== undefined);\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["tool","builder","validation","suspend-resume","human-in-the-loop"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}