Hmbown/CodeWhale · error · anyhow::Error

Failed to parse ONCE AT '{trimmed}'. Use local YYYY-MM-DDTHH

Error message

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

What it means

parse_once_at accepts exactly two shapes: RFC3339 with an explicit offset (DateTime::parse_from_rfc3339), or a naive local 'YYYY-MM-DDTHH:MM' / 'YYYY-MM-DDTHH:MM:SS' resolved in the machine's local timezone. Anything else — date-only, a space instead of 'T', fractional seconds, or slash formats — falls through to this bail. (A naive local time that falls in a DST gap yields the sibling 'ONCE local time does not exist' error.)

Source

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

        .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")
}

#[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"
            );

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Emit the 'T' separator: 2026-08-03T14:30
  2. For unambiguous scheduling send RFC3339 with an offset, e.g. 2026-08-03T14:30:00Z or +02:00
  3. Normalize timestamps to RFC3339 client-side before create_automation
  4. For naive local input keep seconds optional but avoid sub-second precision

Example fix

// before
rrule = "FREQ=ONCE;AT=2026-08-03 14:30"

// after (unambiguous UTC)
rrule = "FREQ=ONCE;AT=2026-08-03T14:30:00Z"
Defensive patterns

Strategy: validation

Validate before calling

fn parseable_once_at(raw: &str) -> bool {
    let t = raw.trim();
    if chrono::DateTime::parse_from_rfc3339(t).is_ok() {
        return true;
    }
    ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"]
        .iter()
        .any(|f| chrono::NaiveDateTime::parse_from_str(t, f).is_ok())
}

Prevention

When it happens

Trigger: AT=2026-08-03 (date only); AT='2026-08-03 14:30' (space separator); AT=2026-08-03T14:30:30.5Z (fractional seconds); AT=03/08/2026.

Common situations: User-entered timestamps from forms; locales using slash or space formats; pasting output of the date command unmodified.

Understand the failure class

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/4e85b66b7a7c3653. Report an issue: GitHub.