n8n-io/n8n · error · Error
Tool "${this.name}" cannot use both approval (.requireApprov
Error message
Tool "${this.name}" cannot use both approval (.requireApproval/.needsApprovalFn) and suspend/resume (.suspend/.resume) What it means
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.
Source
Thrown at packages/@n8n/agents/src/sdk/tool.ts:383
}
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,
resumeSchema: this.resumeSchemaValue,
handleCancellation: this.handleCancellationValue,
toMessage: this.toMessageFn as (output: unknown) => AgentMessage | undefined,
toModelOutput: this.toModelOutputFn as ((output: unknown) => unknown) | undefined,
handler: this.handlerFn as (
input: unknown,
ctx: ToolContext | InterruptibleToolContext,
) => Promise<unknown>,
inputSchema: this.inputSchema,View on GitHub (pinned to 5ac6606e81)
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.
Example fix
// before — throws
const t = new Tool('dangerous')
.description('...')
.input(z.object({ cmd: z.string() }))
.requireApproval(true)
.suspend(z.object({ cmd: z.string() }))
.resume(z.object({ ok: z.boolean() }))
.handler(...);
// after — approval only
const t = new Tool('dangerous')
.description('...')
.input(z.object({ cmd: z.string() }))
.requireApproval(true)
.handler(...); Defensive patterns
Strategy: validation
Validate before calling
function configureInterrupts(tool: Tool, opts: { requireApproval?: boolean; needsApprovalFn?: Function; suspend?: unknown; resume?: unknown }) {
const hasApproval = opts.requireApproval === true || typeof opts.needsApprovalFn === 'function';
const hasSuspend = opts.suspend !== undefined || opts.resume !== undefined;
if (hasApproval && hasSuspend) {
throw new Error('Cannot combine approval with suspend/resume — pick one interrupt mechanism');
}
if (opts.requireApproval) tool.requireApproval(true);
if (opts.suspend) tool.suspend(opts.suspend as any);
if (opts.resume) tool.resume(opts.resume as any);
return tool;
} Type guard
function usesOnlyOneInterruptMechanism(opts: { requireApproval?: boolean; needsApprovalFn?: unknown; suspend?: unknown; resume?: unknown }): boolean {
const hasApproval = opts.requireApproval === true || typeof opts.needsApprovalFn === 'function';
const hasSuspend = opts.suspend !== undefined || opts.resume !== undefined;
return !(hasApproval && hasSuspend);
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Tool "${this.name}" has .suspend() but missing .resume()
- Tool "${this.name}" has .resume() but missing .suspend()
- 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/b09b6586cb59ba11.
Report an issue: GitHub.