gastownhall/beads · error

loop %q: count must be positive

Error message

loop %q: count must be positive

What it means

validateLoopSpec guards numeric loop bounds: a negative Count would make fixed-count expansion nonsensical (the expansion loop simply wouldn't run, silently dropping the body), so any negative Count is rejected with this error naming the step.

Source

Thrown at internal/formula/controlflow.go:86

		loopTypes++
	}
	if loop.Range != "" {
		loopTypes++
	}

	if loopTypes == 0 {
		return fmt.Errorf("loop %q: one of count, until, or range is required", stepID)
	}
	if loopTypes > 1 {
		return fmt.Errorf("loop %q: only one of count, until, or range can be specified", stepID)
	}

	if loop.Until != "" && loop.Max == 0 {
		return fmt.Errorf("loop %q: max is required when until is set", stepID)
	}

	if loop.Count < 0 {
		return fmt.Errorf("loop %q: count must be positive", stepID)
	}

	if loop.Max < 0 {
		return fmt.Errorf("loop %q: max must be positive", stepID)
	}

	// Validate until condition syntax if present
	if loop.Until != "" {
		if _, err := ParseCondition(loop.Until); err != nil {
			return fmt.Errorf("loop %q: invalid until condition %q: %w", stepID, loop.Until, err)
		}
	}

	// Validate range syntax if present
	if loop.Range != "" {
		if err := ValidateRange(loop.Range); err != nil {
			return fmt.Errorf("loop %q: invalid range %q: %w", stepID, loop.Range, err)
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Clamp or validate user-supplied counts to >=1 before constructing LoopSpec
  2. Sanitize computed counts: `if n < 1 { n = 1 }` or fall back to an until-loop for data-driven repetition
  3. Validate the formula file schema (count: integer >= 1) at load time with a clear message

Example fix

// before
count := len(items) - 1 // can be -1
loop := &formula.LoopSpec{Count: count, Body: body}
steps, err := formula.ApplyLoops(steps) // count must be positive
// after
count := len(items)
if count < 1 { count = 1 }
loop := &formula.LoopSpec{Count: count, Body: body}
steps, err := formula.ApplyLoops(steps)
Defensive patterns

Strategy: validation

Validate before calling

n, err := strconv.Atoi(userCount)
if err != nil || n < 1 {
	return fmt.Errorf("loop count must be a positive integer, got %q", userCount)
}
loop := &formula.LoopSpec{Count: n, Body: body}
steps, err := formula.ApplyLoops(steps)

Type guard

func positiveInt(v int) bool { return v > 0 }
if loop != nil && loop.Count < 0 { /* reject before ApplyLoops */ }

Try / catch

steps, err := formula.ApplyLoops(steps)
if err != nil {
	if strings.Contains(err.Error(), "count must be positive") {
		return fmt.Errorf("bad loop count: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: ApplyLoops with a LoopSpec where Count < 0 (e.g. -1 from a subtraction bug or user input like count: -2 in a formula file). Note Count<=0 otherwise falls into the 'one of count/until/range' check; negative values reach this check via loopTypes counting only Count>0.

Common situations: User-supplied count from config/UI accepted without range validation; arithmetic producing -1 (e.g. len(items)-1 passed as a repeat count); templating that interpolates an empty value into a number field.

Related errors


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