gastownhall/beads · error
loop %q: max is required when until is set
Error message
loop %q: max is required when until is set
What it means
Until-loops repeat while their condition is unsatisfied; without a Max cap they could loop forever, so validateLoopSpec requires Max > 0 whenever Until is set. Max=0 (unset) triggers this error.
Source
Thrown at internal/formula/controlflow.go:82
if loop.Count > 0 {
loopTypes++
}
if loop.Until != "" {
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 presentView on GitHub (pinned to 71377f2769)
Solutions
- Set Max to a positive integer safety cap alongside Until: `&LoopSpec{Until: cond, Max: 10, Body: body}`
- Pick Max from the workflow's realistic iteration budget (e.g. retries+slack) rather than leaving it unset
- Add a pre-cook lint that flags any LoopSpec with Until set and Max==0
Example fix
// before
loop := &formula.LoopSpec{Until: "step.status == 'complete'", Body: body}
steps, err := formula.ApplyLoops(steps) // max is required when until is set
// after
loop := &formula.LoopSpec{Until: "step.status == 'complete'", Max: 10, Body: body}
steps, err := formula.ApplyLoops(steps) Defensive patterns
Strategy: validation
Validate before calling
if loop != nil && loop.Until != "" && loop.Max == 0 {
return fmt.Errorf("until-loop on %q needs a positive Max cap", stepID)
}
steps, err := formula.ApplyLoops(steps) Type guard
func untilLoopCapped(l *formula.LoopSpec) bool {
return l.Until == "" || l.Max > 0
} Try / catch
steps, err := formula.ApplyLoops(steps)
if err != nil {
if strings.Contains(err.Error(), "max is required when until is set") {
return fmt.Errorf("uncapped until-loop: %w", err)
}
return err
} Prevention
- Always pair Until with a positive Max
- Treat Max as a mandatory safety budget for conditional loops
- Add a schema/lint rule rejecting until without max
When it happens
Trigger: ApplyLoops with `&LoopSpec{Until: "step.status == 'complete'", Body: body}` and Max left at its zero value; Until set via variable substitution while Max was never populated.
Common situations: Copy-pasting an until-loop example and omitting the max line; assuming a default max exists (there is none); a schema that serializes Max=0 by omitting it, losing the cap.
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
- applying control flow: %w
- loop %q: body is required
- loop %q: one of count, until, or range is required
- loop %q: only one of count, until, or range can be specified
- loop %q: count must be positive
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/c272857b36331182.
Report an issue: GitHub.