gastownhall/beads · error
applyGraph: dependency cycle would be created: %s
Error message
applyGraph: dependency cycle would be created: %s
What it means
The final cycle check found that the edges in this plan would create a dependency cycle among scheduling edges; applyGraph refuses to apply and returns the cycle path in the message. This is an intentional validation failure, not an internal error — the plan itself is cyclic.
Source
Thrown at internal/storage/domain/issue.go:1294
if err != nil {
return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q: %w", node.Key, err)
}
if (dep.Type == types.DepParentChild) != parentPhase {
continue
}
if err := u.depRepo.Insert(ctx, dep, actor, DepInsertOpts{UseWispsTable: useWisp}); err != nil {
return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q: adding dep to %q: %w", node.Key, nd.Target, err)
}
if types.IsSchedulingEdge(dep.Type) {
newSchedulingEdges = append(newSchedulingEdges, [2]string{dep.IssueID, dep.DependsOnID})
}
}
}
}
if cyclePath, err := u.depRepo.CycleThroughEdges(ctx, newSchedulingEdges); err != nil {
return GraphApplyResult{}, fmt.Errorf("applyGraph: final cycle check: %w", err)
} else if cyclePath != "" {
return GraphApplyResult{}, fmt.Errorf("applyGraph: dependency cycle would be created: %s", cyclePath)
}
// Pass 5 — apply deferred assignees.
for i, assignee := range pendingAssignees {
if assignee == "" {
continue
}
id := keyToID[plan.Nodes[i].Key]
if err := u.issueRepo.Update(ctx, id, map[string]any{"assignee": assignee}, actor, IssueTableOpts{UseWispsTable: useWisp}); err != nil {
return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q: defer assignee: %w", plan.Nodes[i].Key, err)
}
}
return GraphApplyResult{IDs: keyToID}, nil
}
// graphParentDepPairs encodes the (childID, parentID) parent-child pairs
// implied by the plan's node ParentKey/ParentID fields. Used by applyGraphView on GitHub (pinned to 71377f2769)
Solutions
- Read the cycle path printed in the message to identify the offending edge loop.
- Remove or reverse one edge in the cycle in the plan file.
- Check existing DB dependencies — the cycle may need only one new edge removed to break.
- Re-run bd graph apply after breaking the loop.
Example fix
// before: cyclic plan
edges:
- {from: a, to: b, type: blocks}
- {from: b, to: c, type: blocks}
- {from: c, to: a, type: blocks}
// after: break the cycle by dropping/reversing one edge
edges:
- {from: a, to: b, type: blocks}
- {from: b, to: c, type: blocks} Defensive patterns
Strategy: validation
Validate before calling
// Detect scheduling-edge cycles in the plan before apply
func hasSchedulingCycle(edges []Edge) bool {
adj := map[string][]string{}
for _, e := range edges { adj[e.From] = append(adj[e.From], e.To) }
return detectCycleDFS(adj) != ""
} Try / catch
if err := bd.GraphApply(ctx, plan); err != nil {
if cycle, ok := parseCyclePath(err); ok {
fmt.Printf("remove one edge from this loop: %s\n", cycle)
return errPlanCyclic
}
return err
} Prevention
- Run a DFS cycle check over plan edges before applying
- Include existing DB dependencies in the pre-check, not just plan edges
- Never author mutual blocking pairs (a blocks b AND b blocks a)
When it happens
Trigger: A plan whose blocking/scheduling edges form a loop, e.g. A blocks B, B blocks C, C blocks A (possibly combined with pre-existing edges in the DB closing a loop that only becomes cyclic with the new edges).
Common situations: Hand-authored or generated plans where blocking direction was inverted; merging plans from multiple sources that each look acyclic but compose into a cycle; an existing dependency in the DB plus one new edge closing the loop.
Related errors
- ExternalDoltConfig: TLSKey set without TLSCert
- edge %d %s->%s creates a blocking dependency cycle
- applyGraph: node %q: planned blocking dependencies create a
- applyGraph: edge %d %s->%s creates a blocking dependency cyc
- no store is open for this workspace
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e3860a9a19b6f9f1.
Report an issue: GitHub.