Hmbown/CodeWhale · warning · anyhow::Error

Once schedule has no future run after {}

Error message

Once schedule has no future run after {}

What it means

next_after_in_timezone for a Once { at } schedule can only ever return the single stored instant. When asked for the next run strictly after a moment that is at or past `at`, there is nothing left to fire, so it bails with the cutoff timestamp. This is the natural end-of-life of a one-shot schedule, surfaced as an error rather than a silent skip.

Source

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

        after: DateTime<Utc>,
        anchor_reference: DateTime<Utc>,
    ) -> Result<DateTime<Utc>> {
        self.next_after_in_timezone(after, anchor_reference, &Local)
    }

    fn next_after_in_timezone<Tz: TimeZone>(
        &self,
        after: DateTime<Utc>,
        anchor_reference: DateTime<Utc>,
        timezone: &Tz,
    ) -> Result<DateTime<Utc>> {
        let local_after = after.with_timezone(timezone);
        match self {
            Self::Once { at } => {
                if *at > after {
                    Ok(*at)
                } else {
                    bail!(
                        "Once schedule has no future run after {}",
                        after.to_rfc3339()
                    )
                }
            }
            Self::Hourly {
                interval_hours,
                byday,
                anchor_hour,
                anchor_minute,
            } => {
                if anchor_hour.is_some() || anchor_minute.is_some() {
                    let local_anchor_reference = anchor_reference.with_timezone(timezone);
                    let hour = anchor_hour.unwrap_or(local_anchor_reference.hour());
                    let minute = anchor_minute.unwrap_or(0);
                    let anchor_naive = local_anchor_reference
                        .date_naive()
                        .and_hms_opt(hour, minute, 0)

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Treat this error as schedule completion: mark the automation done/disable it instead of retrying.
  2. For missed one-shots you still want, recreate the rule with a future AT (`FREQ=ONCE;AT=...`) or switch to CRON.
  3. When previewing, check `if let AutomationSchedule::Once { at } = sched { if *at <= after { /* expired */ } }` before calling.

Example fix

// before
let next = sched.next_after(after, &tz)?; // bails: Once schedule has no future run

// after
let next = match sched.next_after(after, &tz) {
    Ok(t) => t,
    Err(e) if e.to_string().contains("no future run") => {
        mark_automation_complete(&id); // one-shot fired/expired
        return Ok(());
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: fallback

Validate before calling

if let AutomationSchedule::Once { at } = &schedule {
    if *at <= after {
        // One-shot already fired or expired; nothing to schedule.
        disable_automation(&id)?;
        return Ok(None);
    }
}
let next = schedule.next_after(after, &tz)?;

Type guard

fn is_expired_once(sched: &AutomationSchedule, after: chrono::DateTime<chrono::Utc>) -> bool {
    matches!(sched, AutomationSchedule::Once { at } if *at <= after)
}

Try / catch

match schedule.next_after(after, &tz) {
    Ok(next) => Some(next),
    Err(e) if e.to_string().contains("no future run") => { mark_automation_complete(&id); None }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling next_after/next_after_in_timezone on an ONCE schedule after its AT time has passed — e.g. the scheduler wakes late (machine asleep, process suspended) and recomputes the next run, or a missed-run recovery path asks what comes next.

Common situations: Laptop asleep at fire time; long queue or downtime; tests iterating next-run with an `after` beyond AT; UI previewing the 'next occurrence' of an expired one-shot.

Related errors


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