When a Scheduled Agent Misses Its Window, Make the Policy Explicit
A schedule can identify when work should happen. The harder question is what to do when the worker arrives late: catch up, skip, or stop for review. Treating lateness as an explicit policy keeps an agent system predictable instead of letting delay quietly rewrite its commitments.
This article is a companion to Scheduling Agents with a Cadence Contract, Not a Bigger Cron Table. It focuses on the decision that follows slot eligibility: how a DAL workflow should handle missed windows without duplicating work, drifting its cadence, or creating an unreviewed external effect.
A missed slot is not automatically unfinished work
A timer-based system often collapses several states into one. If a process was supposed to run at 09:00 and wakes at 11:00, it may simply execute immediately. That looks helpful, but it hides an important distinction: was the 09:00 commitment still valuable, or has its context expired?
Consider three recurring jobs:
- A draft generator may still be useful two hours late. The output can be labeled late and verified normally.
- A price-monitoring pass may be obsolete if its data window has closed. Running it now could create a misleading snapshot.
- A message intended to announce a live event may be harmful after the event begins. Catching up is worse than skipping.
The scheduler knows the slot and the clock. The job contract must say whether the work remains valid. “Due” should not mean “always execute.” It should mean “evaluate the slot under its lateness policy.”
Give each slot a lateness policy
A useful policy can be small and named. DAL jobs do not need a sprawling scheduling framework to make this decision visible. They need a declared outcome for a late occurrence.
Catch up once
Use this when the work remains useful and can be safely repeated by slot identity. The worker claims the original slot, runs once, and records the actual start time as late. It does not advance the next boundary from the delayed start.
A daily article draft is a good example. If the worker starts at 11:00 instead of 09:00, producing one draft for that calendar day is sensible. Producing two drafts because the process interpreted delay as a new interval is not.
Skip and record
Use this when the slot’s value expires but the fact of the miss matters. The scheduler records skipped_expired with the slot key, expiry time, and reason. This is a successful scheduling decision, not a silent failure.
Recording the skip prevents a later restart from repeatedly rediscovering the same old slot. It also gives operators an honest history: the job did not run, and the system knows why.
Stop for review
Use this when the consequences are ambiguous or external. A late payment instruction, publication, customer notification, or deployment should not be replayed merely because a timer fired. The scheduler can mark the slot review_required, preserve the evidence it has, and wait for an authorized decision.
This is where a review gate is stronger than a retry loop. Retries are good at recovering known-safe internal work. They are not permission to repeat an effect whose status is uncertain.
Separate lateness from failure
A late run is not necessarily a failed run. Conversely, a process that starts on time can still fail to produce its promised result. Keep those dimensions separate in the receipt.
A compact record might contain:
``text slot: article_daily:2026-10-04 scheduled_at: 2026-10-04T09:00Z started_at: 2026-10-04T11:04Z lateness: 124 minutes policy: catch_up_once outcome: completed artifact: COO-FILESYSTEM/articles/drafts/... verified: true ``
The lateness field explains timing. The outcome and verified fields explain completion. Combining them into one status such as success loses information that matters during an incident or audit.
For a skipped slot, the same record can preserve outcome: skipped_expired and a reason such as source_window_closed. For review, it can identify the missing fact: external_effect_status_unknown. These are actionable states, not decorative telemetry.
Do not let catch-up create cadence drift
One subtle failure mode is schedule drift. An interval runner that executes late and then calculates the next run from the actual start time gradually moves the entire workflow away from its intended boundaries. The system appears healthy while its meaning changes.
Slot-based execution avoids that problem. Boundaries come from the declared cadence, not from worker arrival. If the daily boundaries are 09:00 UTC, a late 11:00 execution still belongs to the 09:00 slot; the next slot remains 09:00 on the following day.
For multiple daily slots, the policy should also say whether a worker may catch up more than one missed occurrence. A conservative default is one claim per slot and a bounded catch-up count. If three windows were missed during an outage, blindly replaying all three may flood downstream systems or produce artifacts that no longer make sense. Surface the backlog, then apply the job’s declared rule.
Keep internal recovery separate from external effects
The cleanest design has two phases. First, the scheduler may recover safe preparation work: read the contract, claim a slot, generate a draft, and verify a file. Second, an external transition requires its own gate.
For example, a missed content slot can catch up by writing a draft under articles/drafts/. It should not infer that the draft is approved for publication. A delayed notification task may reconstruct the intended message, but it should not send until its recipient, timing, and approval conditions are still valid.
This separation also improves debugging. If a slot is blocked, the record can say whether the blockage is scheduling-related, evidence-related, or authorization-related. Operators do not have to guess whether “retry” is safe.
A practical decision loop
A DAL runner can express the late-slot decision plainly:
```text slot = cadence.current_slot(job, now)
if receipt.exists(slot): return already_resolved
if now > slot.expires_at: if policy == skip_expired: return record_skip(slot) if policy == review_late: return surface_review(slot)
if policy == catch_up_once: claim(slot) result = run_internal_phase(slot) verify(result) return write_receipt(slot, result) ```
The exact syntax can vary, but the order matters. Resolve existing evidence first. Evaluate expiry before running. Claim by stable slot identity. Verify the result. Keep external actions behind their own authorization boundary.
Conclusion: lateness should be boring
Reliable scheduling is not the promise that every timer fires perfectly. It is the promise that imperfect timing produces an intelligible decision. A late slot should be caught up once, skipped with a receipt, or stopped for review—never handled by an accidental retry policy.
The next step is practical: choose one recurring DAL job, name its late-window behavior, and test a delayed worker, a restart with no receipt, and an uncertain external effect. Preserve the original slot boundary in every case. When the policy is explicit, a missed window becomes an ordinary state transition instead of a new source of operational mystery.