{"record":{"id":"0f848d2932e9f09e","repo":"tinyhumansai/openhuman","slug":"invalid-schedule-at-must-be-in-the-future","errorCode":null,"errorMessage":"Invalid schedule: 'at' must be in the future","messagePattern":"Invalid schedule: 'at' must be in the future","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/cron/schedule.rs","lineNumber":77,"sourceCode":"\npub fn validate_schedule(schedule: &Schedule, now: DateTime<Utc>) -> Result<()> {\n    match schedule {\n        Schedule::Cron {\n            expr,\n            tz,\n            active_hours,\n        } => {\n            let _ = normalize_expression(expr)?;\n            if let Some(active) = active_hours {\n                let _ = ActiveWindow::parse(active)?;\n            }\n            let _ = ScheduleTimeZone::parse(tz.as_deref())?;\n            let _ = next_run_for_schedule(schedule, now)?;\n            Ok(())\n        }\n        Schedule::At { at } => {\n            if *at <= now {\n                anyhow::bail!(\"Invalid schedule: 'at' must be in the future\");\n            }\n            Ok(())\n        }\n        Schedule::Every { every_ms } => {\n            if *every_ms == 0 {\n                anyhow::bail!(\"Invalid schedule: every_ms must be > 0\");\n            }\n            Ok(())\n        }\n    }\n}\n\npub fn schedule_cron_expression(schedule: &Schedule) -> Option<String> {\n    match schedule {\n        Schedule::Cron { expr, .. } => Some(expr.clone()),\n        _ => None,\n    }\n}","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/cron/schedule.rs#L59-L95","documentation":"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.","triggerScenarios":"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.","commonSituations":"RFC3339 strings built with the wrong offset; re-submitting an old job definition unchanged; paused drafts validated late; test fixtures with hardcoded past dates.","solutions":["Compute the timestamp at submission — now + delay — serialized as UTC RFC3339 with a correct offset","Add margin: require at > now + skew tolerance when latency between compose and submit is possible","If the moment has genuinely passed, pick a new time or switch to a recurring Schedule::Cron / Schedule::Every"],"exampleFix":"// before: borderline, elapsed by validation time\nSchedule::At { at: Utc::now() }\n\n// after\nSchedule::At { at: Utc::now() + ChronoDuration::minutes(5) }","handlingStrategy":"validation","validationCode":"const SKEW: chrono::Duration = chrono::Duration::seconds(30);\nif let Schedule::At { at } = &schedule {\n    ensure!(*at > Utc::now() + SKEW, \"'at' is in the past or too close to now\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Generate 'at' server-side (now + user delay) rather than trusting client clocks","Serialize timestamps with explicit UTC offsets","Re-validate one-shots at submission time, not at draft-creation time"],"tags":["cron","schedule","one-shot","time"],"backgroundTag":"scheduled-time-in-past","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}