gastownhall/beads · error

loop : 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.

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.

Appendix: 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)