gastownhall/beads · error

loop %q: %w

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.

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)

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.