Hmbown/CodeWhale · error

ONCE local time does not exist: {trimmed}

Error message

ONCE local time does not exist: {trimmed}

What it means

AT values without an offset are read as local wall time and mapped through resolve_local_datetime; the mapping returns None exactly when that wall-clock reading never existed locally — a time inside a DST spring-forward gap. Unlike HOURLY anchors (which silently skip nonexistent clock times), ONCE refuses to guess a different fire time for your one-shot and errors, naming the offending timestamp.

Source

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

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

#[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 {

View on GitHub (pinned to 8880682c63)

Solutions

  1. Use RFC3339 with an explicit offset/UTC: AT=2026-03-08T06:30:00Z
  2. Or pick a local time outside the gap (e.g. 03:30 after the jump)
  3. Pre-validate local AT strings with Local.from_local_datetime(naive).single().is_some() before persisting

Example fix

// before (America/New_York: 02:30 never exists on 2026-03-08)
let rrule = "FREQ=ONCE;AT=2026-03-08T02:30";
// after
let rrule = "FREQ=ONCE;AT=2026-03-08T06:30:00Z";
Defensive patterns

Strategy: validation

Validate before calling

let trimmed = at_raw.trim();
if chrono::DateTime::parse_from_rfc3339(trimmed).is_err() {
    let naive = chrono::NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S")
        .or_else(|_| chrono::NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M"))?;
    if chrono::Local.from_local_datetime(&naive).single().is_none() {
        anyhow::bail!("local time {trimmed} does not exist (DST spring-forward gap); use an RFC3339 time with offset");
    }
}

Type guard

fn once_local_time_exists(raw: &str) -> bool {
    let trimmed = raw.trim();
    if chrono::DateTime::parse_from_rfc3339(trimmed).is_ok() {
        return true; // explicit offset never hits the gap
    }
    ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"]
        .iter()
        .find_map(|f| chrono::NaiveDateTime::parse_from_str(trimmed, f).ok())
        .is_none_or(|naive| chrono::Local.from_local_datetime(&naive).single().is_some())
}

Prevention

When it happens

Trigger: AT=2026-03-08T02:30 in a US timezone where clocks jump 02:00->03:00 that day; any local naive time that a forward offset transition skipped. RFC3339 inputs with an explicit offset never hit this path.

Common situations: One-shot automations placed at 2:00-2:59 AM on the spring-forward date; servers in a different timezone than the schedule author; tz-database rule changes moving the gap.

Related errors


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