Hmbown/CodeWhale · error · anyhow::Error

CRON EXPR must have exactly 5 fields: minute hour…

Error message

CRON EXPR must have exactly 5 fields: minute hour day-of-month month day-of-week

What it means

ParsedCronExpr::parse splits the EXPR on whitespace and requires exactly five fields — minute, hour, day-of-month, month, day-of-week — before validating each field. Any other count bails with this message describing the expected layout. It fires both when saving a CRON schedule and when next_after_in_timezone re-parses the stored expression.

Solutions

  1. Provide exactly five whitespace-separated fields: 'minute hour day-of-month month day-of-week' (e.g. '0 9 * * *').
  2. Drop the seconds field from 6-field cron expressions (this parser has minute granularity only).
  3. Use '*' for fields you don't want to constrain instead of omitting them.

Example fix

// before
FREQ=CRON;EXPR="0 9 * * * *"
// after
FREQ=CRON;EXPR="0 9 * * *"
Defensive patterns

Strategy: validation

Validate before calling

fn valid_cron_expr(expr: &str) -> bool {
    expr.split_whitespace().count() == 5
}

Try / catch

match AutomationSchedule::parse(rrule) {
    Ok(s) => save(s),
    Err(e) if e.to_string().contains("exactly 5 fields") => show_cron_format_help(&e),
    Err(e) => report(e),
}

Prevention

When it happens

Trigger: AutomationSchedule::parse with FREQ=CRON;EXPR=<expr> where split_whitespace yields != 5 fields: 6-field cron with seconds, 3-field shorthand, or an expression with collapsed/extra whitespace not intended as fields.

Common situations: Users pasting 6-field (with seconds) cron from systemd or Quartz; omitting a field (e.g. '0 9 *' meaning daily 9am); extra spaces normally fine but a missing field is not.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

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

        }
    }
    bail!("Failed to parse ONCE AT '{trimmed}'. Use local YYYY-MM-DDTHH:MM[:SS] or RFC3339")
}

#[derive(Debug, Clone)]
struct ParsedCronExpr {
    minute: CronField,
    hour: CronField,
    day_of_month: CronField,
    month: CronField,
    day_of_week: CronField,
}

impl ParsedCronExpr {
    fn parse(expr: &str) -> Result<Self> {
        let fields: Vec<&str> = expr.split_whitespace().collect();
        if fields.len() != 5 {
            bail!(
                "CRON EXPR must have exactly 5 fields: minute hour day-of-month month day-of-week"
            );
        }
        let parsed = Self {
            minute: CronField::parse(fields[0], 0, 59, CronNameMap::none(), "minute")?,
            hour: CronField::parse(fields[1], 0, 23, CronNameMap::none(), "hour")?,
            day_of_month: CronField::parse(fields[2], 1, 31, CronNameMap::none(), "day-of-month")?,
            month: CronField::parse(fields[3], 1, 12, CronNameMap::month(), "month")?,
            day_of_week: CronField::parse(fields[4], 0, 7, CronNameMap::weekday(), "day-of-week")?
                .normalized_day_of_week(),
        };
        parsed.validate_date_space()?;
        Ok(parsed)
    }

    fn matches(&self, candidate: NaiveDateTime) -> bool {
        if !self.minute.contains(candidate.minute())
            || !self.hour.contains(candidate.hour())

View on GitHub (pinned to 73e0f67d83)