gastownhall/beads · error

branch: join is required

Error message

branch: join is required

What it means

Each branch rule must name a Join step where the parallel paths converge. applyBranchesWithMap rejects rules with an empty Join because the library cannot reconstruct execution ordering without a convergence point.

Source

Thrown at internal/formula/controlflow.go:452

// applyBranchesWithMap applies branch rules using a pre-built stepMap.
// This is the internal implementation used by both ApplyBranches and ApplyControlFlow.
// The stepMap entries are modified in place.
func applyBranchesWithMap(stepMap map[string]*Step, compose *ComposeRules) error {
	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]

View on GitHub (pinned to 71377f2769)

Solutions

  1. Add `join:` naming the convergence step in each branch rule
  2. Confirm the join step is a real step ID in the formula (checked immediately after)
  3. Re-check YAML indentation so join belongs to the same branch list item

Example fix

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

Strategy: validation

Validate before calling

for i, b := range compose.Branch {
	if b.Join == "" {
		return fmt.Errorf("branch[%d]: join is required", i)
	}
}

Try / catch

steps, err := formula.ApplyBranches(steps, compose)
if err != nil {
	if strings.Contains(err.Error(), "join is required") {
		return fmt.Errorf("compose branch missing 'join': %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: ApplyBranches or ApplyControlFlow encountering a compose.Branch entry whose Join field is empty/omitted.

Common situations: YAML branch block missing `join:`; assuming join defaults to the last step (it does not); programmatic construction of ComposeRules leaving Join unset.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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