{"record":{"id":"cc066232a0037e38","repo":"Hmbown/CodeWhale","slug":"cron-schedules-require-expr","errorCode":null,"errorMessage":"CRON schedules require EXPR","messagePattern":"CRON schedules require EXPR","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":617,"sourceCode":"    }\n    let raw_at = parts\n        .get(\"AT\")\n        .ok_or_else(|| anyhow::anyhow!(\"ONCE schedules require AT\"))?;\n    let at = parse_once_at(raw_at)?;\n    Ok(AutomationSchedule::Once { at })\n}\n\nfn parse_cron_schedule(parts: &BTreeMap<String, String>) -> Result<AutomationSchedule> {\n    for key in parts.keys() {\n        if key != \"FREQ\" && key != \"EXPR\" {\n            bail!(\"Unsupported RRULE field '{key}' for CRON. Allowed: FREQ,EXPR\");\n        }\n    }\n    let expr = parts\n        .get(\"EXPR\")\n        .map(|value| value.trim().to_string())\n        .filter(|value| !value.is_empty())\n        .ok_or_else(|| anyhow::anyhow!(\"CRON schedules require EXPR\"))?;\n    ParsedCronExpr::parse(&expr)?;\n    Ok(AutomationSchedule::Cron { expr })\n}\n\nfn parse_once_at(raw: &str) -> Result<DateTime<Utc>> {\n    let trimmed = raw.trim();\n    if let Ok(at) = DateTime::parse_from_rfc3339(trimmed) {\n        return Ok(at.with_timezone(&Utc));\n    }\n    for format in [\"%Y-%m-%dT%H:%M:%S\", \"%Y-%m-%dT%H:%M\"] {\n        if let Ok(naive) = NaiveDateTime::parse_from_str(trimmed, format) {\n            return resolve_local_datetime(&Local, naive)\n                .map(|value| value.with_timezone(&Utc))\n                .ok_or_else(|| anyhow::anyhow!(\"ONCE local time does not exist: {trimmed}\"));\n        }\n    }\n    bail!(\"Failed to parse ONCE AT '{trimmed}'. Use local YYYY-MM-DDTHH:MM[:SS] or RFC3339\")\n}","sourceCodeStart":599,"sourceCodeEnd":635,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/automation_manager.rs#L599-L635","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nlet rrule = \"FREQ=CRON;EXPR=   \";\n// after\nlet rrule = \"FREQ=CRON;EXPR=*/17 * * * *\";","handlingStrategy":"validation","validationCode":"let parts: std::collections::BTreeMap<&str, &str> = rrule\n    .split(';')\n    .filter_map(|kv| kv.split_once('='))\n    .collect();\nif parts.get(\"FREQ\").copied() == Some(\"CRON\") {\n    let expr = parts.get(\"EXPR\").context(\"CRON schedules require EXPR=<5-field cron>\")?;\n    ensure!(!expr.trim().is_empty(), \"EXPR must not be empty\");\n    ParsedCronExpr::parse(expr.trim())?; // reuse the same parser to pre-flight the fields\n}","typeGuard":"fn is_valid_cron_rrule(rrule: &str) -> bool {\n    let parts: std::collections::BTreeMap<&str, &str> = rrule\n        .split(';')\n        .filter_map(|kv| kv.split_once('='))\n        .collect();\n    parts.get(\"FREQ\").copied() == Some(\"CRON\")\n        && parts.get(\"EXPR\").is_some_and(|e| !e.trim().is_empty() && e.trim().split_whitespace().count() == 5)\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["automation","cron","rrule","validation"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}