n8n-io/n8n · error · PlanValidationError
Plan contains duplicate task IDs
Error message
Plan contains duplicate task IDs
What it means
Thrown as a PlanValidationError by validateDependencies (planned-task-service.ts:31-32) when two or more tasks in the submitted graph share the same id. It fires before any dependency or cycle checks, so a duplicate-id graph never reaches scheduling. The class docstring notes that plan.tool.ts catches this specifically and returns the message to the LLM as a tool result for retry.
Source
Thrown at packages/@n8n/instance-ai/src/planned-tasks/planned-task-service.ts:32
* IDs, unknown deps, missing checkpoint deps, dependency cycles, etc.).
* Callers — notably `plan.tool.ts` — should catch this specifically and surface
* the message back to the LLM so it can retry with a corrected graph. Storage,
* abort, or programming errors are NOT this class and must propagate.
*/
export class PlanValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'PlanValidationError';
}
}
function hasDuplicateIds(tasks: PlannedTask[]): boolean {
return new Set(tasks.map((task) => task.id)).size !== tasks.length;
}
function validateDependencies(tasks: PlannedTask[]): void {
if (hasDuplicateIds(tasks)) {
throw new PlanValidationError('Plan contains duplicate task IDs');
}
const knownIds = new Set(tasks.map((task) => task.id));
const byId = new Map(tasks.map((task) => [task.id, task]));
for (const task of tasks) {
for (const depId of task.deps) {
if (!knownIds.has(depId)) {
throw new PlanValidationError(`Task "${task.id}" depends on unknown task "${depId}"`);
}
}
if (task.kind === 'checkpoint') {
if (task.deps.length === 0) {
throw new PlanValidationError(
`Checkpoint task "${task.id}" must depend on at least one build-workflow task`,
);
}
const dependsOnBuildWorkflow = task.deps.some(
(depId) => byId.get(depId)?.kind === 'build-workflow',View on GitHub (pinned to 5ac6606e81)
Solutions
- Give each task a globally unique id (e.g. prefix with a namespace or use a UUID).
- Run a dedupe pass on tasks before calling createPlan: const ids = new Set(); tasks.filter(t => !ids.has(t.id) && ids.add(t.id)).
- If surfaced to the LLM, follow the tool's guidance and re-call with corrected ids.
Example fix
// before: [{ id: 't1', ... }, { id: 't1', ... }]
// after: [{ id: 't1', ... }, { id: 't2', ... }] Defensive patterns
Strategy: validation
Validate before calling
function hasDuplicateIds(tasks: { id: string }[]): boolean {
return new Set(tasks.map(t => t.id)).size !== tasks.length;
}
if (hasDuplicateIds(tasks)) throw new Error('Plan contains duplicate task IDs'); Type guard
function hasUniqueIds(tasks: { id: string }[]): boolean {
return new Set(tasks.map(t => t.id)).size === tasks.length;
} Try / catch
import { PlanValidationError } from './planned-task-service';
try { await coordinator.createPlan(threadId, tasks, meta); }
catch (e) {
if (e instanceof PlanValidationError) {
// return e.message to the LLM as a tool result for retry (as plan.tool.ts does)
}
throw e;
} Prevention
- Generate ids with a UUID or namespaced counter.
- Dedupe tasks by id before calling createPlan.
- Let the LLM retry on PlanValidationError rather than treating it as fatal.
When it happens
Trigger: createPlan is called with a tasks array where new Set(tasks.map(t => t.id)).size < tasks.length. The LLM (or a programmatic caller) reused an id across two build-workflow or checkpoint tasks.
Common situations: LLM generates ids like 'task-1' twice; a programmatic plan builder concatenates two sub-plans without id namespacing; copy-paste of a task template without regenerating the id.
Related errors
- Task "${task.id}" depends on unknown task "${depId}"
- Checkpoint task "${task.id}" must depend on at least one bui
- Plan contains a dependency cycle involving "${taskId}"
- 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/13d0cd389b170602.
Report an issue: GitHub.