Workflow · last verified 2026-10-19

Scheduled AI agents: check the artifact, not the green run

What this workflow is

A scheduled agent that ran is not a scheduled agent that worked, so once a week check what each one was supposed to leave behind, not the status it reported. The vendor status tells you a session started and exited. Whether the task in the prompt succeeded, whether the output reached anyone, and whether the job still exists are three separate questions, and none of them is answered by a green dot.

The practice: register every job with its expected artifact, a maximum age and the step that consumes its output; read each job's state by its own id rather than from a listing; gate on the artifact instead of on which job claims to produce it; and count output that nobody processed as a failed run. Six dated failures from running our own scheduled agents sit below, each next to the check that would have caught it. It is part of the Engineering Workflow Library and is vendor-neutral: the vendor behaviour it leans on is cited and dated, and the failures are ours.

The job this workflow does

The decision it improves is a weekly one: did each of my scheduled agents do its job this week, and what do I do when the answer is no? Setting one up takes minutes and the vendors document it well. Operating a handful of them for months is a different discipline, and it is the one that fails quietly. A scheduled agent runs unattended by design, so nobody is watching when it stops, and the usual signal that something is wrong, an error in front of you, never arrives. What arrives instead is silence, and silence reads exactly like a quiet week.

"Scheduled agent" here means any prompt plus permissions that a scheduler starts without you: a cloud routine, a task in a desktop app, an operating-system scheduler launching a CLI session. The checks below do not care which. They care about what each job leaves on disk or in a repository, how old it is, and whether the next step in the chain picked it up.

What the vendors say, as of 2026-10-19

Anthropic describes Claude Code routines as being "in research preview" with behaviour and limits that "may change", so treat every row as dated. All three pages were fetched on 2026-10-19 and returned HTTP 200.

ConditionWhat the vendor documentsSource
A green run"A green status in the run list means the session started and exited without an infrastructure error. It does not mean the task in your prompt succeeded." The failures surface in the transcript, not the indicator.Claude Code routines
GitHub connection lapsesWith no valid connection the routine skips runs for up to 72 hours, then switches itself off and stays off until someone reconnects GitHub and re-enables it.Routines
Subscription pausedThe routines are suspended while the subscription is paused, and they stay idle after it resumes until someone re-enables them.Routines
Organization switchOn Team and Enterprise plans an Owner can switch routines off for the whole organization, and the ones already defined stop firing.Routines
GitHub event burstsWebhook events are subject to hourly caps: "Events beyond the limit are dropped until the window resets."Routines
Local desktop task, computer asleepSleep costs you runs: nothing fires while the app is closed or the computer sleeps, and missed slots are not replayed one by one. Desktop "starts exactly one catch-up run for the most recently missed time and discards anything older."Desktop scheduled tasks
OpenAI scheduled tasks on a local projectOpenAI documents "scheduled tasks" in the ChatGPT desktop app. A project-scoped task needs the machine powered on, the app running and the project still on disk. Nobody is at the keyboard when it fires, and it uses whatever sandbox mode you have set as the default, so tool calls fail when that mode is too tight for the work.OpenAI scheduled tasks

Two things follow before any of our own evidence. First, several of these end with a job that is simply off, and the documented remedy is a person turning it back on, which means somebody has to notice. Second, both vendors point you at reading what the run did: Anthropic's page shows /schedule why did my nightly review do nothing this morning? as a way to have Claude read a routine's recent runs and logs (it requires Claude Code v2.1.227 or later), and OpenAI's page says to test the prompt in a regular chat and to "review the first few runs". That is the right instinct, but it is a human reading one run at a time. It does not tell you that a job has been absent for three weeks.

Six failures from running our own

All six come from this platform's coordination of its weekly jobs. Each is dated and traceable to the decision record, the ledgers, the pull-request metadata or the commit history. What most of them share is that the producer ran fine and something around it did not.

1. The job that was specified, gated on, and apparently never created

A Monday gate in this repo asserted that a pull request from a job called monday-predrafts existed within the last seven days. It failed on that check on every run since the gate shipped. On 2026-07-26 the job list was read directly and did not contain the routine, and no id for it has ever surfaced, so it appears never to have been created. (A listing is weaker evidence than it looks, as case 4 shows.) It had been ratified and written into a setup spec in the 2026-07-16 org redesign, and nothing ever built it. Meanwhile both of its supposed outputs were landing on time, the currency sweep that day and the next Brief draft, produced by other means. The gate was red while the work was green, for its whole life.

There was a second error inside the first. Two earlier dead-job investigations had assumed that whether a cloud routine exists could only be checked in a web dashboard, and had inferred existence from the absence of its artifact. That belief was false: the session tool can list, get, create and run them. The replacement is to read the job's state and gate its artifact as two separate checks, never one standing in for the other. The record's own line is the useful one: a gate that names a job "cannot tell a dead job from a job that never existed". The fix deleted the identity check and asserted the two artifacts at ten days each, whoever produced them.

2. Two jobs on one artifact

Two weekly currency sweeps covered the same sources: a cloud routine, and a second sweep bound to the desktop app. The decision record logs two collisions at the same report path, on 2026-08-17 and 2026-09-21. Only the cloud sweep found the renumbering in the OWASP LLM Top 10 2026. The desktop-bound twin needed the owner's computer awake on Monday, which is the same condition the Desktop scheduled-tasks page documents for any local task. On 2026-09-25 the decision was to keep the cloud routine, disable the twin and leave its definition on disk. Two producers for one artifact meant two reports to reconcile every week and duplicate spend, and the weaker detector had been the one whose delivery channel happened to work.

3. Output nobody was assigned to read

The better detector delivered its report as a pull request, and nothing in the Monday sequence merged it. PR #22 was opened 2026-07-13 and PR #67 on 2026-08-17; both were closed unmerged on 2026-09-25, after 74 and 39 days (the decision record says 70 days for #22). The OWASP finding sat inside #67. The record's rationale for the next change is the sentence this whole workflow rests on: the better detector "was being wasted by its delivery channel, not by its detection". Monday step two now merges the week's PR before ingesting tickets.

That fix failed its first Monday. PR #85, opened 2026-09-28, sat open five days and was merged on 2026-10-03. The response was a check: the weekly gate now fails when a currency-watch PR is open more than three days.

4. The listing that hid the jobs

When asked on 2026-10-05 whether the scheduled jobs had been checked, the honest answer was no. The huddle workflow had no job step, and the session tool that lists routines was capped at 20 results and ignored its cursor. It returned 17 one-shot pull-request reminders and hid four routines. Three weeks earlier, on 2026-09-14, a currency-sweep investigation had hit the same wall: the list "still loops on the same 20 disabled one-shots". The fix is to hold a table of job ids and read each by its own id. A listing is a search, not an inventory, and an agent that cannot call the tool cannot see the job at all.

5. A gate that only runs when the sequence runs

After the three-day check shipped, a currency-watch PR still sat unmerged for a week. PR #97 was opened 2026-10-12 at 06:38 UTC and merged 2026-10-19 at 06:57 UTC, just over seven days later, alongside PR #102 (opened 2026-10-19). The artifacts say the Monday sequence did not run on 2026-10-12: there is no huddle report for that date, and the next report records that nothing was committed for twelve days. The sweep's headline finding, the Claude Haiku 5.5 release, was absent from the repo for the week. The three-day check was correct, but it is a step inside the sequence it was meant to protect, and nothing suggests it ran that week. (Inferring a missed run from missing artifacts is the inference step 2 warns about; the seven-day PR is the part that is certain.)

The lesson is not "add another gate". A check that someone has to remember to run is a scheduled job of its own, with the same failure modes, and it needs a registry row: what runs it, how old its last result is, and who notices when that result is missing.

6. The reporter that flagged finished work as unfiled

The fix for case 4 is a jobs reporter in the weekly huddle: a registry of jobs, each with an output location, a maximum age and a rule for what "processed" means, checked once a week. It was specified on 2026-10-05 and its first table is in the 2026-10-19 huddle. That table reported five Track G regulatory-watch findings as unfiled. They were not. Four had shipped on 2026-10-03 as tickets Q-717 to Q-720 in commit e4e2b7f, and the fifth was already folded into an open ticket. The ledger pass that followed recorded the cause as the reporter matching on output path. The closed rows do cite the report file followed by a row number, so I have not isolated exactly how the match failed. What is established is that the checker's first red was false.

That is the case for fixturing a new reporter against one job you know is processed and one you know is not, before believing its table. It is the same discipline as validating the validator, applied to a checker whose subject is other jobs.

Two related cases are told elsewhere and not repeated here. A daily cron that reported outcome: ok while silently discarding changes is the subject of consume then commit, and two hooks that never fired once from a well-formed settings file are in the hooks guide. Both are the same family: a control that is present, correctly shaped and doing nothing.

The workflow

1. Register each job before you trust it

One row per job, in a file the weekly check reads. The row is five fields, and the last two are the ones people skip:

FieldWhat it holdsExample from this repo's currency sweep
KindWhere it runs. This decides what can silently stop it.A cloud routine
Cadence and max ageWhen it should fire, and how old its newest output may be before it is overdue: one cadence plus a day's grace.Weekly, 8 days
Expected artifactA path or branch pattern you can look for, not "the job's status".A PR from a currency-watch/<date> branch; after merge, a dated file in docs/compliance-watch/
Delivery channelHow the artifact reaches the tree or the reader, and who completes that step.The Monday step that merges the PR
Processing ruleWhat turns the artifact into done.An open PR means the findings are not ingested; every ## Tickets row needs a matching ledger row

This repo's registry is a constant in .claude/workflows/huddle.js. It holds five jobs across three places a job can live: a cloud routine, desktop-app schedulers and the operating system's own scheduler. It did not exist before 2026-10-05, which is the gap cases 3 and 4 lived in.

2. Read each job's state by its own id

Keep a table of job ids next to the registry (never in anything public) and fetch each job's detail: enabled or not, last fired, last result, next run. Treat a listing as a way to discover candidates, never as the inventory, and do not infer a job from its artifact in either direction. For a job that lives on your machine, the state is the machine's: a sleeping laptop is a documented reason for a skipped run, and the scheduler's own history is where the skip is recorded.

3. Gate the artifact, not the producer

For each registry row assert that the artifact exists and is younger than its max age, whoever or whatever made it. An absent or header-only artifact is a failure with its own message, not a pass: see fail empty. This is a deterministic gate and it belongs in the check you already run before the weekly work. Skipping it means skipping the whole weekly sequence, which still happens (case 5).

The trade-off is real and worth stating. An artifact gate cannot tell a job that ran from a person who did its work by hand. In this repo the sweep has in practice been carried by interactive sessions on some weeks, and the gate tolerates that by design. If you need to know the job itself fired, that is step 2's job, not step 3's.

4. Count unprocessed output as a failed run

A job whose findings never reach the ledger has the same effect as a job that did not run. Define "processed" per row and report it as a state, not a boolean: yes, partly, no, not applicable. Then find the step that completes delivery and give it an owner and a clock. Cases 3 and 5 are the same defect twice: the producer worked, the delivery step was a human memory. When the delivery channel is a pull request, an open one past its due day is a failed step, so the weekly check should fail on it rather than mention it.

5. One producer per artifact

Before you add a job, search the registry for its artifact path. If another job already writes it, pick one by detection quality and delivery reliability, in that order of evidence, and disable the other without deleting its definition, as in case 2. Two sweeps do not double your coverage; they double the reconciliation and make a collision at the output path routine.

6. Make "nothing found" a written result, and read a sample

Some jobs are designed to produce nothing on a quiet week: the currency routine opens no pull request when there is no drift, so the absence of a PR can never be evidence of anything. Where the job allows it, have it write a short dated "nothing found, checked these sources" artifact, so a quiet week and a dead job stop looking alike. Then read the transcript of a sample run each cycle. The vendors both tell you to open the run; the point of the gate is that you only have to do that for one job in a few weeks, not for all of them every week.

7. Fixture the checker before you act on it

The reporter in step 4 is itself a job with an output. Run it once against a job you know is processed and one you know is not, and read its table against what you know before it files a single ticket. Case 6 is what the first real run looks like without that.

Failure modes

  • Reading the green as success. The vendor's own wording is that it means the session started and exited. Everything else needs the artifact.
  • Gating on the job's identity. A check that names a job cannot tell a dead job from one that never existed, and cannot see work that arrived another way (case 1).
  • A disabled job that nobody knows is disabled. A lapsed connection, a paused subscription or an organization switch each end with a job that is off and a human who must turn it on.
  • A quiet week and a dead job look identical. If silence is a valid output, silence must be written down as one.
  • Two producers, one path. Collisions, duplicate spend and a weaker detector that happens to deliver (case 2).
  • Delivery by memory. A pull request, a draft or a report that waits on a person remembering is a failed run in waiting (cases 3 and 5).
  • The gate that is itself unscheduled. A check run only inside the sequence it protects fails when the sequence does.
  • A listing mistaken for an inventory. A cap, a filter or a cursor you do not follow hides jobs without an error (case 4).
  • Trusting the reporter's first output. A new checker can be wrong in either direction on its first run (case 6).
  • A local job on a machine that sleeps. The vendor documents the skip and a single catch-up. A weekly job that missed three cycles runs once, not three times.

Verification

The weekly table is the observable: one row per registry job with its liveness read by id, the newest artifact and its age, and its processed state. You know the workflow works when a job you stop on purpose appears in the next table as overdue within one cycle, and when no delivery pull request is older than its due day. Test that with a throwaway job before you rely on it for a real one.

As of today this repo has one data point, and it is mixed. The first table, in the 2026-10-19 huddle, correctly showed two unmerged currency-watch PRs with the older one at seven days, and correctly noted that the Brief drafts had not been through their gates. It also reported five findings as unfiled that had shipped. A single run is not a track record, and the verification this page can honestly claim is that the table exists, reads real state, and has been wrong once in a way we caught.

When not to use this workflow

Skip the registry for a single job whose output you read the day it lands. The registry would be longer than the job, and your own attention is already the check. Skip it for short interactive loops you are watching, such as a follow-up poll while a deploy finishes: the failure shows up in front of you.

Do not build a bespoke reporter if you already run monitoring with heartbeats for your scheduled work. Use that for liveness and keep only the artifact and processed columns from this workflow, because those are the two your monitoring will not have. And skip step 6's written "nothing found" for any job where a quiet result is already loud, for instance because it posts into the channel you read every morning.

The cost is real: a file to maintain and a weekly read. It only pays when the output feeds something else, a ledger, a release, a reader, and when more than one of those jobs exists.

Changelog

  • 2026-10-19 — Initial version. The vendor rows were read from the three cited pages on 2026-10-19. The six cases are verified against the decision record (2026-07-26, 2026-09-25, 2026-10-05), the pull-request metadata for #22, #67, #85, #97 and #102, the merge commits of 2026-10-19, the 2026-10-19 huddle report, and the weekly gate script.
Sources & provenance
  • Claude Code routines: code.claude.com/docs/en/routines, fetched 2026-10-19 (HTTP 200). Research-preview notice, the green-status wording, the 72-hour GitHub disconnect, the paused-subscription hold, the organization toggle, dropped webhook events and the /schedule run-history query (Claude Code v2.1.227 or later). The doc is in research preview and may change.
  • Claude Code Desktop scheduled tasks: code.claude.com/docs/en/desktop-scheduled-tasks, fetched 2026-10-19 (HTTP 200). App-running and computer-awake requirement, skipped runs, and the one-catch-up rule over the last seven days.
  • OpenAI scheduled tasks: learn.chatgpt.com/docs/automations, fetched 2026-10-19 (HTTP 200). The page is titled "Scheduled tasks" and covers ChatGPT on the web, mobile and the desktop app; the table row uses only the desktop-app, project-scoped part: the project-on-disk and app-running requirement, unattended default-sandbox behaviour and the advice to review the first few runs. No other Codex-specific claim is made on this page.
  • Cases 1 to 3: this repo's decision record, entries dated 2026-07-26 (monday-predrafts dissolved) and 2026-09-25 (one currency watcher). PR dates for #22, #67 and #85 are from the GitHub pull-request records; the 74-day figure is computed from the opening and closing dates of #22.
  • Case 3's follow-up check: staleCurrencyWatchPrs in scripts/lint-monday-artifacts.mjs and ticket Q-703 (2026-10-03).
  • Case 4: the 2026-10-05 decision record and ticket Q-642 (2026-09-14). The list-cap behaviour is our observation of one session tool, not a documented vendor limit.
  • Case 5: PR #97 and #102 records and the merge commits of 2026-10-19; the 2026-10-19 huddle report. Case 6: the same report's scheduled-jobs table and its ledger pass, the closed tickets Q-717 to Q-720, and commit e4e2b7f.
  • The registry fields: the job registry in .claude/workflows/huddle.js.
  • Practice claims ("this repo's own...") come from operating the aiArch repo, not from a vendor source. Vendor behaviour changes; re-read the cited pages before relying on a row.

The incidents are fixed to the dates given; the files they live in are under active change. Re-read the source before relying on any snippet. Corrections: hello@aiarch.dev.

Learn to run agents you are not watching.

aiArch teaches evaluation, gating and production agent operations by building them, on a platform that publishes the weeks its own scheduled jobs failed.

Free with an account: Track 0 (The Mindset Shift) and the Track 0 coach allowance, no card · every claim cited · full curriculum with membership