n8n-io/n8n · error · PlanValidationError
Plan contains a dependency cycle involving "${taskId}"
Error message
Plan contains a dependency cycle involving "${taskId}" What it means
Thrown as a PlanValidationError by the DFS cycle check in validateDependencies (planned-task-service.ts:63-67). The visit function marks nodes in a 'visiting' set during the current path; re-encountering a node already in 'visiting' means a cycle. The taskId in the message is the node at which the cycle was detected. This runs after duplicate-id, unknown-dep, and checkpoint checks, so the graph is otherwise well-formed.
Source
Thrown at packages/@n8n/instance-ai/src/planned-tasks/planned-task-service.ts:66
}
const dependsOnBuildWorkflow = task.deps.some(
(depId) => byId.get(depId)?.kind === 'build-workflow',
);
if (!dependsOnBuildWorkflow) {
throw new PlanValidationError(
`Checkpoint task "${task.id}" must depend on at least one build-workflow task`,
);
}
}
}
const visiting = new Set<string>();
const visited = new Set<string>();
const visit = (taskId: string) => {
if (visited.has(taskId)) return;
if (visiting.has(taskId)) {
throw new PlanValidationError(`Plan contains a dependency cycle involving "${taskId}"`);
}
visiting.add(taskId);
const task = byId.get(taskId);
for (const depId of task?.deps ?? []) {
visit(depId);
}
visiting.delete(taskId);
visited.add(taskId);
};
for (const task of tasks) {
visit(task.id);
}
}
function isSuccess(task: PlannedTaskRecord): boolean {
return task.status === 'succeeded';View on GitHub (pinned to 5ac6606e81)
Solutions
- Break the cycle by removing or reversing at least one dep edge.
- Model the dependency as a sequence (linearize the order) instead of a loop.
- If two tasks are truly co-dependent, merge them into a single task.
- Re-call the plan tool with the acyclic graph.
Example fix
// before: [{ id: 'a', deps: ['b'] }, { id: 'b', deps: ['a'] }]
// after: [{ id: 'a', deps: [] }, { id: 'b', deps: ['a'] }] Defensive patterns
Strategy: validation
Validate before calling
function hasCycle(tasks: { id: string; deps: string[] }[]): boolean {
const byId = new Map(tasks.map(t => [t.id, t]));
const visiting = new Set<string>(), visited = new Set<string>();
const visit = (id: string): boolean => {
if (visited.has(id)) return false;
if (visiting.has(id)) return true;
visiting.add(id);
for (const d of byId.get(id)?.deps ?? []) if (visit(d)) return true;
visiting.delete(id); visited.add(id);
return false;
};
return tasks.some(t => visit(t.id));
}
if (hasCycle(tasks)) throw new Error('Plan contains a dependency cycle'); Type guard
function isAcyclic(tasks: { id: string; deps: string[] }[]): boolean {
return !hasCycle(tasks);
} Try / catch
try { await coordinator.createPlan(threadId, tasks, meta); }
catch (e) {
if (e instanceof PlanValidationError && /cycle/.test(e.message)) {
// remove/reverse an edge and retry
}
throw e;
} Prevention
- Topologically sort tasks before submission to confirm acyclicity.
- Avoid back-references when wiring deps programmatically.
- Merge truly co-dependent tasks into one.
When it happens
Trigger: Any tasks[] where the deps edges form a cycle, e.g. A depends on B and B depends on A, or a longer loop A->B->C->A. The recursive visit walks deps edges.
Common situations: LLM generates mutually dependent tasks; a programmatic builder adds a back-reference; manual plan editing introduces a circular dependency; checkpoints gating each other transitively.
Related errors
- Plan contains duplicate task IDs
- Task "${task.id}" depends on unknown task "${depId}"
- Checkpoint task "${task.id}" must depend on at least one bui
- Cannot decrease maxIterations when resuming a run. Expected
- Checkpoint for runId ${this.runId} has pending tool calls —
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/a363f7c356c95e41.
Report an issue: GitHub.