n8n-io/n8n · error · StaleResumeError

Run ${this.runId} is not suspended. Cannot resume.

Error message

Run ${this.runId} is not suspended. Cannot resume.

What it means

Thrown by the memory-session-key validator when an AI memory subnode's session key expression uses $json. $json resolves to the current node's input, which is fragile and ambiguous for session keys — it can produce unstable or colliding sessions. The validator requires an explicit node reference so the key is deterministic.

Source

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

					`Cannot decrease maxIterations when resuming a run. Expected >= ${persistedMaxIterations}, received ${callerMaxIterations}.`,
				);
			}

			const mergedMaxIterations = callerMaxIterations ?? persistedMaxIterations;
			const mergedExecOptions: ExecutionOptions & { iterationCount?: number } = {
				...callerExecOptions,
				...(mergedMaxIterations !== undefined ? { maxIterations: mergedMaxIterations } : {}),
				...(state.iterationCount !== undefined ? { iterationCount: state.iterationCount } : {}),
			};

			const resumeOptions: RuntimeExecutionOptions = {
				persistence: state.persistence,
				...mergedExecOptions,
			};

			const claimed = await this.runState.claimResume(this.runId, state);
			if (!claimed) {
				throw new StaleResumeError(`Run ${this.runId} is not suspended. Cannot resume.`);
			}
			await options.onResumeClaimed?.();

			abortScope = this.eventBus.createAbortScope(resumeOptions.abortSignal);
			const activeAbortScope = abortScope;

			const pendingResume: PendingResume = {
				pendingToolCalls: state.pendingToolCalls,
				resumeToolCallId: options.toolCallId,
				resumeData,
			};

			await this.ensureModelCost();

			await this.memory.setListObservationLogMemory(list, state.persistence);

			if (method === 'generate') {
				const sink = new GenerateSink(this.createRunServices());

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Reference the trigger explicitly, e.g. nodeJson(trigger, 'message.chat.id').
  2. Or use the full node reference form $('Trigger').item.json.message.chat.id.
  3. Pin the session key to a stable upstream field that uniquely identifies the conversation/user.

Example fix

// before
memory: windowBufferMemory({
  sessionId: '={{ $json.message.chat.id }}',
}),

// after
memory: windowBufferMemory({
  sessionId: expr("={{ $('Trigger').item.json.message.chat.id }}"),
});
Defensive patterns

Strategy: validation

Validate before calling

function isUnsafeSessionExpression(value: unknown): value is string {
  return typeof value === 'string' && value.includes('$json') && (value.startsWith('=') || value.includes('{{'));
}

// before assigning a session key:
if (isUnsafeSessionExpression(sessionId)) {
  throw new Error('Session key uses $json; use an explicit node reference like $(\'Trigger\').item.json.message.chat.id');
}

Type guard

function isUnsafeSessionExpression(value: unknown): value is string {
  return typeof value === 'string' && value.includes('$json') && (value.startsWith('=') || value.includes('{{'));
}

Prevention

When it happens

Trigger: A session-key parameter value is a string that includes '$json' AND either starts with '=' or contains '{{'. The helper isUnsafeSessionExpression matches that shape, and createIssue is emitted for that parameter path.

Common situations: Wiring a memory subnode (Buffer/Window memory) and pointing the sessionId at {{$json.message.chat.id}}; an AI builder defaults to $json because it is the shortest reference; copying a Telegram-bot tutorial that used $json before this guard existed.

Related errors


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