n8n-io/n8n · error · Error

No pending tool call found for toolCallId: ${resumedId}

Error message

No pending tool call found for toolCallId: ${resumedId}

What it means

Thrown during tool-call resume when pendingResume.pendingToolCalls does not contain an entry for the resumeToolCallId. The resume path expects to find the previously-suspended tool call in the pending map so it can re-execute it with the resume data. A missing entry means the checkpoint referenced a tool call that was never suspended or has already been completed/cleaned up.

Source

Thrown at packages/@n8n/agents/src/runtime/tools/tool-call-executor.ts:501

	 * Returns a `ToolCallBatchResult` — the caller handles persistence.
	 */
	async iteratePendingToolCallsConcurrent(
		ctx: ToolBatchContext & { pendingResume: PendingResume },
	): Promise<ToolCallBatchResult> {
		const {
			pendingResume,
			toolMap,
			list,
			runId,
			persistence,
			telemetry: resolvedTelemetry,
			executionCounter,
			abortSignal,
		} = ctx;
		const resumedId = pendingResume.resumeToolCallId;
		const resumedEntry = pendingResume.pendingToolCalls[resumedId];
		if (!resumedEntry) {
			throw new Error(`No pending tool call found for toolCallId: ${resumedId}`);
		}

		const resumedToolName = resumedEntry.toolName;
		const results: ToolCallSuccess[] = [];
		const suspensions: ToolCallSuspension[] = [];
		const errors: ToolCallError[] = [];
		const pending: Record<string, PendingToolCall> = {};

		// 1. Execute the resumed tool
		const processResult = await this.processToolCall(
			this.pendingToolCallParams(resumedEntry, ctx, pendingResume.resumeData),
		);

		if (processResult.outcome === 'suspended') {
			pending[resumedId] = {
				suspended: true,
				toolCallId: resumedEntry.toolCallId,
				toolName: resumedToolName,

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Ensure each suspended execution is resumed exactly once — use idempotency keys or a lock on the execution ID.
  2. Verify the checkpoint store correctly persists and restores the pendingToolCalls map.
  3. If the resume token is stale, start a fresh execution instead of resuming.
  4. Add logging to capture the available pendingToolCallIds vs the requested resumeToolCallId at resume time.
Defensive patterns

Strategy: validation

Validate before calling

function hasPendingToolCall(pendingToolCalls: Record<string, unknown>, resumeToolCallId: string): boolean {
  return resumeToolCallId in pendingToolCalls;
}
if (!hasPendingToolCall(ctx.pendingToolCalls, resumeData.resumeToolCallId)) {
  // do not attempt resume; start a new execution
}

Try / catch

try {
  await executor.resume(pendingResume);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('No pending tool call found')) {
    // resume token is stale; start a fresh execution
  } else throw e;
}

Prevention

When it happens

Trigger: Resuming an execution with a resumeToolCallId that does not match any key in pendingToolCalls. This happens if the checkpoint store returns a stale or already-consumed resume token, if the tool call was completed in a separate resume attempt, or if the pending map was reconstructed incorrectly from persistence.

Common situations: Double-resume: the same suspended execution is resumed twice (the first resume consumed the pending entry). Checkpoint store does not enforce single-consumer semantics. Race condition where two resume requests hit the same execution. Checkpoint data corruption or schema migration that lost the pending tool call map.

Related errors


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