gastownhall/beads · error

node %q: %w

Error message

node %q: %w

What it means

validateGraphApplyNodeFields wraps errors from validating a node's explicit ID with the node's plan key: when node.ID is set, it must pass ValidateIDFormat, and any format violation is reported as `node "<key>": <underlying reason>`. The wrap keeps the plan key visible even though the failing value is the ID.

Source

Thrown at cmd/bd/graph_apply.go:653

				return fmt.Errorf("edge %d: spawner_id %q must match to_id %q (the waits-for target is the spawner)", i, edge.SpawnerID, edge.ToID)
			}
		}
	}

	if err := validateGraphApplyLocalCycles(plan, seenKeys); err != nil {
		return err
	}

	return nil
}

// validateGraphApplyNodeFields checks the single-node fields added for
// bd-create parity, mirroring the flag-shape checks `bd create` applies
// (config-gated template linting is not run on graph plans).
func validateGraphApplyNodeFields(node GraphApplyNode, customTypes, customStatuses []string, opts GraphApplyOptions) error {
	if node.ID != "" {
		if _, err := validation.ValidateIDFormat(node.ID); err != nil {
			return fmt.Errorf("node %q: %w", node.Key, err)
		}
	}
	// Friendlier status message than the issue-model validator's below.
	if node.Status != "" && !types.Status(node.Status).IsValidWithCustom(customStatuses) {
		return fmt.Errorf("node %q: invalid status %q (valid: %s; configure custom statuses via 'bd config set status.custom')", node.Key, node.Status, workapi.ValidStatusList(customStatuses))
	}
	if node.WispType != "" && !types.WispType(node.WispType).IsValid() {
		return fmt.Errorf("node %q: invalid wisp_type %q (must be %s)", node.Key, node.WispType, types.ValidWispTypeNames())
	}
	if node.MolType != "" && !types.MolType(node.MolType).IsValid() {
		return fmt.Errorf("node %q: invalid mol_type %q (must be %s)", node.Key, node.MolType, types.ValidMolTypeNames())
	}
	if (node.EventKind != "" || node.Actor != "" || node.Target != "" || node.Payload != "") && node.Type != string(types.TypeEvent) {
		return fmt.Errorf("node %q: event_kind, actor, target, and payload require type %q", node.Key, types.TypeEvent)
	}
	// Issue-model rules (type validity, priority range, estimate sign,
	// metadata JSON, ...) run against the same materialized issue the apply
	// path stores, so plan-time and insert-time validation can't drift.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped underlying message and fix node.id to satisfy the ID format rules
  2. Remove node.id and let bd assign/resolve the ID when possible
  3. Sanitize generated IDs (lowercase, allowed charset, separators) in your plan generator

Example fix

// before
{"key": "auth", "id": "Auth Feature #1"}
// after
{"key": "auth", "id": "auth-feature-1"}
Defensive patterns

Strategy: validation

Validate before calling

for _, n := range plan.Nodes {
  if n.ID != "" {
    if err := validation.ValidateIDFormat(n.ID); err != nil { return fmt.Errorf("node %q: %w", n.Key, err) }
  }
}

Type guard

func validNodeID(n Node) bool { return n.ID == "" || validation.ValidateIDFormat(n.ID) == nil }

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
  var fmtErr *fmt.Errorf
  if strings.Contains(err.Error(), "invalid") { /* fix plan and retry */ }
  return err
}

Prevention

When it happens

Trigger: A graph plan node sets an id field that violates ID format rules (bad characters, wrong length, invalid prefix/shape), e.g. id: "my issue!" or an ID containing uppercase/spaces.

Common situations: Hand-written plans using human-readable labels as IDs; generating IDs from names without sanitization; copy-pasting IDs from external systems with different formats.

Related errors


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