gastownhall/beads · error

edge %d: cannot specify both spawner_key and spawner_id

Error message

edge %d: cannot specify both spawner_key and spawner_id

What it means

A waits-for edge identifies its spawner either by plan key (spawner_key) or by issue ID (spawner_id), never both. Supplying both is ambiguous, so validation fails the plan rather than picking one arbitrarily.

Source

Thrown at cmd/bd/graph_apply.go:619

		}
		if edge.ToKey == "" && edge.ToID == "" {
			return fmt.Errorf("edge %d: must specify to_key or to_id", i)
		}
		if edge.Type != "" {
			dt := types.DependencyType(edge.Type)
			if !dt.IsValid() {
				return fmt.Errorf("edge %d: invalid dependency type %q", i, edge.Type)
			}
		}
		if edge.Gate != "" || edge.SpawnerKey != "" || edge.SpawnerID != "" {
			if graphApplyDependencyType(edge.Type) != types.DepWaitsFor {
				return fmt.Errorf("edge %d: gate/spawner fields require type %q", i, types.DepWaitsFor)
			}
			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)
			}
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Delete spawner_id and keep spawner_key when the spawner node lives in this plan
  2. Delete spawner_key and keep spawner_id when referencing an issue outside the plan (and use to_id accordingly)
  3. Regenerate the plan so exactly one spawner identifier is emitted

Example fix

// before
{"type": "waits-for", "spawner_key": "parent", "spawner_id": "bd-42"}
// after
{"type": "waits-for", "spawner_key": "parent", "to_key": "parent"}
Defensive patterns

Strategy: validation

Validate before calling

if e.SpawnerKey != "" && e.SpawnerID != "" {
  return fmt.Errorf("edge: specify only one of spawner_key or spawner_id")
}

Type guard

func hasSingleSpawnerRef(e Edge) bool { return (e.SpawnerKey != "") != (e.SpawnerID != "") }

Prevention

When it happens

Trigger: An edge sets both spawner_key and spawner_id, typically after a tool enriched a key-only plan with IDs, or hand-merging two edge definitions.

Common situations: Merging plans from two generators; post-processing scripts that fill in spawner_id without clearing spawner_key; manual edits adding the ID for clarity.

Related errors


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