tinyhumansai/openhuman · error

Invalid schedule: 'at' must be in the future

Error message

Invalid schedule: 'at' must be in the future

What it means

validate_schedule rejects a Schedule::At whose timestamp is <= the `now` it is validated against: a one-shot job scheduled in the past can never fire. Validation happens at job create/update, comparing against the caller-supplied current time.

Source

Thrown at src/openhuman/cron/schedule.rs:77

pub fn validate_schedule(schedule: &Schedule, now: DateTime<Utc>) -> Result<()> {
    match schedule {
        Schedule::Cron {
            expr,
            tz,
            active_hours,
        } => {
            let _ = normalize_expression(expr)?;
            if let Some(active) = active_hours {
                let _ = ActiveWindow::parse(active)?;
            }
            let _ = ScheduleTimeZone::parse(tz.as_deref())?;
            let _ = next_run_for_schedule(schedule, now)?;
            Ok(())
        }
        Schedule::At { at } => {
            if *at <= now {
                anyhow::bail!("Invalid schedule: 'at' must be in the future");
            }
            Ok(())
        }
        Schedule::Every { every_ms } => {
            if *every_ms == 0 {
                anyhow::bail!("Invalid schedule: every_ms must be > 0");
            }
            Ok(())
        }
    }
}

pub fn schedule_cron_expression(schedule: &Schedule) -> Option<String> {
    match schedule {
        Schedule::Cron { expr, .. } => Some(expr.clone()),
        _ => None,
    }
}

View on GitHub (pinned to 7491200858)

Solutions

  1. Compute the timestamp at submission — now + delay — serialized as UTC RFC3339 with a correct offset
  2. Add margin: require at > now + skew tolerance when latency between compose and submit is possible
  3. If the moment has genuinely passed, pick a new time or switch to a recurring Schedule::Cron / Schedule::Every

Example fix

// before: borderline, elapsed by validation time
Schedule::At { at: Utc::now() }

// after
Schedule::At { at: Utc::now() + ChronoDuration::minutes(5) }
Defensive patterns

Strategy: validation

Validate before calling

const SKEW: chrono::Duration = chrono::Duration::seconds(30);
if let Schedule::At { at } = &schedule {
    ensure!(*at > Utc::now() + SKEW, "'at' is in the past or too close to now");
}

Prevention

When it happens

Trigger: Creating a one-shot job with an already-elapsed timestamp; sending local time labelled as UTC (hours off, landing in the past); a delay between composing and submitting the job that outlives the chosen moment; client/host clock skew.

Common situations: RFC3339 strings built with the wrong offset; re-submitting an old job definition unchanged; paused drafts validated late; test fixtures with hardcoded past dates.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/0f848d2932e9f09e. Report an issue: GitHub.