gastownhall/beads · error

branch: join step 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.

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.

Appendix: 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)