n8n-io/n8n · error · Error

Tool "${this.name}" has .resume() but missing .suspend()

Error message

Tool "${this.name}" has .resume() but missing .suspend()

What it means

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.

Source

Thrown at packages/@n8n/agents/src/sdk/tool.ts:377

		}
		if (!this.desc) {
			throw new Error(`Tool "${this.name}" requires a description`);
		}
		if (!this.inputSchema) {
			throw new Error(`Tool "${this.name}" requires an input schema`);
		}
		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,

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Add the matching .suspend(z.object({...})) defining the payload your handler yields when pausing.
  2. If resume was added by mistake, remove the .resume() call entirely.
  3. Confirm the suspend schema your handler yields matches (structurally) the resume schema the resume path will validate against.

Example fix

// before
const t = new Tool('ask_user')
  .description('Ask the user')
  .input(z.object({ q: z.string() }))
  .resume(z.object({ answer: z.string() }))
  .handler(...); // missing .suspend()

// after
const t = new Tool('ask_user')
  .description('Ask the user')
  .input(z.object({ q: z.string() }))
  .suspend(z.object({ q: z.string() }))
  .resume(z.object({ answer: z.string() }))
  .handler(...);
Defensive patterns

Strategy: validation

Validate before calling

function interruptibleTool(name: string, suspendSchema: unknown, resumeSchema: unknown) {
  if ((suspendSchema && !resumeSchema) || (resumeSchema && !suspendSchema)) {
    throw new Error('suspend and resume schemas must be provided together');
  }
  const t = new Tool(name);
  if (suspendSchema) t.suspend(suspendSchema as any);
  if (resumeSchema) t.resume(resumeSchema as any);
  return t;
}

Type guard

function isPairedSuspendResume(suspend?: unknown, resume?: unknown): boolean {
  return (suspend !== undefined) === (resume !== undefined);
}

Prevention

When it happens

Trigger: 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.

Common situations: Copy-pasting a resume schema from another tool without its suspend counterpart; refactoring that removes .suspend() but leaves .resume(); misunderstanding the pairing requirement.

Related errors


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