gastownhall/beads · error
edge %d: spawner_id %q must match to_id %q (the waits-for ta
Error message
edge %d: spawner_id %q must match to_id %q (the waits-for target is the spawner)
What it means
Mirror of the spawner_key/to_key rule for ID-addressed edges: for waits-for edges carrying spawner_id, the spawner ID must equal to_id because gate evaluation reads the spawner from the dependency target. A mismatch is rejected at validation time.
Source
Thrown at cmd/bd/graph_apply.go:635
}
if edge.SpawnerKey != "" && edge.SpawnerID != "" {
return fmt.Errorf("edge %d: cannot specify both spawner_key and spawner_id", i)
}
if edge.SpawnerKey != "" && !seenKeys[edge.SpawnerKey] {
return fmt.Errorf("edge %d: spawner key %q not found in plan", i, edge.SpawnerKey)
}
// Gate evaluation reads the spawner from the dependency target
// (depends_on_id), not metadata, so the spawner must equal the to
// endpoint. Since to_id overrides to_key at apply time
// (resolveEdgeRef), a key-named spawner can't be combined with to_id.
if edge.SpawnerKey != "" && edge.ToID != "" {
return fmt.Errorf("edge %d: spawner_key %q cannot be combined with to_id %q (to_id overrides to_key as the waits-for target; use spawner_id)", i, edge.SpawnerKey, edge.ToID)
}
if edge.SpawnerKey != "" && edge.SpawnerKey != edge.ToKey {
return fmt.Errorf("edge %d: spawner_key %q must match to_key %q (the waits-for target is the spawner)", i, edge.SpawnerKey, edge.ToKey)
}
if edge.SpawnerID != "" && edge.SpawnerID != edge.ToID {
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)View on GitHub (pinned to 71377f2769)
Solutions
- Set spawner_id equal to to_id
- Remove spawner_id if gate/spawner semantics aren't needed
- Reverse or restructure the edge so the target is the actual spawner
Example fix
// before
{"type": "waits-for", "spawner_id": "bd-42", "to_id": "bd-99"}
// after
{"type": "waits-for", "spawner_id": "bd-99", "to_id": "bd-99"} Defensive patterns
Strategy: validation
Validate before calling
if e.Type == "waits-for" && e.SpawnerID != "" && e.SpawnerID != e.ToID {
return fmt.Errorf("spawner_id must equal to_id for waits-for edges")
} Type guard
func spawnerIDMatchesTarget(e Edge) bool { return e.SpawnerID == "" || e.SpawnerID == e.ToID } Prevention
- Fill spawner_id and to_id from the same source field in generators
- Don't reverse edges by swapping only one endpoint field
- Add ID consistency checks to plan CI linting
When it happens
Trigger: An edge with type waits-for where spawner_id = "bd-42" but to_id = "bd-99".
Common situations: Swapping edge direction without swapping spawner_id; generating IDs from a template that fills the wrong column; refactoring plans and renaming IDs inconsistently.
Related errors
- edge %d: spawner_key %q must match to_key %q (the waits-for
- edge %d: gate/spawner fields require type %q
- edge %d: invalid gate %q (valid: %s, %s)
- edge %d: cannot specify both spawner_key and spawner_id
- edge %d: spawner key %q not found in plan
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/d3f0a74ab938fa2c.
Report an issue: GitHub.