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

  1. Provide a 5-field local cron: FREQ=CRON;EXPR=*/17 * * * *
  2. Quote the whole RRULE so the cron's spaces survive; an EXPR that fails to parse raises its own parse error
  3. 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

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


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/cc066232a0037e38. Report an issue: GitHub.