gastownhall/beads · error

loop

Error message

loop %q: %w

What it means

expandLoopWithVars parses the step's Loop.Range with ParseRange using the provided template variables and wraps any parse failure. This is the variable-substitution range path: the range string is expanded with vars before parsing, and if the resulting spec is unparseable the loop expansion aborts.

Solutions

  1. Ensure every variable used in the range expression is present in the vars map and resolves to an integer
  2. Print/log the substituted range string to see what ParseRange actually received
  3. Fix the range syntax in the formula definition
  4. Use validateLoopSpec/ApplyLoops preflight to catch malformed ranges before expansion

Example fix

// before
expandLoop(step, map[string]string{"start": "one"}) // range: "{{start}}..3"
// after
expandLoop(step, map[string]string{"start": "1"})
Defensive patterns

Strategy: validation

Validate before calling

func checkRangeVars(r string, vars map[string]string) error {
	for _, m := range regexp.MustCompile(`\{\{(\w+)\}\}`).FindAllStringSubmatch(r, -1) {
		if _, ok := vars[m[1]]; !ok {
			return fmt.Errorf("range %q: missing variable %q", r, m[1])
		}
	}
	return nil
}

Try / catch

expanded, err := expandLoops(steps, vars)
if err != nil {
	if strings.Contains(err.Error(), "loop \"") {
		return fmt.Errorf("range loop failed to parse; check vars resolve to integers: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: expandLoop (called during formula expansion with variables) seeing a Loop.Range that, after variable substitution, still fails ParseRange — e.g. unresolved `{{var}}` placeholders or malformed bounds.

Common situations: A variable referenced in the range was not provided in the vars map, so `{{n}}` remains literal; a variable resolves to a non-numeric string; typo in the range separator after substitution.

Related errors


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

Appendix: source

Thrown at internal/formula/controlflow.go:146

		}

		// Recursively expand any nested loops FIRST
		var err error
		result, err = ApplyLoops(result)
		if err != nil {
			return nil, err
		}

		// THEN chain iterations on the expanded result
		// This must happen AFTER recursive expansion so we chain the final steps
		if step.Loop.Count > 1 {
			result = chainExpandedIterations(result, step.ID, step.Loop.Count)
		}
	} else if step.Loop.Range != "" {
		// Range loop: expand body for each value in the computed range
		rangeSpec, err := ParseRange(step.Loop.Range, vars)
		if err != nil {
			return nil, fmt.Errorf("loop %q: %w", step.ID, err)
		}

		// Validate range
		if rangeSpec.End < rangeSpec.Start {
			return nil, fmt.Errorf("loop %q: range end (%d) is less than start (%d)",
				step.ID, rangeSpec.End, rangeSpec.Start)
		}

		// Expand body for each value in range
		count := rangeSpec.End - rangeSpec.Start + 1
		iterNum := 0
		for val := rangeSpec.Start; val <= rangeSpec.End; val++ {
			iterNum++
			// Build iteration vars: include the loop variable if specified
			iterVars := make(map[string]string)
			if step.Loop.Var != "" {
				iterVars[step.Loop.Var] = fmt.Sprintf("%d", val)
			}

View on GitHub (pinned to 71377f2769)