{"record":{"id":"8825f993391f9d1c","repo":"Hmbown/CodeWhale","slug":"once-schedules-require-at","errorCode":null,"errorMessage":"ONCE schedules require AT","messagePattern":"ONCE schedules require AT","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":602,"sourceCode":"            \"SU\" => Weekday::Sun,\n            other => bail!(\"Invalid BYDAY value '{other}'\"),\n        };\n        if !days.contains(&day) {\n            days.push(day);\n        }\n    }\n    Ok(days)\n}\n\nfn parse_once_schedule(parts: &BTreeMap<String, String>) -> Result<AutomationSchedule> {\n    for key in parts.keys() {\n        if key != \"FREQ\" && key != \"AT\" {\n            bail!(\"Unsupported RRULE field '{key}' for ONCE. Allowed: FREQ,AT\");\n        }\n    }\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}","sourceCodeStart":584,"sourceCodeEnd":620,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/automation_manager.rs#L584-L620","documentation":"AutomationSchedule::parse_rrule treats FREQ=ONCE as a one-shot: only FREQ and AT are allowed, and the AT key is mandatory because a one-shot without a fire time is meaningless. A missing AT is rejected with this error at parse time, before the automation is stored.","triggerScenarios":"Creating/updating an automation with rrule 'FREQ=ONCE' (no ';AT=...'), or with the field misspelled so the parser sees no AT key. AT accepts local 'YYYY-MM-DDTHH:MM[:SS]' or RFC3339.","commonSituations":"Authors copying the ONCE template but deleting the AT clause; model-generated schedules omitting the timestamp; field-name typos like 'AT ' or 'AT=' (empty value fails later in parse_once_at instead).","solutions":["Append AT with a local or RFC3339 time: FREQ=ONCE;AT=2026-08-03T14:30 or FREQ=ONCE;AT=2026-08-03T12:30:00Z","Use exactly the key AT and no other fields (only FREQ and AT are allowed for ONCE)","Validate with parse_rrule before persisting"],"exampleFix":"// before\nlet rrule = \"FREQ=ONCE\";\n// after\nlet rrule = \"FREQ=ONCE;AT=2026-08-03T14:30\";","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(\"ONCE\") {\n    let at = parts.get(\"AT\").context(\"ONCE schedules require AT=YYYY-MM-DDTHH:MM[:SS] or RFC3339\")?;\n    ensure!(!at.trim().is_empty(), \"AT must not be empty\");\n}","typeGuard":"fn is_valid_once_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(\"ONCE\")\n        && parts.get(\"AT\").is_some_and(|at| !at.trim().is_empty())\n        && parts.keys().all(|k| matches!(*k, \"FREQ\" | \"AT\"))\n}","tryCatchPattern":null,"preventionTips":["Emit ONCE schedules from a template that always includes AT","Accept only 'YYYY-MM-DDTHH:MM[:SS]' local or RFC3339 AT values; anything else fails in parse_once_at with a usage hint","Run parse_rrule on user- or model-supplied RRULEs before persisting"],"tags":["automation","rrule","scheduling","validation"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}