gastownhall/beads · error
edge %d: spawner_key %q must match to_key %q (the waits-for
Error message
edge %d: spawner_key %q must match to_key %q (the waits-for target is the spawner)
What it means
For waits-for edges the spawner IS the dependency target: the waits-for target is the node that spawns/parented the waiter. If spawner_key differs from to_key, the edge semantics are inconsistent and validation rejects it.
Source
Thrown at cmd/bd/graph_apply.go:632
}
if edge.Gate != "" && !types.IsValidWaitsForGate(edge.Gate) {
return fmt.Errorf("edge %d: invalid gate %q (valid: %s, %s)", i, edge.Gate, types.WaitsForAllChildren, types.WaitsForAnyChildren)
}
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 {View on GitHub (pinned to 71377f2769)
Solutions
- Set spawner_key equal to to_key (the waits-for target is the spawner)
- Or remove the spawner fields if you want a plain waits-for without gate semantics
- If you intended a non-spawner dependency, use a different dependency type
Example fix
// before
{"type": "waits-for", "spawner_key": "parent-a", "to_key": "sibling-b"}
// after
{"type": "waits-for", "spawner_key": "sibling-b", "to_key": "sibling-b"} Defensive patterns
Strategy: validation
Validate before calling
if e.Type == "waits-for" && e.SpawnerKey != "" && e.SpawnerKey != e.ToKey {
return fmt.Errorf("spawner_key must equal to_key for waits-for edges")
} Type guard
func spawnerMatchesTarget(e Edge) bool { return e.SpawnerKey == "" || e.SpawnerKey == e.ToKey } Prevention
- Treat waits-for spawner as the to endpoint, not an arbitrary third node
- Use plain dependency types when the target is not the spawner
- Template waits-for edges as a single unit (spawner == target)
When it happens
Trigger: An edge with type waits-for where spawner_key = "x" but to_key = "y" (x != y).
Common situations: Authoring waits-for edges between arbitrary nodes as if they were plain dependencies; copy-pasting a dependency edge and changing to_key without updating spawner_key; misunderstanding waits-for semantics.
Related errors
- edge %d: spawner_id %q must match to_id %q (the waits-for ta
- 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/213bf9f9dbf137b6.
Report an issue: GitHub.