n8n-io/n8n · warning · Error

No checkpoint found for runId: ${this.runId}

Error message

No checkpoint found for runId: ${this.runId}

What it means

Thrown by the merge-node validator when a Merge node has fewer than two distinct connected inputs. Merge nodes combine multiple branches, so a single input defeats their purpose and usually indicates a wiring mistake (e.g. both branches connected to the same input index).

Source

Thrown at packages/@n8n/agents/src/runtime/loop/agent-runtime.ts:514

		}
	}

	/**
	 * Durable-log RFC (resilience phase): re-drive a run from a `running`-status
	 * step checkpoint after a process crash. Unlike resume(), there is no
	 * pending tool call to settle — the checkpoint was written at a step
	 * boundary — so the loop re-enters directly at the next model call.
	 * `contextNotes` are appended as user messages before the model call: the
	 * host uses them to surface interrupted tool calls ("effect unverified —
	 * verify before retrying") and undrained steering corrections recovered
	 * from its durable event log. Tool calls are never re-executed mechanically.
	 */
	async crashResume(
		options: { runId: string; contextNotes?: string[] } & ExecutionOptions,
	): Promise<StreamResult> {
		this.runId = options.runId;
		const state = await this.runState.loadForCrashResume(this.runId);
		if (!state) throw new Error(`No checkpoint found for runId: ${this.runId}`);
		if (state.status !== 'running') {
			throw new Error(
				`Checkpoint for runId ${this.runId} has status '${state.status}' — crashResume only accepts step checkpoints; use resume() for suspended runs`,
			);
		}
		// A claimed HITL resume also persists as 'running' but still carries its
		// pending tool calls; re-driving it would skip settling them. Step
		// checkpoints are always written with empty pendingToolCalls.
		if (Object.keys(state.pendingToolCalls).length > 0) {
			throw new Error(
				`Checkpoint for runId ${this.runId} has pending tool calls — crashResume only accepts step checkpoints`,
			);
		}

		const list = AgentMessageList.deserialize(state.messageList);
		this.context.hydrateDeferredToolsFromList(list);
		await hydrateFileParts(list.messages(), this.config.fileStore, {
			threadId: state.persistence?.threadId,

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Wire each upstream branch to a distinct input: .to(mergeNode.input(0)) and .to(mergeNode.input(1)).
  2. Verify the Merge node's input mode (append/combine/chooseBranch) matches how many branches you connect.
  3. If only one branch is genuinely needed, remove the Merge node and connect the branch directly.

Example fix

// before — both branches land on input 0
branchA.to(mergeNode);
branchB.to(mergeNode);

// after
branchA.to(mergeNode.input(0));
branchB.to(mergeNode.input(1));
Defensive patterns

Strategy: validation

Validate before calling

function distinctInputCount(connections: Array<{ target?: { inputIndex?: number }; targetInputIndex?: number }>): number {
  const set = new Set<number>();
  for (const c of connections) {
    const idx = c.target?.inputIndex ?? c.targetInputIndex ?? 0;
    set.add(idx);
  }
  return set.size;
}

// before validating a Merge node:
if (distinctInputCount(incomingConnections) < 2) {
  throw new Error('Merge node needs >= 2 distinct inputs; use .to(merge.input(0)) and .to(merge.input(1)).');
}

Prevention

When it happens

Trigger: The validator collects the set of distinct inputIndex/targetInputIndex values across all connections targeting the Merge node; if connectedInputIndices.size < 2, the issue fires. This happens when no connection targets input 1, or every connection targets the same input index.

Common situations: Both upstream branches are wired with .to(mergeNode) (defaults to input 0); an AI builder forgets to specify .input(1); a branch was deleted leaving only one input connected.

Related errors


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