October 28, 2026 · 7 min read
Scheduled AI agent jobs: double fires, missed fires, stale settings
By Anna, co-founder, build and content
Scheduled AI agent jobs fail in three ways: they fire twice, they do not fire at all, or they fire on settings that are not live yet. Double fires are loud once you look. The other two are silent, and a silent job reads as a clean one. Each needs a different guard.
We run scheduled agents every day, and we have hit all three on our own systems. This post covers what the vendors document, what went wrong for us with dates, and what to check. A tomte, our word for one production agent with one defined job, is only as reliable as the schedule that wakes it.
Why does a scheduler fire a job twice?
Most schedulers promise at least one run, not exactly one. Kubernetes states it in its CronJob documentation (read October 2026): a CronJob creates a Job “approximately once” per scheduled time, and there are circumstances where two Jobs might be created or none. The docs conclude that Jobs should be idempotent.
That is the vendor telling you the duplicate is your problem. Add people to the mix: someone clicks “run now” while the schedule is about to fire, or a retry kicks in after a slow response.
Our own case is dated. On 2026-08-28 our blog pipeline fired three times across its two daily slots, and none of the fires landed at the configured hour. Nothing was broken in the model. The trigger layer simply delivered more events than we had asked for, at odd times.
The fix was a guard that runs before any work: check whether this slot already shipped today, and if it did, write a “duplicate fire” line and stop. No research, no draft, no publish. We cover the lease and key patterns in depth in idempotency for AI agents, so this post does not repeat them.
Why does a scheduled job miss its run?
A job misses its run when the thing that triggers it is not there at the moment it should fire, and the scheduler has no rule to catch up. The causes are mundane: a sleeping machine, a platform under load, a deadline that expired, a workflow that was switched off.
The documented versions are worth knowing by heart.
- Apple’s launchd documentation (archived guide, read October 2026) says a calendar-interval job runs on wake if the Mac was asleep at the scheduled time. If the machine was off, the job waits for the next designated time. Other launchd jobs are skipped while the computer is off or asleep.
- GitHub Docs on the schedule event (read October 2026) say scheduled workflows can be delayed during high load, especially at the start of each hour, and that some queued jobs may be dropped if load is high enough. Scheduled workflows run only from the default branch, and a public repository’s scheduled workflows are disabled automatically after 60 days with no repository activity.
- Kubernetes (read October 2026) skips a run if it misses its
startingDeadlineSeconds. With no deadline set, the controller counts missed schedules since the last run, and if it finds more than 100 it refuses to start the Job and logs an error. The docs give the example of a controller down for under two hours on a once-a-minute schedule. A short outage can kill a job that nobody is watching.
What does a missed fire look like from the inside?
A missed fire leaves no trace, which is the whole problem. There is no failed run, no error and no red light. The log simply has a gap, and nobody reads a gap.
Our version: on 2026-10-05 we loaded a nightly job on a Mac by pointing the launch system at a plist file that sat inside a project folder. The status command showed it loaded that day, and we reported it as live. By 2026-10-08 it was gone. The system said it could not find the service, no log had ever been written, and the job had run zero times.
The cause was where the file lived. A launch agent that is not installed in the user’s LaunchAgents folder is not loaded at login, so it survives only until the next logout or reboot. The same-day status check proved nothing about day three.
The fix had two parts. Install the plist in the proper folder, lint it, and reload from there. Then add a check a few days out that reads the log file’s modified time, not the job’s status. A status field tells you what the system believes. A log mtime tells you what happened. For more on this habit, see how to know an automation is still working after day one.
When is a setting not live yet?
A setting is not live until the thing that reads it has restarted, rebuilt or redeployed. Changing a value and seeing it saved is not the same as the running job using it. The gap between the two is a missed or wrong fire that looks like a successful change.
Netlify’s documentation (read October 2026) is explicit: after you create environment variables, “build and deploy your site for the additions to take effect,” and changes to existing ones need a build and deploy as well. Scheduled functions there run only for published deploys, so a branch or preview deploy does not trigger them.
We learned this on 2026-09-23. We switched on a batch of feature flags in production through the command line. A scheduled function fired minutes later with the old environment. By our own flag convention, an unset flag means “do nothing”, so the run was a silent no-op and wrote no record. The next scheduled function fired after a deploy had landed and ran clean. Two jobs, same change, opposite outcomes, separated by a few minutes and a build.
The rule we kept: after flipping a setting that a scheduled job reads, either trigger a deploy or expect the first slot after the flip to do nothing. Then verify with the run records, not with the settings page.
Which failure mode needs which guard?
Each failure needs its own guard, and none of them is the model.
| Failure mode | What you see | Guard |
|---|---|---|
| Double fire | Two outputs for one slot, or one at the wrong hour | Check “did this slot already run?” before any work; stop on yes |
| Missed fire | A gap in the log, no error | Freshness check on the newest output or log mtime, days later |
| Setting not live | Job runs, does nothing or the old thing | Redeploy or restart after a change; verify from run records |
| Catch-up flood | A burst of runs after downtime, or none after a long outage | Set a start deadline; cap catch-up; alert on skipped slots |
How do you check a schedule is real?
Check it from the outside, on a clock the job does not control. Five checks cover most of it.
- Does a run record exist for every expected slot? Count slots expected against slots recorded. A gap is a missed fire.
- Is there a dedupe check before the first side effect? Without one, a double fire becomes a double publish.
- What happens at a missed slot: catch up, skip or alert? Pick one on purpose. Vendors differ, as above.
- After the last settings change, did a run happen on the new settings? Find the first run after the change and read what it did.
- Where does the job live? A job on one laptop, one branch or one preview deploy is a job with a single point of failure.
Run each check once on the day you write it, then again three days later. A check that cannot run reports nothing, and nothing looks like success.
What we do not claim
We have not solved missed fires. We detect them after the fact and fix them by hand. A freshness check tells us a slot was missed, not why, and the platform behaviours above can change; the dates in this post are when we read each page. A gap we have not looked for is still a gap.
Where this fits
Every agent in an AI Workforce Sprint ships with its schedule documented: where it runs, what the dedupe check is, what a missed slot triggers, and how to confirm a settings change is live. You own the delivered code, data and documentation. Our agent fleet proof page shows what we run ourselves, with dates.
Start async
Send us the list of jobs that run on a timer and what each one does when it fires. We reply in writing within one business day with which ones are exposed to a double fire, a missed fire or a stale setting. No meetings. Start async.