n8n-io/n8n · error · UnimplementedError

unimplemented

unimplemented

Error message

Graphs with back-edges (loops) are not supported yet

What it means

UnimplementedError (code: unimplemented) thrown by validateExecutableGraph when any edge has isBackEdge === true. The engine does not yet support loops; until CAT-2875 lands (re-runnable steps for loop iteration), back-edges are rejected outright to prevent deadlock rather than failing at runtime.

Source

Thrown at packages/@n8n/engine/src/graph/validate-executable-graph.ts:34

 * is created for it. The single place executability rules live; new rules are
 * added here as they arise.
 *
 * Throws `GraphValidationError` for graphs that can never run, and
 * `UnimplementedError` for shapes the engine doesn't support yet.
 */
export function validateExecutableGraph(graph: WorkflowGraph): void {
	const triggers = graph.nodes.filter((node) => node.type === 'trigger');
	if (triggers.length === 0) {
		throw new GraphValidationError('Graph has no trigger node to start from');
	}
	if (triggers.length > 1) {
		throw new GraphValidationError('Graph must have exactly one trigger node');
	}

	// TODO(CAT-2875): loop iteration needs re-runnable steps; until that lands,
	// graphs with back-edges are rejected outright rather than deadlocking.
	if (graph.edges.some((edge) => edge.isBackEdge)) {
		throw new UnimplementedError('Graphs with back-edges (loops) are not supported yet');
	}

	// Slot indices are structural, so they're enforced here rather than left to
	// the transport boundary. TODO(CAT-3042): enforce an upper bound too.
	for (const edge of graph.edges) {
		for (const index of [edge.outputIndex, edge.inputIndex]) {
			if (!Number.isInteger(index) || index < 0) {
				throw new GraphValidationError(
					`Edge ${edge.from} → ${edge.to} has slot index ${index}; slot indices are non-negative integers`,
				);
			}
			if (index > MAX_SLOT_INDEX) {
				throw new GraphValidationError(
					`Edge ${edge.from} → ${edge.to} has slot index ${index}; slot indices above ${MAX_SLOT_INDEX} are not supported yet`,
				);
			}
		}
	}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Refactor the loop out of the graph: unroll a bounded number of iterations, or move the loop into a single node's runtime logic.
  2. Track CAT-2875 for native loop support and avoid cyclic edges until then.
  3. If the back-edge is accidental (miswired connection), remove or redirect it.
  4. For retry patterns, use a node that handles its own retry internally instead of a graph-level cycle.

Example fix

// before
edges: [
  { from: 'A', to: 'B', outputIndex: 0, inputIndex: 0, isBackEdge: false },
  { from: 'B', to: 'A', outputIndex: 0, inputIndex: 0, isBackEdge: true }, // loop
]

// after
// move iteration inside node B; graph stays acyclic
edges: [
  { from: 'A', to: 'B', outputIndex: 0, inputIndex: 0, isBackEdge: false },
]
Defensive patterns

Strategy: validation

Validate before calling

function isAcyclic(graph: WorkflowGraph): boolean {
  return !graph.edges.some(e => e.isBackEdge);
}
if (!isAcyclic(graph)) { // do not submit; refactor the loop into a node }

Type guard

function isAcyclicGraph(graph: WorkflowGraph): boolean {
  return graph.edges.every(e => !e.isBackEdge);
}

Try / catch

try {
  validateExecutableGraph(graph);
} catch (err) {
  if (err instanceof UnimplementedError && /back-edges/.test(err.message)) {
    // unroll iterations or move loop into a node
  } else throw err;
}

Prevention

When it happens

Trigger: Submitting a graph where at least one edge creates a cycle (target precedes source in topological order, marked isBackEdge). Any loop/feedback construct trips this.

Common situations: A user models a retry/feedback loop or iterative process; graph synthesis code generates cyclic dependencies; converting a DAG-based workflow that used a different loop primitive; an importer that preserves cycles from another format.

Related errors


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