gastownhall/beads · error
applyGraph: node %d (key=%q) has nil Issue
Error message
applyGraph: node %d (key=%q) has nil Issue
What it means
applyGraph validates that every plan node carries a non-nil Issue before minting IDs and inserting. A nil node.Issue means the plan was constructed incorrectly (e.g. a node added as a link-only placeholder without issue data). This is a caller/plan-construction bug, not a storage failure.
Source
Thrown at internal/storage/domain/issue.go:1116
func (u *issueUseCaseImpl) ApplyWispGraph(ctx context.Context, plan GraphPlan, actor string) (GraphApplyResult, error) {
return u.applyGraph(ctx, plan, actor, true)
}
func (u *issueUseCaseImpl) applyGraph(ctx context.Context, plan GraphPlan, actor string, useWisp bool) (GraphApplyResult, error) {
keyToID := make(map[string]string, len(plan.Nodes))
pendingAssignees := make(map[int]string, len(plan.Nodes))
// Pass 1 — create every node as a top-level issue. We deliberately do
// not pass ParentID to u.create: graph nodes with parent_key/parent_id
// receive top-level hash (or counter) IDs and have their parent linkage
// added as a separate parent-child dep below. This matches embedded
// executeGraphApply (cmd/bd/graph_apply.go) and lets children precede
// parents in plan order — keyToID is only consulted after every node
// has minted its ID.
for i, node := range plan.Nodes {
if node.Issue == nil {
return GraphApplyResult{}, fmt.Errorf("applyGraph: node %d (key=%q) has nil Issue", i, node.Key)
}
// The whole plan routes to one table, so every node's storage class
// must match. The CLI pre-validates this; guard here too so other
// callers cannot route wisp-flagged issues into the durable table.
if nodeWisp := node.Issue.Ephemeral || node.Issue.NoHistory; nodeWisp != useWisp {
return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q storage class (ephemeral=%t, no_history=%t) does not match plan routing (wisp=%t)", node.Key, node.Issue.Ephemeral, node.Issue.NoHistory, useWisp)
}
if node.AssignAfterCreate {
pendingAssignees[i] = node.Assignee
node.Issue.Assignee = ""
} else if node.Assignee != "" {
node.Issue.Assignee = node.Assignee
}
params := CreateIssueParams{
Issue: node.Issue,
Labels: node.Labels,View on GitHub (pinned to 71377f2769)
Solutions
- Regenerate the plan with the current bd CLI (bd graph apply plan) instead of hand-building it
- Ensure every node added to plan.Nodes has a populated Issue struct
- Skip/convert link-only placeholder nodes into proper dependency edges rather than plan nodes
- Check for nil-Issue assignments when deserializing plan JSON
Example fix
// before: placeholder node with no issue
plan.Nodes = append(plan.Nodes, GraphNode{Key: "parent"})
// after: attach the issue payload
plan.Nodes = append(plan.Nodes, GraphNode{Key: "parent", Issue: &types.Issue{Title: "parent", ...}}) Defensive patterns
Strategy: validation
Validate before calling
for i, n := range plan.Nodes {
if n.Issue == nil {
return fmt.Errorf("plan node %d (key=%s) has nil Issue; regenerate plan", i, n.Key)
}
} Type guard
func validNodes(plan GraphPlan) bool {
for _, n := range plan.Nodes {
if n.Issue == nil { return false }
}
return true
} Try / catch
result, err := uc.ApplyGraph(ctx, plan, actor)
if err != nil && strings.Contains(err.Error(), "has nil Issue") {
plan = regeneratePlan(sourceFile) // rebuild from authoritative source
result, err = uc.ApplyGraph(ctx, plan, actor)
} Prevention
- Always generate plans via the bd CLI rather than hand-building structs
- Validate plan JSON after deserialization (nil issues, missing keys)
- Skip or convert link-only placeholder nodes into dependency edges
- Add a pre-apply plan lint step in CI
When it happens
Trigger: Calling ApplyGraph with a plan whose Nodes[i].Issue is nil — typically from hand-built plans or a bug in plan generation that emits placeholder nodes.
Common situations: Custom tooling building GraphPlan structs directly; older plan files generated by a different bd version missing issue payloads; deserialized plans where issues were dropped.
Related errors
- edge %d %s->%s creates a blocking dependency cycle
- no store is open for this workspace
- not found
- no absolute native user directory is available
- ExternalDoltConfig: set either Socket OR (Host, Port), not b
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/ee8959bcb04ae2e8.
Report an issue: GitHub.