Hmbown/CodeWhale · error · anyhow::Error

CRON step must be >= 1

Error message

CRON {field_name} step must be >= 1

What it means

A step value in a cron field (the '/n' in '*​/n' or 'range/n') must be at least 1. A step of 0 is meaningless — it would repeat with no advancement — so the parser rejects it with the field name interpolated.

Solutions

  1. Replace the 0 step with a real positive step, e.g. '*/5' for every 5 minutes
  2. Use '*' alone to mean every unit instead of '*/0'
  3. Validate the step variable is >= 1 at the call site before building the expression

Example fix

// before
let expr = format!("*/{} * * * *", interval); // interval = 0
// after
assert!(interval >= 1);
let expr = if interval == 1 { "*".into() } else { format!("*/{} * * * *", interval) };
Defensive patterns

Strategy: validation

Validate before calling

assert!(step >= 1, "cron step must be >= 1");

Prevention

When it happens

Trigger: Writing '*/0 * * * *', '0 0 1-10/0 * *', or computing the step from a variable that is 0 (e.g. interval_minutes=0).

Common situations: Config value of 0 intended to mean 'every' or unset; division-derived step rounding to 0; typo using /0 instead of *.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/e47b883b37902759. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/automation_manager.rs:837

    fn parse(raw: &str, min: u32, max: u32, names: CronNameMap, field_name: &str) -> Result<Self> {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            bail!("CRON {field_name} field must not be empty");
        }
        let mut values = Vec::new();
        let is_wildcard = trimmed == "*";
        for part in trimmed.split(',') {
            let part = part.trim();
            if part.is_empty() {
                bail!("CRON {field_name} field contains an empty list item");
            }
            let (base, step) = if let Some((base, step)) = part.split_once('/') {
                let step = step
                    .trim()
                    .parse::<u32>()
                    .with_context(|| format!("Failed to parse CRON {field_name} step"))?;
                if step == 0 {
                    bail!("CRON {field_name} step must be >= 1");
                }
                (base.trim(), step)
            } else {
                (part, 1)
            };

            let range = if base == "*" {
                (min, max)
            } else if let Some((start, end)) = base.split_once('-') {
                let start = parse_cron_atom(start.trim(), min, max, names, field_name)?;
                let end = parse_cron_atom(end.trim(), min, max, names, field_name)?;
                if start > end {
                    bail!("CRON {field_name} range start must be <= end");
                }
                (start, end)
            } else {
                let start = parse_cron_atom(base, min, max, names, field_name)?;
                if part.contains('/') {

View on GitHub (pinned to 73e0f67d83)