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
- Reference the trigger explicitly, e.g. nodeJson(trigger, 'message.chat.id').
- Or use the full node reference form $('Trigger').item.json.message.chat.id.
- 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
- Always reference the trigger or a named upstream node for session keys.
- Ban $json in session-key expressions in your generator's lint rules.
- Pin the key to a stable conversation/user identifier.
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
- Reflector output must be valid JSON
- Reflector output must be a JSON object
- Invalid memory configuration. Use: new Memory() for in-proce
- Observational memory requires a storage backend that impleme
- Episodic memory requires a storage backend that implements B
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/6d154edf140e44fa.
Report an issue: GitHub.