{"record":{"id":"4c2ec4603acee5d1","repo":"n8n-io/n8n","slug":"cyclic-dependency-json-stringify-node","errorCode":null,"errorMessage":"Cyclic dependency: ${JSON.stringify(node)}","messagePattern":"Cyclic dependency: (.+?)","errorType":"exception","errorClass":"TypeORMError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/typeorm/src/persistence/SubjectTopoligicalSorter.ts","lineNumber":177,"sourceCode":"\t\t\t\tif (res.indexOf(edge[0]) < 0) res.push(edge[0]);\n\t\t\t\tif (res.indexOf(edge[1]) < 0) res.push(edge[1]);\n\t\t\t}\n\t\t\treturn res;\n\t\t}\n\n\t\tconst nodes = uniqueNodes(edges);\n\t\tlet cursor = nodes.length,\n\t\t\tsorted = new Array(cursor),\n\t\t\tvisited: any = {},\n\t\t\ti = cursor;\n\n\t\twhile (i--) {\n\t\t\tif (!visited[i]) visit(nodes[i], i, []);\n\t\t}\n\n\t\tfunction visit(node: any, i: number, predecessors: any[]) {\n\t\t\tif (predecessors.indexOf(node) >= 0) {\n\t\t\t\tthrow new TypeORMError('Cyclic dependency: ' + JSON.stringify(node)); // todo: better error\n\t\t\t}\n\n\t\t\tif (!~nodes.indexOf(node)) {\n\t\t\t\tthrow new TypeORMError(\n\t\t\t\t\t'Found unknown node. Make sure to provided all involved nodes. Unknown node: ' +\n\t\t\t\t\t\tJSON.stringify(node),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (visited[i]) return;\n\t\t\tvisited[i] = true;\n\n\t\t\t// outgoing edges\n\t\t\tlet outgoing = edges.filter(function (edge) {\n\t\t\t\treturn edge[0] === node;\n\t\t\t});\n\t\t\tif ((i = outgoing.length)) {\n\t\t\t\tlet preds = predecessors.concat(node);","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/typeorm/src/persistence/SubjectTopoligicalSorter.ts#L159-L195","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Make at least one side of the cycle nullable, insert with null, then UPDATE the FK in a second pass.","Save in dependency order: persist and reload the parent, set its id on the child, then save the child.","Use deferred FK constraints (Postgres `DEFERRABLE`) and wrap the save in a transaction so the cycle resolves at commit."],"exampleFix":"// before — circular mandatory FKs in one save\na.b = b; b.a = a;\nawait repo.save([a, b]); // Cyclic dependency\n\n// after — nullable side + two-step\n@Entity() class A { @ManyToOne(() => B, { nullable: true }) b: B | null; }\nawait repo.save(a);          // a inserted, b null\na.b = b; await repo.save(b); // b inserted\nawait repo.save(a);          // a updated with b's id","handlingStrategy":"validation","validationCode":"// Before a multi-entity save, detect circular non-nullable FKs in the working set\nfunction hasCycle(nodes: string[], edges: [string, string][]): boolean {\n  const adj = new Map<string, string[]>(); nodes.forEach(n => adj.set(n, []));\n  edges.forEach(([a, b]) => adj.get(a)!.push(b));\n  const state = new Map<string, 'vis'|'done'>();\n  const dfs = (n: string): boolean => {\n    state.set(n, 'vis');\n    for (const nb of adj.get(n) ?? []) { if (state.get(nb) === 'vis') return true; if (!state.has(nb) && dfs(nb)) return true; }\n    state.set(n, 'done'); return false;\n  };\n  return nodes.some(n => !state.has(n) && dfs(n));\n}","typeGuard":null,"tryCatchPattern":"try { await repo.save(graph); } catch (e) { if (e instanceof TypeORMError && /Cyclic dependency/) { /* split into ordered saves or make one FK nullable */ } throw e; }","preventionTips":["Make at least one side of any mutual FK nullable.","Save parents first, set ids, then save children in ordered steps.","Consider DEFERRABLE FK constraints inside a transaction on Postgres."],"tags":["typeorm","persistence","circular-dependency","topological-sort","insert"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}