{"record":{"id":"975820129edb79bb","repo":"nocodb/nocodb","slug":"circular-reference","errorCode":"CIRCULAR_REFERENCE","errorMessage":"Circular reference detected","messagePattern":"Circular reference detected","errorType":"validation","errorClass":"FormulaError","httpStatus":null,"severity":"error","filePath":"packages/nocodb-sdk/src/lib/formula/validate-extract-tree.ts","lineNumber":741,"sourceCode":"      // if this node has neighbours, increase visited by 1\n      const neighbours = adj.get(src) || new Set();\n      if (neighbours.size > 0) {\n        visited += 1;\n      }\n      // iterate each neighbouring nodes\n      neighbours.forEach((neighbour: string) => {\n        // decrease in-degree of its neighbours by 1\n        inDegrees.set(neighbour, inDegrees.get(neighbour) - 1);\n        // if in-degree becomes 0\n        if (inDegrees.get(neighbour) === 0) {\n          // then put the neighboring node to the queue\n          queue.push(neighbour);\n        }\n      });\n    }\n    // vertices not same as visited = cycle found\n    if (vertices !== visited) {\n      throw new FormulaError(\n        FormulaErrorType.CIRCULAR_REFERENCE,\n        {\n          key: 'msg.formula.cantSaveCircularReference',\n        },\n        'Circular reference detected'\n      );\n    }\n  }\n}\n\nexport async function validateFormulaAndExtractTreeWithType({\n  formula,\n  column,\n  columns,\n  clientOrSqlUi,\n  getMeta,\n  trackPosition,\n}: {","sourceCodeStart":723,"sourceCodeEnd":759,"githubUrl":"https://github.com/nocodb/nocodb/blob/d3caaf4e890acf64bd49b788cf6dc32b7644c895/packages/nocodb-sdk/src/lib/formula/validate-extract-tree.ts#L723-L759","documentation":"Thrown by the dependency cycle detector (validate-extract-tree.ts:741) after a Kahn topological sort over formula-column references. It builds an adjacency list and in-degree map from formula dependency paths, BFS-visits zero-in-degree nodes, and if the visited count is less than the vertex count it concludes a cycle exists among formula columns. This is a graph-level check, independent of any single formula's syntax.","triggerScenarios":"Two or more formula columns that reference each other directly (ColA = {ColB} + 1, ColB = {ColA} + 1) or transitively (A→B→C→A).","commonSituations":"Editing a formula column to reference another formula that eventually points back; bulk-importing columns that form a cycle; renaming/swapping columns so a previously-acyclic graph becomes cyclic.","solutions":["Break the cycle by replacing at least one formula column with a plain column or a value that does not reference back.","Trace the dependency graph (the collected formulaPaths) to find the smallest cycle and remove one edge.","Re-model the computed value as a single self-contained formula with no cross-formula references."],"exampleFix":"// before (cycle)\n//   ColA = {ColB} + 1\n//   ColB = {ColA} + 1\n// after\n//   ColA = {Base} + 1\n//   ColB = {ColA} + 1","handlingStrategy":"validation","validationCode":"// Detect a cycle among formula column references BEFORE calling the API.\n// refs: map of columnId -> columnIds it references in its formula\nfunction hasFormulaCycle(refs: Record<string, string[]>): boolean {\n  const inDeg: Record<string, number> = {};\n  for (const [src, deps] of Object.entries(refs)) {\n    inDeg[src] = inDeg[src] || 0;\n    for (const d of deps) inDeg[d] = (inDeg[d] || 0) + 1;\n  }\n  const q = Object.keys(inDeg).filter((k) => inDeg[k] === 0);\n  let visited = 0;\n  while (q.length) {\n    const n = q.shift()!;\n    visited++;\n    for (const d of refs[n] || []) if (--inDeg[d] === 0) q.push(d);\n  }\n  return visited !== Object.keys(inDeg).length;\n}","typeGuard":"function isCircularReferenceError(e: unknown): e is FormulaError {\n  return e instanceof FormulaError && e.type === FormulaErrorType.CIRCULAR_REFERENCE;\n}","tryCatchPattern":"try {\n  await validateFormulaAndExtractTreeWithType({ formula, columns, clientOrSqlUi, getMeta });\n} catch (e) {\n  if (e instanceof FormulaError && e.type === FormulaErrorType.CIRCULAR_REFERENCE) {\n    // tell user the formula columns form a cycle; break one reference\n  }\n  throw e;\n}","preventionTips":["Maintain a dependency graph of formula columns and reject edits that create a cycle before saving.","When renaming/swapping columns, re-check the graph for cycles.","Avoid formulas that reference other formulas which can transitively point back."],"tags":["formula","circular-reference","dependency-graph","topological-sort"],"backgroundTag":null,"analyzedSha":"d3caaf4e890acf64bd49b788cf6dc32b7644c895","analyzedAt":"2026-08-12T13:07:32.092Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}