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

  1. Break the cycle by removing or reversing at least one dep edge.
  2. Model the dependency as a sequence (linearize the order) instead of a loop.
  3. If two tasks are truly co-dependent, merge them into a single task.
  4. 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

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


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