Hmbown/CodeWhale · error
CRON schedules require EXPR
Error message
CRON schedules require EXPR
What it means
AutomationSchedule::parse_rrule treats FREQ=CRON as a wrapper around a standard 5-field local-time cron expression carried in EXPR. The value is trimmed and empty values count as missing, so this error covers both an absent EXPR key and one that is blank or whitespace-only. The expression must additionally parse (ParsedCronExpr::parse) or you get a parse error instead.
Source
Thrown at crates/tui/src/automation_manager.rs:617
}
let raw_at = parts
.get("AT")
.ok_or_else(|| anyhow::anyhow!("ONCE schedules require AT"))?;
let at = parse_once_at(raw_at)?;
Ok(AutomationSchedule::Once { at })
}
fn parse_cron_schedule(parts: &BTreeMap<String, String>) -> Result<AutomationSchedule> {
for key in parts.keys() {
if key != "FREQ" && key != "EXPR" {
bail!("Unsupported RRULE field '{key}' for CRON. Allowed: FREQ,EXPR");
}
}
let expr = parts
.get("EXPR")
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow::anyhow!("CRON schedules require EXPR"))?;
ParsedCronExpr::parse(&expr)?;
Ok(AutomationSchedule::Cron { expr })
}
fn parse_once_at(raw: &str) -> Result<DateTime<Utc>> {
let trimmed = raw.trim();
if let Ok(at) = DateTime::parse_from_rfc3339(trimmed) {
return Ok(at.with_timezone(&Utc));
}
for format in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"] {
if let Ok(naive) = NaiveDateTime::parse_from_str(trimmed, format) {
return resolve_local_datetime(&Local, naive)
.map(|value| value.with_timezone(&Utc))
.ok_or_else(|| anyhow::anyhow!("ONCE local time does not exist: {trimmed}"));
}
}
bail!("Failed to parse ONCE AT '{trimmed}'. Use local YYYY-MM-DDTHH:MM[:SS] or RFC3339")
}View on GitHub (pinned to 8880682c63)
Solutions
- Provide a 5-field local cron: FREQ=CRON;EXPR=*/17 * * * *
- Quote the whole RRULE so the cron's spaces survive; an EXPR that fails to parse raises its own parse error
- Validate with parse_rrule before persisting
Example fix
// before let rrule = "FREQ=CRON;EXPR= "; // after let rrule = "FREQ=CRON;EXPR=*/17 * * * *";
Defensive patterns
Strategy: validation
Validate before calling
let parts: std::collections::BTreeMap<&str, &str> = rrule
.split(';')
.filter_map(|kv| kv.split_once('='))
.collect();
if parts.get("FREQ").copied() == Some("CRON") {
let expr = parts.get("EXPR").context("CRON schedules require EXPR=<5-field cron>")?;
ensure!(!expr.trim().is_empty(), "EXPR must not be empty");
ParsedCronExpr::parse(expr.trim())?; // reuse the same parser to pre-flight the fields
} Type guard
fn is_valid_cron_rrule(rrule: &str) -> bool {
let parts: std::collections::BTreeMap<&str, &str> = rrule
.split(';')
.filter_map(|kv| kv.split_once('='))
.collect();
parts.get("FREQ").copied() == Some("CRON")
&& parts.get("EXPR").is_some_and(|e| !e.trim().is_empty() && e.trim().split_whitespace().count() == 5)
} Prevention
- Remember EXPR is trimmed: blank or whitespace-only counts as missing
- Quote RRULE strings in shells/configs so the cron expression's four spaces survive intact
- Pre-flight EXPR through the same 5-field cron parser before storing
- Generate CRON RRULEs programmatically rather than by string surgery on templates
When it happens
Trigger: Creating/updating an automation with rrule 'FREQ=CRON' or 'FREQ=CRON;EXPR=' or 'FREQ=CRON;EXPR= ' — no usable expression where EXPR is expected (only FREQ and EXPR are allowed keys).
Common situations: Templates with a placeholder EXPR left empty; shell quoting that swallowed the expression's spaces when constructing the RRULE; model-generated schedules dropping the EXPR clause.
Related errors
- WEEKLY schedules require BYDAY
- WEEKLY schedules require BYHOUR
- WEEKLY schedules require BYMINUTE
- ONCE schedules require AT
- Invalid CRON {field_name} value '{raw}'
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/cc066232a0037e38.
Report an issue: GitHub.