gastownhall/beads · error

branch: from step %q not found

Error message

branch: from step %q not found

What it means

applyBranchesWithMap verifies that the rule's From step exists in the step map built from the formula's steps. If the ID is set but does not match any step, expansion fails — this catches dangling references after renames or typos.

Source

Thrown at internal/formula/controlflow.go:457

	if compose == nil || len(compose.Branch) == 0 {
		return nil
	}

	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]

View on GitHub (pinned to 71377f2769)

Solutions

  1. Update the branch rule's from to the current ID of the fan-out step
  2. List the formula's step IDs and diff them against from/steps/join references
  3. Restore the deleted step or remove the stale branch rule
  4. Check for casing/whitespace differences in the ID

Example fix

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

Strategy: validation

Validate before calling

func checkBranchFrom(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.From] {
			return fmt.Errorf("branch from %q not found", b.From)
		}
	}
	return nil
}

Try / catch

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

Prevention

When it happens

Trigger: ApplyBranches/ApplyControlFlow with a branch rule whose From names a step ID absent from the steps list (typo, renamed/deleted step, wrong casing).

Common situations: Renaming a step without updating the branch rule's from reference; copy-pasting branch config from another formula; case mismatch since step IDs are matched exactly.

Related errors


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