gastownhall/beads · error
applyGraph: node %q storage class (ephemeral=%t, no_history=
Error message
applyGraph: node %q storage class (ephemeral=%t, no_history=%t) does not match plan routing (wisp=%t)
What it means
applyGraph routes the entire plan to either the durable issues table or the wisps (ephemeral) table based on the useWisp flag. Every node's storage class (Ephemeral || NoHistory) must agree with that routing; this guard prevents wisp-flagged issues being written to the durable table and vice versa. The CLI pre-validates this; the domain layer re-checks for other callers.
Source
Thrown at internal/storage/domain/issue.go:1122
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,
}
r, err := u.create(ctx, params, actor, useWisp)
if err != nil {
return GraphApplyResult{}, fmt.Errorf("applyGraph: node %q: %w", node.Key, err)
}
keyToID[node.Key] = r.Issue.IDView on GitHub (pinned to 71377f2769)
Solutions
- Split the plan into two homogeneous plans (one all-wisp, one all-durable) and apply each with the matching variant
- Set Ephemeral/NoHistory consistently across all nodes to match the intended routing
- Use the CLI path (bd graph apply) which pre-validates storage class before reaching the domain layer
- Fix flags on imported issue data before building the plan
Example fix
// before: mixed storage classes in one plan
node1.Issue.Ephemeral = true; node2.Issue.Ephemeral = false
// after: one storage class per plan, or split
wispPlan.Nodes = filter(plan.Nodes, func(n) { return n.Issue.Ephemeral || n.Issue.NoHistory }) Defensive patterns
Strategy: validation
Validate before calling
func homogeneous(plan GraphPlan) (allWisp bool, ok bool) {
if len(plan.Nodes) == 0 { return false, true }
want := plan.Nodes[0].Issue.Ephemeral || plan.Nodes[0].Issue.NoHistory
for _, n := range plan.Nodes {
if (n.Issue.Ephemeral || n.Issue.NoHistory) != want { return want, false }
}
return want, true
}
// split into wispPlan/durablePlan if !ok before calling ApplyGraph Try / catch
err := uc.ApplyGraph(ctx, plan, actor)
if err != nil && strings.Contains(err.Error(), "storage class") {
wispPlan, durablePlan := splitByStorageClass(plan)
uc.ApplyGraph(ctx, wispPlan, actor)
uc.ApplyGraph(ctx, durablePlan, actor)
} Prevention
- Never mix ephemeral/no_history issues with durable issues in one plan
- Set Ephemeral/NoHistory flags deliberately at plan construction
- Use the CLI path which pre-validates storage class
- Normalize flags when importing graphs from other sources
When it happens
Trigger: Calling ApplyGraph with a plan mixing ephemeral/no_history issues and durable issues, or calling the wisp variant with durable nodes (or vice versa).
Common situations: Programmatically constructing plans that mix wisp and non-wisp issues; importing a graph from another workspace with different ephemeral flags; version drift where a flag was added to some nodes only.
Related errors
- node %q: storage_class ephemeral and no_history are mutually
- invalid key %q: expected storage-class.<issue-type> (e.g. st
- config storage-class.%s: %w
- duplicate node key %q
- node %q has empty title
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5c877e43508226a3.
Report an issue: GitHub.