n8n-io/n8n · error · TypeORMError

Cyclic dependency: ${JSON.stringify(node)}

Error message

Cyclic dependency: ${JSON.stringify(node)}

What it means

`SubjectTopoligicalSorter` orders subjects for insert/update so parents are persisted before children. It performs a DFS over the dependency edges; if a node appears in its own predecessor chain (a true cycle), TypeORM cannot order the work and throws `Cyclic dependency: <node>`. Unlike the schema-level CircularRelationsError, this fires at flush time on the actual subject graph.

Source

Thrown at packages/@n8n/typeorm/src/persistence/SubjectTopoligicalSorter.ts:177

				if (res.indexOf(edge[0]) < 0) res.push(edge[0]);
				if (res.indexOf(edge[1]) < 0) res.push(edge[1]);
			}
			return res;
		}

		const nodes = uniqueNodes(edges);
		let cursor = nodes.length,
			sorted = new Array(cursor),
			visited: any = {},
			i = cursor;

		while (i--) {
			if (!visited[i]) visit(nodes[i], i, []);
		}

		function visit(node: any, i: number, predecessors: any[]) {
			if (predecessors.indexOf(node) >= 0) {
				throw new TypeORMError('Cyclic dependency: ' + JSON.stringify(node)); // todo: better error
			}

			if (!~nodes.indexOf(node)) {
				throw new TypeORMError(
					'Found unknown node. Make sure to provided all involved nodes. Unknown node: ' +
						JSON.stringify(node),
				);
			}

			if (visited[i]) return;
			visited[i] = true;

			// outgoing edges
			let outgoing = edges.filter(function (edge) {
				return edge[0] === node;
			});
			if ((i = outgoing.length)) {
				let preds = predecessors.concat(node);

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Make at least one side of the cycle nullable, insert with null, then UPDATE the FK in a second pass.
  2. Save in dependency order: persist and reload the parent, set its id on the child, then save the child.
  3. Use deferred FK constraints (Postgres `DEFERRABLE`) and wrap the save in a transaction so the cycle resolves at commit.

Example fix

// before — circular mandatory FKs in one save
a.b = b; b.a = a;
await repo.save([a, b]); // Cyclic dependency

// after — nullable side + two-step
@Entity() class A { @ManyToOne(() => B, { nullable: true }) b: B | null; }
await repo.save(a);          // a inserted, b null
a.b = b; await repo.save(b); // b inserted
await repo.save(a);          // a updated with b's id
Defensive patterns

Strategy: validation

Validate before calling

// Before a multi-entity save, detect circular non-nullable FKs in the working set
function hasCycle(nodes: string[], edges: [string, string][]): boolean {
  const adj = new Map<string, string[]>(); nodes.forEach(n => adj.set(n, []));
  edges.forEach(([a, b]) => adj.get(a)!.push(b));
  const state = new Map<string, 'vis'|'done'>();
  const dfs = (n: string): boolean => {
    state.set(n, 'vis');
    for (const nb of adj.get(n) ?? []) { if (state.get(nb) === 'vis') return true; if (!state.has(nb) && dfs(nb)) return true; }
    state.set(n, 'done'); return false;
  };
  return nodes.some(n => !state.has(n) && dfs(n));
}

Try / catch

try { await repo.save(graph); } catch (e) { if (e instanceof TypeORMError && /Cyclic dependency/) { /* split into ordered saves or make one FK nullable */ } throw e; }

Prevention

When it happens

Trigger: Persisting two or more new entities whose non-nullable foreign keys point at each other in the same `save()` call, so neither can be inserted first. A self-referencing mandatory relation (entity A must have a parent A) being inserted without a pre-existing root.

Common situations: Calling `repo.save([a, b])` where `a.ref = b` and `b.ref = a` and both FKs are non-nullable. Building a graph in memory and saving it in one pass with circular mandatory dependencies.

Related errors


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