gastownhall/beads · error

branch: join step %q not found

Error message

branch: join step %q not found

What it means

applyBranchesWithMap verifies the rule's Join step exists in the step map, just like From. A Join pointing at a nonexistent step ID means the convergence point is dangling and the branch cannot be applied.

Source

Thrown at internal/formula/controlflow.go:460

	for _, branch := range compose.Branch {
		// Validate the branch rule
		if branch.From == "" {
			return fmt.Errorf("branch: from is required")
		}
		if len(branch.Steps) == 0 {
			return fmt.Errorf("branch: steps is required")
		}
		if branch.Join == "" {
			return fmt.Errorf("branch: join is required")
		}

		// Verify all steps exist
		if _, ok := stepMap[branch.From]; !ok {
			return fmt.Errorf("branch: from step %q not found", branch.From)
		}
		if _, ok := stepMap[branch.Join]; !ok {
			return fmt.Errorf("branch: join step %q not found", branch.Join)
		}
		for _, stepID := range branch.Steps {
			if _, ok := stepMap[stepID]; !ok {
				return fmt.Errorf("branch: parallel step %q not found", stepID)
			}
		}

		// Add dependencies: branch steps depend on 'from'
		for _, stepID := range branch.Steps {
			step := stepMap[stepID]
			step.Needs = appendUnique(step.Needs, branch.From)
		}

		// Add dependencies: 'join' depends on all branch steps
		joinStep := stepMap[branch.Join]
		for _, stepID := range branch.Steps {
			joinStep.Needs = appendUnique(joinStep.Needs, stepID)
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Update join to the current ID of the convergence step
  2. Verify the join step is a top-level step (not nested inside a loop body)
  3. Remove the branch rule if the join step no longer exists
  4. Check casing/whitespace in the join ID

Example fix

# before
- from: start
  steps: [a, b]
  join: finsh
# after
- from: start
  steps: [a, b]
  join: finish
Defensive patterns

Strategy: validation

Validate before calling

func checkBranchJoin(steps []*Step, compose *formula.ComposeRules) error {
	ids := map[string]bool{}
	for _, s := range steps { ids[s.ID] = true }
	for _, b := range compose.Branch {
		if !ids[b.Join] {
			return fmt.Errorf("branch join %q not found", b.Join)
		}
	}
	return nil
}

Try / catch

steps, err := formula.ApplyBranches(steps, compose)
if err != nil {
	if strings.Contains(err.Error(), "join step") && strings.Contains(err.Error(), "not found") {
		return fmt.Errorf("stale branch 'join' reference: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: ApplyBranches/ApplyControlFlow with a branch rule whose Join names a step ID not present among the formula's steps.

Common situations: Join step deleted or renamed during refactoring; typo in the join ID; referring to a step defined inside a loop body that is not a top-level step at compose time.

Related errors


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