phacility/phabricator · error · PhabricatorEdgeCycleException

Graph cycle detected (type=%s, cycle=%s).

Error message

Graph cycle detected (type=%s, cycle=%s).

What it means

Phabricator's edge editor links objects together (project membership, task dependencies, subproject hierarchies) via a directed graph stored as edges. Before saving, PhabricatorEdgeEditor builds a PhabricatorEdgeGraph seeded with the affected PHIDs and runs detectCycles() on each; if any node can reach itself through the accumulated edges, PhabricatorEdgeCycleException is thrown and the transaction is rejected. This keeps hierarchies (which assume a DAG) acyclic, otherwise queries like ancestor walks would never terminate.

Source

Thrown at src/infrastructure/edges/editor/PhabricatorEdgeEditor.php:400

   */
  private function detectCycles(array $phids, $edge_type) {
    // For simplicity, we just seed the graph with the affected nodes rather
    // than seeding it with their edges. To do this, we just add synthetic
    // edges from an imaginary '<seed>' node to the known edges.


    $graph = id(new PhabricatorEdgeGraph())
      ->setEdgeType($edge_type)
      ->addNodes(
        array(
          '<seed>' => $phids,
        ))
      ->loadGraph();

    foreach ($phids as $phid) {
      $cycle = $graph->detectCycles($phid);
      if ($cycle) {
        throw new PhabricatorEdgeCycleException($edge_type, $cycle);
      }
    }
  }


}

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Inspect the existing edges of the same type for the destination PHID (Maniphest/project UI or ./bin/search or a management script using PhabricatorEdgeQuery) and remove or repoint the edge that closes the loop.
  2. Reorder the operation: first remove the old parent/dependency edge with removeEdge() and save, then add the new edge that previously would have closed the cycle.
  3. If you drive edge writes from a script, pre-check reachability with PhabricatorEdgeGraph->loadGraph()->detectCycles($src_phid) after simulating the new edge, and skip or report the conflicting pair.
  4. For migrations importing existing data that legitimately contains cycles, break the cycle at import time (drop the lowest-priority edge) rather than trying to bypass the editor - there is no supported way to save a cyclic edge graph.

Example fix

// before: adding the reverse edge directly closes a loop
$editor = id(new PhabricatorEdgeEditor())
  ->addEdge($project_a_phid, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $project_b_phid)
  ->save(); // throws PhabricatorEdgeCycleException if B is already above A

// after: remove the opposing hierarchy edge first, then apply the new relation
$editor = id(new PhabricatorEdgeEditor())
  ->removeEdge($project_b_phid, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $project_a_phid)
  ->addEdge($project_a_phid, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $project_b_phid)
  ->save();
Defensive patterns

Strategy: try-catch

Validate before calling

$graph = id(new PhabricatorEdgeGraph())
  ->setEdgeType($edge_type)
  ->addNodes(array('<seed>' => array($dst_phid)))
  ->loadGraph();
$cycle = $graph->detectCycles($dst_phid);
if ($cycle) {
  // adding src->dst would close a loop; refuse or de-conflict first
  return new Aphront404Response();
}

Try / catch

try {
  id(new PhabricatorEdgeEditor())
    ->addEdge($src, $edge_type, $dst)
    ->save();
} catch (PhabricatorEdgeCycleException $ex) {
  // surface a user-actionable error: the target is already an ancestor
  $errors[] = pht(
    'Cannot create this relation: %s is already above %s in the hierarchy.',
    $dst_name, $src_name);
}

Prevention

When it happens

Trigger: Calling $editor->addEdge($src, $edge_type, $dst)->save() where the reverse path $dst -> ... -> $src already exists for that edge type; e.g. addEdge(A, 'member.project', B) when B is already an ancestor of A in the project-membership graph, or re-parenting a subproject under one of its own descendants.

Common situations: Reorganizing project hierarchies in the UI (making P1 a subproject of P2 while P2 is already under P1); scripts or migrations that bulk-insert membership edges without checking existing paths; two admins reparenting the same projects concurrently so the second save creates the loop.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/3331572a313d674b. Report an issue: GitHub.