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.ID

View on GitHub (pinned to 71377f2769)

Solutions

  1. Split the plan into two homogeneous plans (one all-wisp, one all-durable) and apply each with the matching variant
  2. Set Ephemeral/NoHistory consistently across all nodes to match the intended routing
  3. Use the CLI path (bd graph apply) which pre-validates storage class before reaching the domain layer
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/5c877e43508226a3. Report an issue: GitHub.