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

  1. Give each task a globally unique id (e.g. prefix with a namespace or use a UUID).
  2. Run a dedupe pass on tasks before calling createPlan: const ids = new Set(); tasks.filter(t => !ids.has(t.id) && ids.add(t.id)).
  3. 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

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


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