The Loops Doctrine
Before this, read:
- Crons as a heartbeat — the fixed-cadence primitive you’ll be comparing against
“I’ll check back on that” is a promise a chat reply can’t keep. Sessions end, context rotates, attention moves. Every future obligation that lives only in a conversation will eventually be dropped. The loops doctrine is the rule that forces the right primitive in the same moment you create the obligation.
The core rule
Section titled “The core rule”Whenever you create a future obligation — “watch for X,” “check back on Y,” “Z expires on a date,” “waiting on a reply,” “verify after deploy” — you wire it to the right loop primitive before moving on. A promise in a reply is a dropped promise.
Four primitives, four different shapes:
| Shape | Primitive | When |
|---|---|---|
| Fixed cadence forever | cron | ”Run every hour/day/week” |
| Condition that fires once | watcher | ”When this email lands,” “when this file appears,” “when this URL changes” |
| In-flight work in this session | session loop | CI run, long deployment, “keep going until this job is done” |
| Date-based, one-shot | durable one-shot | ”Remind me on Jun 13,” “run this once at 3pm” |
Cron: fixed cadence forever
Section titled “Cron: fixed cadence forever”Use cron when the cadence is the point — “every hour at :03” regardless of whether anything has changed. Domain heartbeats, daily briefings, weekly reviews, database backups.
Crons blast on schedule whether or not anything changed. This makes them the wrong choice for “check if X changed and notify me” — that pattern should be a watcher with change-gating.
The 4-hour open-loops check is a real example of this mistake in the system: the same open-loop list was being blasted 6 times a day, unchanged. No content-hash. No “only notify if the list changed.” JD named this his #1 noise complaint. The fix: move condition-checkable items from cron blast to change-gated watchers.
Watchers: condition-until-it-fires
Section titled “Watchers: condition-until-it-fires”A watcher checks a condition on a cadence and fires once when the condition becomes true, then stops.
# Register a watcherpython3.12 -m agents.ai_os.watchers add \ --title "HydroJug reply" \ --kind gmail_search \ --cadence 30 \ --escalate "HydroJug replied — handle the thread" \ --expires 30dFour check kinds:
gmail_search— true when a search query returns resultsfile_exists— true when a file appears at a pathfile_mtime— true when a file’s modification time changesshell— true when a shell command exits 0
Every watcher requires a mandatory expiry — --expires 30d by default. No immortal watchers. An obligation that has no clear end-condition either has the wrong primitive or needs a human to make the call.
The watcher registry lives at ~/agent-system/state/watchers.json. The sweep cron runs every 15 minutes.
Direction-check on gmail watchers. The first HydroJug watcher false-triggered on JD’s own outbound mail because the search wasn’t pinned to from:. Gmail search queries for watchers should include from:<expected sender> to prevent this.
Session loops: in-flight work this session
Section titled “Session loops: in-flight work this session”Session loops are for work that needs to stay alive during a single session but doesn’t need to persist across sessions. A CI run you’re waiting on, a long build job, a “keep retrying this until it passes” pattern.
These don’t need a cron or a watcher — they’re just Bash loops or Agent tool calls with run_in_background: true, where the parent session monitors completion via the notification event.
The key property: session loops die with the session. Use a watcher or cron if the obligation must survive a session rotation.
Durable one-shots: date-based future obligations
Section titled “Durable one-shots: date-based future obligations”For “do X on date Y” — a reminder, a scheduled run, a one-time future action:
# Via the watchers system's date_reached kindpython3.12 -m agents.ai_os.watchers add \ --title "LinkedIn token expiry" \ --kind date_reached \ --date 2026-06-10 \ --escalate "LinkedIn OAuth token expires today — run reauth script"The date_reached kind shipped 2026-06-08. Its first real use: a watcher that fired on June 10 with the LinkedIn reauth command, three days before the token would have died silently.
Off-conditions are non-negotiable
Section titled “Off-conditions are non-negotiable”Every loop must name when it stops. For watchers, that’s the expiry or the trigger. For crons, that’s the calendar (or a manual delete). For session loops, it’s the end of the session. For durable one-shots, it’s the single fire.
A loop without an off-condition is noise waiting to happen. The watcher’s default 30-day expiry is there because most conditions either resolve or become irrelevant within a month. If your condition genuinely needs longer, set it explicitly — but name it.
How this plays out: the June 2026 loop-adoption audit
Section titled “How this plays out: the June 2026 loop-adoption audit”In June 2026, a loop-adoption audit swept the system and found 18 candidates for conversion to watchers or durable one-shots. 5 watchers were registered. 3 were self-rejected as noise (the condition would generate more alerts than value). The remaining 10 were either already in crons or folded into the open-loops ledger.
Two important findings from that audit:
-
Open-loop entries that are machine-checkable conditions should have a paired watcher (tracked in the ledger as
linked_open_loop). The ledger records the human promise; the watcher does the checking. -
The zombie-cron class — cron jobs whose Python modules had been deleted but whose crontab entries remained, running to guaranteed failure on every tick — was caught by the loop audit (and by the CI import-smoke job). Two such zombies were found:
jitai_sensorandlinkedin_approval_gate. Neither produced errors anyone noticed because their failures were silent. The remedy: delete the crontab entry the moment you delete the Python module.
The discipline in practice
Section titled “The discipline in practice”Whenever you finish a piece of work and there’s a pending “and we should follow up on…” at the end, stop before leaving the session and ask: what shape is this obligation?
- Needs to happen on a fixed schedule? Add the crontab line.
- Needs to fire when a condition changes? Register the watcher.
- Needs to happen on a specific date? Register the date_reached watcher.
- Only relevant for this session? A session loop or a plain reminder is fine.
That pause — five minutes to wire the obligation to the right primitive — is the whole doctrine. It’s what makes “I’ll check back on that” true instead of aspirational.
Next: Watchers in depth — the four check kinds, the direction-check pattern, how open-loop pairing works. Watchers: condition-until-it-fires.