n8n-io/n8n · error · TypeORMError

Cyclic dependency

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.

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.

Appendix: 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)