nocodb/nocodb · error · FormulaError

CIRCULAR_REFERENCE

CIRCULAR_REFERENCE

Error message

Circular reference detected

What it means

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.

Source

Thrown at packages/nocodb-sdk/src/lib/formula/validate-extract-tree.ts:741

      // if this node has neighbours, increase visited by 1
      const neighbours = adj.get(src) || new Set();
      if (neighbours.size > 0) {
        visited += 1;
      }
      // iterate each neighbouring nodes
      neighbours.forEach((neighbour: string) => {
        // decrease in-degree of its neighbours by 1
        inDegrees.set(neighbour, inDegrees.get(neighbour) - 1);
        // if in-degree becomes 0
        if (inDegrees.get(neighbour) === 0) {
          // then put the neighboring node to the queue
          queue.push(neighbour);
        }
      });
    }
    // vertices not same as visited = cycle found
    if (vertices !== visited) {
      throw new FormulaError(
        FormulaErrorType.CIRCULAR_REFERENCE,
        {
          key: 'msg.formula.cantSaveCircularReference',
        },
        'Circular reference detected'
      );
    }
  }
}

export async function validateFormulaAndExtractTreeWithType({
  formula,
  column,
  columns,
  clientOrSqlUi,
  getMeta,
  trackPosition,
}: {

View on GitHub (pinned to d3caaf4e89)

Solutions

  1. Break the cycle by replacing at least one formula column with a plain column or a value that does not reference back.
  2. Trace the dependency graph (the collected formulaPaths) to find the smallest cycle and remove one edge.
  3. Re-model the computed value as a single self-contained formula with no cross-formula references.

Example fix

// before (cycle)
//   ColA = {ColB} + 1
//   ColB = {ColA} + 1
// after
//   ColA = {Base} + 1
//   ColB = {ColA} + 1
Defensive patterns

Strategy: validation

Validate before calling

// Detect a cycle among formula column references BEFORE calling the API.
// refs: map of columnId -> columnIds it references in its formula
function hasFormulaCycle(refs: Record<string, string[]>): boolean {
  const inDeg: Record<string, number> = {};
  for (const [src, deps] of Object.entries(refs)) {
    inDeg[src] = inDeg[src] || 0;
    for (const d of deps) inDeg[d] = (inDeg[d] || 0) + 1;
  }
  const q = Object.keys(inDeg).filter((k) => inDeg[k] === 0);
  let visited = 0;
  while (q.length) {
    const n = q.shift()!;
    visited++;
    for (const d of refs[n] || []) if (--inDeg[d] === 0) q.push(d);
  }
  return visited !== Object.keys(inDeg).length;
}

Type guard

function isCircularReferenceError(e: unknown): e is FormulaError {
  return e instanceof FormulaError && e.type === FormulaErrorType.CIRCULAR_REFERENCE;
}

Try / catch

try {
  await validateFormulaAndExtractTreeWithType({ formula, columns, clientOrSqlUi, getMeta });
} catch (e) {
  if (e instanceof FormulaError && e.type === FormulaErrorType.CIRCULAR_REFERENCE) {
    // tell user the formula columns form a cycle; break one reference
  }
  throw e;
}

Prevention

When it happens

Trigger: Two or more formula columns that reference each other directly (ColA = {ColB} + 1, ColB = {ColA} + 1) or transitively (A→B→C→A).

Common situations: 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.

Related errors


AI-assisted analysis of nocodb/nocodb@d3caaf4e89 (2026-08-12). Data as JSON: /api/errors/975820129edb79bb. Report an issue: GitHub.