gastownhall/beads · error
applyGraph: edge %d %s->%s creates a blocking dependency cyc
Error message
applyGraph: edge %d %s->%s creates a blocking dependency cycle
What it means
During planned blocking-cycle validation, an edge whose resolved fromID equals its toID (a self-loop) is immediately rejected as a blocking dependency cycle — an issue cannot block itself. The message reports the edge index and the resolved issue ID on both sides.
Source
Thrown at internal/storage/domain/issue.go:1471
}
adj := make(map[string][]string)
checks := make([]plannedEdge, 0, len(plan.Edges))
for i, edge := range plan.Edges {
depType := edge.Type
if depType == "" {
depType = types.DepBlocks
}
if !cycleRelevantDepType(depType) {
continue
}
fromID := resolveEdgeRef(edge.FromKey, edge.FromID, keyToID)
toID := resolveEdgeRef(edge.ToKey, edge.ToID, keyToID)
if fromID == "" || toID == "" {
continue
}
if fromID == toID {
return fmt.Errorf("applyGraph: edge %d %s->%s creates a blocking dependency cycle", i, fromID, toID)
}
adj[fromID] = append(adj[fromID], toID)
checks = append(checks, plannedEdge{index: i, fromID: fromID, toID: toID})
}
depCache := make(map[string][]*types.Dependency)
for _, edge := range checks {
hasPath, err := u.graphHasPath(ctx, adj, depCache, edge.toID, edge.fromID, cycleRelevantDepType)
if err != nil {
return fmt.Errorf("applyGraph: edge %d %s->%s: checking planned blocking cycle: %w", edge.index, edge.fromID, edge.toID, err)
}
if hasPath {
return fmt.Errorf("applyGraph: edge %d %s->%s creates a blocking dependency cycle", edge.index, edge.fromID, edge.toID)
}
}
return nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Find edge <index> in the plan and check whether from and to resolve to the same node.
- Correct the edge so it connects two distinct issues, or delete it.
- Ensure node keys are unique and no aliasing collapses two keys to one ID.
- Re-run apply.
Example fix
// before: self edge
edges:
- {from: bd-1, to: bd-1, type: blocks}
// after: distinct endpoints or removed
edges: [] Defensive patterns
Strategy: validation
Validate before calling
// Reject self-edges before apply
for i, e := range plan.Edges {
if resolveEdgeRef(e.FromKey, e.FromID, keyToID) == resolveEdgeRef(e.ToKey, e.ToID, keyToID) {
return fmt.Errorf("edge %d is a self-loop", i)
}
} Type guard
func isSelfEdge(e Edge, keyToID map[string]string) bool {
return resolveEdgeRef(e.FromKey, e.FromID, keyToID) != "" &&
resolveEdgeRef(e.FromKey, e.FromID, keyToID) == resolveEdgeRef(e.ToKey, e.ToID, keyToID)
} Try / catch
if err := bd.GraphApply(ctx, plan); err != nil {
if strings.Contains(err.Error(), "creates a blocking dependency cycle") {
return fmt.Errorf("fix plan edges (self-loop or cycle): %w", err)
}
return err
} Prevention
- Check from != to on every edge in plan linters
- Ensure node keys are unique so aliasing cannot collapse endpoints
- Filter no-op edges out of generated plans
When it happens
Trigger: A plan edge where fromKey/fromID and toKey/toID resolve to the same issue — e.g. both set to the same node's key, duplicated keys, or resolveEdgeRef mapping two different key spellings to one ID.
Common situations: Copy-paste errors in plan files where from and to are identical; templated plan generation emitting the same key on both sides; aliasing where two keys map to one issue ID after creation.
Related errors
- edge %d %s->%s creates a blocking dependency cycle
- applyGraph: dependency cycle would be created: %s
- applyGraph: node %q: planned blocking dependencies create a
- no store is open for this workspace
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/afb7eb642ac9a15e.
Report an issue: GitHub.