{"record":{"id":"b09b6586cb59ba11","repo":"n8n-io/n8n","slug":"tool-this-name-cannot-use-both-approval-req","errorCode":null,"errorMessage":"Tool \"${this.name}\" cannot use both approval (.requireApproval/.needsApprovalFn) and suspend/resume (.suspend/.resume)","messagePattern":"Tool \"(.+?)\" cannot use both approval \\(\\.requireApproval/\\.needsApprovalFn\\) and suspend/resume \\(\\.suspend/\\.resume\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/sdk/tool.ts","lineNumber":383,"sourceCode":"\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,\n\t\t\ttoModelOutput: this.toModelOutputFn as ((output: unknown) => unknown) | undefined,\n\t\t\thandler: this.handlerFn as (\n\t\t\t\tinput: unknown,\n\t\t\t\tctx: ToolContext | InterruptibleToolContext,\n\t\t\t) => Promise<unknown>,\n\t\t\tinputSchema: this.inputSchema,","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/sdk/tool.ts#L365-L401","documentation":"Tool.build() rejects combining an approval gate (.requireApproval() or .needsApprovalFn()) with custom suspend/resume schemas. Both are interrupt mechanisms that suspend tool execution to wait for external input, but they use incompatible suspend payloads (approval uses a fixed approval-schema shape). Mixing them would produce ambiguous interrupt routing, so the build fails fast.","triggerScenarios":"Calling both .requireApproval(true) (or .needsApprovalFn(fn)) AND .suspend(...) and/or .resume(...) on the same Tool, then building. The check runs after suspend/resume pairing validation.","commonSituations":"Adding approval to an existing human-in-the-loop tool that already uses suspend/resume; copy-pasting approval config onto a tool with custom suspend logic; misunderstanding that approval IS a suspend mechanism internally.","solutions":["If you want simple yes/no human approval, remove .suspend()/.resume() and keep .requireApproval(true) — approval's suspend/resume schemas are wired automatically.","If you need a custom suspend payload (e.g. asking for arbitrary structured input, not just approve/deny), remove .requireApproval()/.needsApprovalFn() and keep your custom suspend/resume pair, handling approval logic inside the handler.","Split into two tools: one approval-gated wrapper and one suspend/resume-based core tool, if both behaviors are genuinely needed."],"exampleFix":"// before — throws\nconst t = new Tool('dangerous')\n  .description('...')\n  .input(z.object({ cmd: z.string() }))\n  .requireApproval(true)\n  .suspend(z.object({ cmd: z.string() }))\n  .resume(z.object({ ok: z.boolean() }))\n  .handler(...);\n\n// after — approval only\nconst t = new Tool('dangerous')\n  .description('...')\n  .input(z.object({ cmd: z.string() }))\n  .requireApproval(true)\n  .handler(...);","handlingStrategy":"validation","validationCode":"function configureInterrupts(tool: Tool, opts: { requireApproval?: boolean; needsApprovalFn?: Function; suspend?: unknown; resume?: unknown }) {\n  const hasApproval = opts.requireApproval === true || typeof opts.needsApprovalFn === 'function';\n  const hasSuspend = opts.suspend !== undefined || opts.resume !== undefined;\n  if (hasApproval && hasSuspend) {\n    throw new Error('Cannot combine approval with suspend/resume — pick one interrupt mechanism');\n  }\n  if (opts.requireApproval) tool.requireApproval(true);\n  if (opts.suspend) tool.suspend(opts.suspend as any);\n  if (opts.resume) tool.resume(opts.resume as any);\n  return tool;\n}","typeGuard":"function usesOnlyOneInterruptMechanism(opts: { requireApproval?: boolean; needsApprovalFn?: unknown; suspend?: unknown; resume?: unknown }): boolean {\n  const hasApproval = opts.requireApproval === true || typeof opts.needsApprovalFn === 'function';\n  const hasSuspend = opts.suspend !== undefined || opts.resume !== undefined;\n  return !(hasApproval && hasSuspend);\n}","tryCatchPattern":null,"preventionTips":["Decide upfront whether a tool needs simple approval or a custom suspend payload — do not combine.","Centralize interrupt configuration in one helper per tool so the mutual exclusion is enforced structurally.","When migrating a tool from custom suspend to approval (or vice versa), remove all traces of the old mechanism in the same change."],"tags":["tool","builder","validation","approval","suspend-resume","mutually-exclusive"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}