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

  1. If you want simple yes/no human approval, remove .suspend()/.resume() and keep .requireApproval(true) — approval's suspend/resume schemas are wired automatically.
  2. 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.
  3. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/b09b6586cb59ba11. Report an issue: GitHub.