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
- 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.
- 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.
- 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.
- 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 building hierarchy editors, always execute removeEdge() for the inverse relation in the same editor before addEdge().
- Treat detectCycles() as part of your domain model: simulate the edge, check, then save.
- In UI flows, disable or warn on moves that would reparent a project under its own descendant.
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
- Invalid '%s' value for Edge transaction. Value should contai
- Edge transactions must have destination PHIDs as in edge lis
- Edge transactions must have PHIDs or edge specs as values (f
- Transaction edge data must either be the edge PHID or an edg
- Transaction edge specification contains unexpected key "%s".
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/3331572a313d674b.
Report an issue: GitHub.