Skip to main content

Your Scheduled Agent Has Four Clocks, and You Are Trusting the Wrong One

· 12 min read
Tian Pan
Software Engineer

A daily standup summary is scheduled for 09:00 UTC. The cron fires on time. A worker pod spins up two seconds later. The LLM call takes another forty seconds round-trip. The model writes its summary believing it is February of last year, because that is the last thing its training data confidently knew. The tool layer dispatches the Slack message against the wall clock at 09:00:42 UTC, on a date the model never mentions because nobody asked it to. The message lands in the right channel, with yesterday's standup notes summarized as "today's," and nobody notices for three weeks.

This is not a bug in any one component. It is a contract that nobody wrote between four different clocks that all believe they know what "now" is.

The cron has its own moment — the firing time, the only thing in the system that is definitionally on schedule. The worker has a wall clock that may be a few hundred milliseconds off from the scheduler, more if the pod is cold-starting. The model has a vibes-based "now" derived from whatever timestamps appeared in its context plus a vague memory of its training cutoff. The tool API has its own wall clock, which may be a different machine entirely, and which is the moment the side effect actually lands. In a healthy system, these four moments collapse to one because the gaps are small and the actions are coarse. In an unhealthy system — which is to say, most production agent systems — the gaps are the action, and the team has shipped a scheduler whose semantics depend on whichever clock won the race that turn.

Four Clocks, One Slack Message

Walk the cron-to-agent-to-tool chain slowly and the disagreements become visible.

The scheduler's clock is the intended firing moment. If your cron says 0 9 * * *, the contract is "fire at 09:00." Most schedulers honor this within a second under normal load. Under queue backpressure or a degraded control plane, the firing can slip — a job set for 09:00 may not actually invoke a worker until 09:04. The slip is rarely reported back to the agent.

The worker's clock is the wall clock on the machine that runs the agent loop. NTP keeps this within a few milliseconds of UTC under normal conditions, but containers without explicit TZ=UTC can drift into local time interpretations, and cold-started pods can have skew of a hundred milliseconds or more in the first second. Distributed-systems literature has documented for decades that clock skew across nodes is a constant background problem; agent systems inherit every word of it.

The model's mental "now" is the most slippery clock of the four. The model does not have access to the wall clock. It reasons about time based on whatever you put in its prompt plus its training cutoff. As of mid-2026, cutoff dates are scattered across the last twelve to eighteen months — different providers, different models, different training runs all anchor to different "knowledge horizons." When the prompt is silent about the date, the model fills in with whatever felt most representative during training. Sometimes that is the cutoff month; sometimes it is months earlier; sometimes it is wildly wrong in ways that only surface for relative-time reasoning ("send this in two hours").

The tool API's clock is the wall clock at the moment the side effect actually fires. This is usually a different machine from the worker — your worker calls Slack's API, Slack stamps the message with Slack's clock. If your tool layer carries no explicit time payload, "when this happened" is a fact that exists nowhere except in the receiving system's logs.

The team that did not name these four clocks as separate has a system whose timing semantics emerge from whichever component happens to be authoritative at any given step. That is not a contract. That is an accident waiting to compound.

The Failure Modes That Don't Look Like Time Bugs

The reason this class of bug stays invisible is that its symptoms rarely look like time bugs. They look like content bugs, retry bugs, or "the model hallucinated a date" bugs. A few worth recognizing:

The model summarizes yesterday and calls it today. The cron fires for the daily standup. The prompt does not specify the date. The model writes "Here's what the team is working on today," then enumerates work items that look plausible because they always look plausible, anchored against whatever date the model implicitly believed. The summary is posted, looks fine on a quick read, and quietly references stale tickets. The eval suite has no synthetic case where the model's mental date and the actual date diverge by months, so the eval never catches it.

The "reminder for tomorrow" fires never, or fires in the past. The model is asked to schedule a follow-up. It computes "tomorrow" against its internal date — call it 2025-08-15 — and emits 2025-08-16T09:00:00Z as the schedule time. The scheduler dutifully tries to fire a job in the past, which either errors silently or fires immediately as a backfill. The user does not get a reminder tomorrow; they get one now, or never. The model behaved consistently with its own beliefs; the system gave those beliefs operational weight without checking them.

The retry crosses a day boundary and lands on the wrong day. The cron fires at 23:59:30. The agent's first call to the model fails on a transient provider error. The retry policy backs off and fires the second call at 00:00:15 the next day. The tool now dispatches an action — "post the day's summary" — and the action lands on the next day's channel, summarizing yesterday's data. The retry was idempotent in the sense the literature means it: same key, same call. It was not idempotent against the intended firing time, because no layer was carrying that time as ground truth.

The DST shift duplicates or drops a run. On the spring-forward day, a job scheduled for 2:30 local does not exist; it is silently skipped. On the fall-back day, the same job exists twice and runs twice, depending on the scheduler's DST policy. There is documented industry experience of distributed schedulers like Kubernetes CronJobs firing thousands of duplicate invocations across a DST boundary, with five-figure cloud-cost incidents. Agents inherit this on top of every other clock problem they already have.

The "9 AM" the cron means is not the "9 AM" the prompt means. The cron expression is 0 9 * * * in America/New_York. The system prompt says "the daily 9 AM standup." The model, with no timezone context, reasons about "9 AM" as the user's local time and emits a message that says "good morning" to readers in San Francisco at 6 AM. Two correct timezone interpretations are in tension, and the team has shipped both without picking one.

Time Is State. Plumb It Like State.

The patterns that close these gaps all share a single move: stop treating time as ambient context and start treating it as plumbed state, with the cron's intent as the canonical source.

Loading…
References:Let's stay in touch and Follow me for more thoughts and updates