{"record":{"id":"580bbd130b8707b8","repo":"n8n-io/n8n","slug":"tool-this-name-has-resume-but-missing-sus","errorCode":null,"errorMessage":"Tool \"${this.name}\" has .resume() but missing .suspend()","messagePattern":"Tool \"(.+?)\" has \\.resume\\(\\) but missing \\.suspend\\(\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/sdk/tool.ts","lineNumber":377,"sourceCode":"\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,\n\t\t\tresumeSchema: this.resumeSchemaValue,\n\t\t\thandleCancellation: this.handleCancellationValue,\n\t\t\ttoMessage: this.toMessageFn as (output: unknown) => AgentMessage | undefined,","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/sdk/tool.ts#L359-L395","documentation":"Tool.build() enforces that resume and suspend schemas are declared together. .resume(schema) defines the payload accepted when resuming a suspended tool, but a resume schema without a corresponding .suspend(schema) means there is no defined payload the tool yields to pause — the resume path could never be reached. The build rejects the orphan resume schema early.","triggerScenarios":"Calling .resume(z.object({...})) without a matching .suspend(...), then building. Fires after the suspend-without-resume check, so the message specifically names .resume() as the orphan.","commonSituations":"Copy-pasting a resume schema from another tool without its suspend counterpart; refactoring that removes .suspend() but leaves .resume(); misunderstanding the pairing requirement.","solutions":["Add the matching .suspend(z.object({...})) defining the payload your handler yields when pausing.","If resume was added by mistake, remove the .resume() call entirely.","Confirm the suspend schema your handler yields matches (structurally) the resume schema the resume path will validate against."],"exampleFix":"// before\nconst t = new Tool('ask_user')\n  .description('Ask the user')\n  .input(z.object({ q: z.string() }))\n  .resume(z.object({ answer: z.string() }))\n  .handler(...); // missing .suspend()\n\n// after\nconst t = new Tool('ask_user')\n  .description('Ask the user')\n  .input(z.object({ q: z.string() }))\n  .suspend(z.object({ q: 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":["Treat suspend and resume as a single interrupt declaration — never edit one without the other.","Use a code-search check before merging: any .suspend( must have a matching .resume( in the same builder chain.","Document the resume payload contract next to the suspend schema so the pairing isn't accidentally split."],"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-13T14:17:21.547Z"}