gastownhall/beads · error
graph dependency cycle would be created: %s
Error message
graph dependency cycle would be created: %s
What it means
This is the deliberate rejection raised when the final cycle check finds that the newly added scheduling dependencies would form a cycle. The message includes cyclePath, the concrete chain of issues forming the loop, so the developer can see exactly which edges to break.
Source
Thrown at cmd/bd/graph_apply.go:1088
continue
}
d, err := types.NewGraphNodeDependency(issues[i].ID, depType, dep.Target, keyToID)
if err != nil {
return fmt.Errorf("node %q: %w", node.Key, err)
}
if err := tx.AddDependency(ctx, d, actor); err != nil {
return fmt.Errorf("node %q: adding dep to %q: %w", node.Key, dep.Target, err)
}
if graphApplySchedulingDependencyType(d.Type) {
newSchedulingEdges = append(newSchedulingEdges, [2]string{d.IssueID, d.DependsOnID})
}
}
}
}
if cyclePath, err := tx.CycleThroughEdges(ctx, newSchedulingEdges); err != nil {
return fmt.Errorf("final graph cycle check: %w", err)
} else if cyclePath != "" {
return fmt.Errorf("graph dependency cycle would be created: %s", cyclePath)
}
// Apply deferred assignees.
for i, assignee := range pendingAssignees {
updates := map[string]interface{}{
"assignee": assignee,
}
if err := tx.UpdateIssue(ctx, issues[i].ID, updates, actor); err != nil {
return fmt.Errorf("node %q: setting assignee: %w", plan.Nodes[i].Key, err)
}
}
return nil
}); err != nil {
return nil, err
}
return &GraphApplyResult{IDs: keyToID}, nilView on GitHub (pinned to 71377f2769)
Solutions
- Read cyclePath in the message to identify the loop's members
- Remove or reverse one edge in the reported path
- Change one edge to a non-scheduling type (not blocks) if a soft relation was intended
- Run bd blocked / bd dep tree on the path members to understand pre-existing deps
Example fix
// before
{"from":"a","to":"b","type":"blocks"},{"from":"b","to":"a","type":"blocks"}
// after: drop the back edge
{"from":"a","to":"b","type":"blocks"} Defensive patterns
Strategy: validation
Validate before calling
// topological check over plan edges plus existing DB deps
if err := topoSort(planNodes, append(planEdges, existingDBDeps()...)); err != nil {
return fmt.Errorf("plan would create a cycle: %w", err)
} Type guard
func acyclic(edges []Edge) bool { return topoSort(nodeKeys(edges), edges) == nil } Try / catch
if err := bd.GraphApply(ctx, plan); err != nil {
if strings.Contains(err.Error(), "cycle would be created") {
path := extractCyclePath(err.Error())
plan = dropOneEdgeOnPath(plan, path)
return bd.GraphApply(ctx, plan)
}
return err
} Prevention
- Run a topological sort on plan edges plus existing deps before applying
- Never author mutual blocks between two nodes
- Check bd dep tree for pre-existing deps that new edges might close loops with
When it happens
Trigger: tx.CycleThroughEdges returns a non-empty cyclePath after the plan's parent-child and scheduling edges are inserted — i.e. some combination of new edges closes a loop (A blocks B blocks ... blocks A).
Common situations: Hand-authored graphs where mutual blocks were specified, or a new edge that closes a loop through pre-existing dependencies in the database.
Related errors
- edge %d %s->%s creates a blocking dependency cycle
- duplicate node key %q
- node %q has empty title
- duplicate explicit id %q (node %q)
- node %q: metadata ref %q references unknown key %q
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/270f2bf236d77c30.
Report an issue: GitHub.