A scheduler can be perfectly healthy and still run the wrong job at the wrong time.
The failure is usually not the cron expression. It is the boundary between wall-clock time, monotonic elapsed time, leases, retries, and a process that may or restart.
A reliable agent scheduler needs an explicit clock contract. Without one, a clock correction can make a job run twice, never run, or run after its authorization window has expired.
Use wall-clock time for human meaning and durable records:
Use a monotonic clock for elapsed-time decisions inside one process:
Use a database or provider sequence for ordering across processes:
A monotonic timestamp cannot be compared across hosts, and a wall-clock timestamp cannot safely measure a five-minute lease if NTP steps the clock backward. Store both kinds of evidence instead of pretending one timestamp answers every question.
Here is a deliberately boring record shape:
action: send_digest
run_id: 01J...
scheduled_at: 2026-08-19T08:00:00Z
not_before: 2026-08-19T08:00:00Z
expires_at: 2026-08-19T08:05:00Z
lease_owner: worker-7
lease_token: 1842
attempt: 1
state: READY
The important part is not the field names. It is the decision rule:
That last step matters after restarts. A clean restart is not proof that the previous effect did not happen. I covered the effect-side version of this problem in restart-safe agent deduplication.
A clock-skew budget is the maximum uncertainty you will tolerate between the clock used to schedule a run and the clock used to authorize dispatch.
For example:
Do not silently turn the budget into a larger retry window. If the observed offset exceeds the budget, stop dispatching new work or move runs to CLOCK_UNCERTAIN. Existing in-flight work needs its own lease and effect policy.
A simple gate can look like this:
action = now_wall < run.not_before
expired = now_wall >= run.expires_at
clock_uncertain = abs(host_offset) > CLOCK_SKEW_BUDGET
if clock_uncertain:
return CLOCK_UNCERTAIN
if action:
return NOT_READY
if expired:
return EXPIRED
return DISPATCHABLE
The ordering is intentional. A scheduler should not dispatch merely because a job is due if the host's clock is outside the authority's accepted uncertainty.
Test at least these cases:
| Fault | Unsafe symptom | Safer result |
|---|---|---|
| Clock jumps backward | due work appears early or leases live too long | use monotonic lease timers and hold new dispatch |
| Clock jumps forward | future work runs immediately or expires | reject dispatch outside the freshness window |
| NTP becomes unavailable | stale schedule decisions continue silently | enter CLOCK_UNCERTAIN with an alert |
| Worker s during a lease | two workers perform one effect | fencing token rejects the stale worker |
| Scheduler restarts at a boundary | a run is lost or duplicated | reload durable state and reconcile by run ID |
| DST or timezone conversion changes | local-time jobs shift unexpectedly | store UTC plus the original schedule zone |
A useful failure-injection test does not just mock now(). a worker after it claims a lease, advance the authority clock, start a replacement worker, and then let the old worker attempt the effect. The expected result is a rejected stale token, not a second provider call.
Human schedules may be expressed as “every weekday at 09:00 Europe/Berlin.” Convert that schedule to an unambiguous UTC occurrence before creating the run record. Persist the timezone and the resolved occurrence together.
Do not let a browser session, email worker, or MCP tool reinterpret the local schedule. By the time work reaches an effect boundary, it should carry a concrete run ID, expiry, authority version, and idempotency key.
This also makes audits possible. When a user asks why a run happened at 08:00 UTC, you can distinguish:
Before running an always-on agent scheduler, verify:
If you need a managed always-on runtime for an OpenClaw or browser-based agent, managed agent hosting on Ampere is one option to evaluate. Hosting can keep a process running, but it does not define the clock contract, lease semantics, effect idempotency, or recovery policy. Those still belong in the application.
The practical lesson is simple: cron tells you when to try. A clock contract tells you whether the attempt is still authorized, current, and safe.
If this kind of control-plane detail is useful, follow for practical agent reliability patterns rather than model demos.