gastownhall/beads · error

duplicate step IDs after expansion: %v

Error message

duplicate step IDs after expansion: %v

What it means

After all expand/map rules are applied, ApplyExpansions runs findDuplicateStepIDs over the final step list. If two or more steps share an ID, the result would be ambiguous, so it fails with the list of duplicated IDs.

Source

Thrown at internal/formula/expand.go:160

			result = replaceStep(result, targetStep.ID, expandedSteps)
			expanded[targetStep.ID] = true

			// Update dependencies: any step that depended on the target should now
			// depend on the last step of the expansion
			if len(expandedSteps) > 0 {
				lastStepID := expandedSteps[len(expandedSteps)-1].ID
				result = UpdateDependenciesForExpansion(result, targetStep.ID, lastStepID)
			}

			// Rebuild stepMap from result so subsequent iterations see resolved deps
			stepMap = buildStepMap(result)
		}
	}

	// Validate no duplicate step IDs after expansion
	if dups := findDuplicateStepIDs(result); len(dups) > 0 {
		return nil, fmt.Errorf("duplicate step IDs after expansion: %v", dups)
	}

	return result, nil
}

// findDuplicateStepIDs returns any duplicate step IDs found in the steps slice.
// It recursively checks all children.
func findDuplicateStepIDs(steps []*Step) []string {
	seen := make(map[string]int)
	countStepIDs(steps, seen)

	var dups []string
	for id, count := range seen {
		if count > 1 {
			dups = append(dups, id)
		}
	}
	return dups

View on GitHub (pinned to 71377f2769)

Solutions

  1. Make template step IDs unique, e.g. include ${parent} or an input variable in the id
  2. Narrow map select patterns so no step is expanded by two rules
  3. Rename static steps that collide with template-generated IDs

Example fix

# before
steps:
  - id: "child"       # collides for every expansion
# after
steps:
  - id: "${parent}-child"
Defensive patterns

Strategy: validation

Validate before calling

ids := map[string]int{}
for _, s := range expandedStepsLocalSimulation { ids[s.ID]++ }
for id, n := range ids {
    if n > 1 { return fmt.Errorf("would produce duplicate step id %q", id) }
}

Try / catch

result, err := formula.ApplyExpansions(steps, compose)
if err != nil && strings.Contains(err.Error(), "duplicate step IDs") {
    return fmt.Errorf("fix template id variables to guarantee uniqueness: %w", err)
}

Prevention

When it happens

Trigger: ApplyExpansions final validation finds duplicates, typically because two map rules expanded steps into templates producing the same generated ID, or an expansion template produced IDs colliding with existing steps.

Common situations: Template IDs built from variables that resolve to the same value for different inputs; overlapping map select patterns expanding the same step twice; hardcoded template IDs colliding with static step IDs.

Related errors


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