Background Agents

Pro · 6 min read

A background agent is one that starts itself — on a schedule, a webhook, an event — does its work, and reports. Nightly dependency triage, a morning test-flake report, auto-labeling new issues, a weekly docs-drift check: the class of work that's valuable, boring, and never urgent enough to do by hand. The scheduling is trivial — cron is fifty years old; every CI system has a schedule: trigger; Claude Code runs headless from any of them (lesson 18 covers the invocation). Everything non-trivial about background agents comes from one fact: nobody is watching. Unattended failure has three shapes attended work never taught you to fear, plus a stop-rule discipline and an authority boundary. That's this lesson.

Failure shape one: overlap

Monday's run hangs on a slow network call. Tuesday's fires anyway. Now two agents, same job, same files — lesson 13's shared-directory corruption, on a timer, with no human around to notice the pile-up. By Thursday there are three.

Every scheduled agent needs a concurrency answer designed, not defaulted: a lockfile the run takes and releases; your scheduler's built-in guard (GitHub Actions' concurrency: key does this in two lines); or a fresh worktree per run (lesson 13) so overlapping runs at least can't corrupt each other. And the policy question stated explicitly: when the previous run is still alive, do you skip this firing, queue it, or kill the stale run? For most maintenance jobs, skip is correct — a missed night costs almost nothing; an interleaved night costs an investigation. Write the choice down in the job itself.

Failure shape two: drift

A run fails halfway and leaves partial state — half-updated files, a held lock, an abandoned branch, a temp directory. The next run starts from that wreckage and compounds it. By Friday the job fails for reasons four days removed from the original cause, and the investigation has to dig through the sediment of every intermediate failure.

The cure is a lifecycle rule with no third state: every run starts from known state and ends in one of exactly two conditions — completed, or cleaned up. Fresh clone or fresh worktree at start; a cleanup path (trap handlers, a finally, the scheduler's post-step) that runs even on failure. "Failed messily" is not an allowed exit. This is also an argument for making runs idempotent where possible: a job that can safely re-run from the top is a job whose drift surface is near zero.

Failure shape three: silence

The job broke in March. It's discovered in May. Nothing alerted, because the thing that would have noticed is the person who isn't there. Two disciplines close the gap, and the second is the one everyone misses:

Failures announce themselves somewhere humans already look — Slack, an issue, email. Not a log file nobody tails; logs are where alerts go to be archived.

Success leaves evidence too. A heartbeat artifact — "ran at 02:00, examined 34 PRs, acted on 3" — is what distinguishes quiet because fine from quiet because dead since March. Without it, the two are literally indistinguishable, and you'll only learn which one you had when the work it was doing silently stops being done. The rule: silence must never be ambiguous. (Lesson 18's structured run artifacts are the natural heartbeat; this is one more reason every headless run should leave one.)

runningon its schedule, unattendeda stop rule tripshaltedvisible, stationary, inspectablea human resets itnever through itselfthe only way back is through a person —
The state machine that makes unattended safe. Tripping is cheap by design - a false halt costs a restart. The clearing is what must stay expensive: a system that can decide its own alarm was spurious will eventually be wrong about exactly that, at the worst hour. Halted is a good state; it means the design worked.

Stop rules: retry versus runaway

Attended, a failing job gets a human sigh and a fix. Unattended, the retry policy is the judgement, so write it with the care you'd want at 2am:

  • Allow N consecutive failures, then halt. Three is a sane default.
  • Count consecutive, not cumulative. A cumulative counter eventually halts every healthy job that fails occasionally — a job failing once a fortnight is fine forever, and a counter that doesn't reset on success will kill it by June. This bug is subtle, common, and always discovered as "why did the healthy job stop?"
  • Halted stays halted until a human clears it. The halt is a good outcome: a loop that would have burned money reproducing the same failure nightly has converted itself into a visible, stationary problem. The schedule may not restart it; only a person, having looked, may. (Auto-resume defeats the entire mechanism — it turns "halt" back into "retry with extra steps.")

Stack the money guards underneath (lesson 5): per-run budget refused up front, monthly cap at the provider — scheduled × runaway is exactly the multiplication that turns a bug into an invoice.

What deserves a schedule, and how much authority

Three admission criteria: the work is genuinely recurring (one-offs get sessions); it's tolerant of a missed run (if a skipped night hurts, you're building production infrastructure — give it lesson 28's discipline, not a cron line); and it's bounded in authority.

That last one is the deepest design choice, and the principle is worth engraving: an unattended agent proposes; humans (or gated pipelines) dispose. Open the PR — don't merge it. File the issue — don't close others'. Draft the reply — don't send it. Hooks enforce the boundary mechanically (lesson 15); the halt rule bounds the blast radius; propose-don't-dispose bounds the blast consequence — because some run, someday, will be wrong, and the design question is never "how do we prevent all wrong runs" but "what does the wrong run cost." Design for that one run, and the other three hundred are free.

Done right, background agents are the best deal in the course: an evening of setup that runs for a year is hundreds of hours of attention you never spend. The craft is entirely in the guards — overlap, drift, silence, stop, authority — every one cheap on the day you build the job, every one expensive on the week you learn why it was missing.

A personal teaching project, in development. The registry it builds is a teaching artifact and is not an assurance opinion about any issuer. Not affiliated with or endorsed by any employer, or by any vendor whose tools it describes. Where a unit depends on a specific flag or path, it names the version it was verified against. All units.