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

  1. Set spawner_id equal to to_id
  2. Remove spawner_id if gate/spawner semantics aren't needed
  3. 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

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


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