Running a fleet of twenty Claude agents on a launchd schedule teaches you fast that scheduler guarantees are weaker than you expect. Each agent wakes, processes a slice of work, writes its results, and goes back to sleep until the next tick. The pattern is simple; the failure modes are not. Three months of nightly runs have turned up launchd timing quirks, session-end failures, and the quiet pressure of growing turn counts. This post is a candid maintenance log: what broke, why it broke, and what I changed.
The scheduler is not a magic timer #
When I first set up the cron-style jobs, I used launchd because it integrates cleanly with macOS and gives fine-grained control over start times, resource limits, and restart policies. The first surprise was that launchd does not guarantee exact start times. If the system is busy, a job may be delayed by several seconds, and those seconds add up over a day. For a single agent the drift is negligible, but with a fleet of twenty the cumulative delay can push the final run past the intended window, causing overlapping executions.
The overlapping runs manifested as two agents trying to write to the same SQLite file at the same time. SQLite locks the file for writes, so the second agent stalled until the lock cleared. In a tight schedule that meant a cascade of timeouts, and eventually the launchd daemon marked the job as failed. The fix was to assign each agent a distinct, fixed start minute in its launchd plist. Rather than allowing multiple agents to share the same start slot, hand-spacing the schedule across the day gave each agent a clear window with no overlap and eliminated the write-lock cascade.
Another hidden quirk is launchd's handling of environment variables. The agents rely on a PATH that includes the Python interpreter and a few helper scripts. When launchd launches a job, it inherits a minimal environment that does not include the user's shell profile. The first few runs failed with "command not found" errors because the interpreter could not be located. The solution was to define the full PATH inside the launchd plist and to reference the interpreter with an absolute path. This made the jobs independent of any interactive shell configuration, and the "command not found" errors disappeared permanently.
Turn budgets are counts, not tokens #
Claude agents in this fleet run under per-ticket turn-count ceilings, not token budgets. The ceilings live in a central configuration file and vary by the type of work: a build ticket gets 80 turns, a review 30, a design 50. The numbers were calibrated against historical run data at roughly twice the measured mean, so legitimate work should rarely approach the limit.
The monitoring piece is in place: a turn monitor runs alongside each agent session and fires warnings at the ceiling and again at 1.5x. What is not yet in place is hard enforcement. The turn-budget governor is currently in calibration mode -- it surfaces warnings to the agent but does not halt the session. The practical consequence is that an agent running long on an unusually complex ticket will see warnings but continue until it finishes naturally or hits the wall-clock timeout.
Getting from warnings to enforcement requires validating the ceiling values against a wider range of ticket types and understanding how the numbers shift as the prompt cache warms and cools. The instrumentation came first; the enforcement follows once the calibration is solid. The lesson: instrument early, calibrate patiently, enforce later. Running agents without any turn monitoring made it impossible to see which ticket types were consuming outlier turns until the pattern was already established.
Session-end handling and data ownership #
Claude agents automatically save a session summary at the end of each run. The summary is written to a local SQLite file that lives in the project directory. The file is owned by the user, and the agent does not attempt to upload it anywhere without explicit instruction. This design gives engineers full control over the data, but it also means that session-end failures can leave orphaned files or partially written rows.
I wrote earlier about the cost of context loss between agent sessions -- that post focuses on the token waste. Here the problem is the structural consequence: lost writes corrupt downstream data. The most common failure mode was a sudden termination of the Python process due to an unhandled exception. When the process died, the SQLite transaction was left open, and the next run attempted to write to a locked database, resulting in "database is locked" errors. The lock persisted until the operating system reclaimed the file handle, which could take minutes. During that window the entire fleet stalled.
Ensuring that every database connection is explicitly closed on exit -- whether the session ends cleanly or not -- is the fix. A connection left open by a crashed process holds the write lock until the OS reclaims the file descriptor. Adding explicit close calls in the error path, rather than relying on garbage collection, keeps the lock window short and the next scheduled run clean.
Another subtle issue was the handling of open questions. The agents try to capture any unanswered items that arise during a run and store them as separate rows. The capture is best-effort: if the session does not contain enough signal, the question is not recorded. Early on I assumed that every open question would be saved, and I built downstream alerts that fired on missing rows. When the capture failed silently, the alerts generated noise and eroded trust in the monitoring system.
The lesson was to treat the open-question log as a helpful hint rather than a strict contract. I added a status flag that indicates whether the capture was successful, and downstream processes now check that flag before acting on the data. If the flag is false, the pipeline skips the alert and moves on, avoiding false alarms.
What three months taught me about autonomous AI fleets #
Running a fleet of Claude agents on a schedule is not a set-and-forget exercise. The environment changes, the models evolve, and the operating system introduces its own quirks. Over the past three months I have learned to treat the fleet as a living system that needs regular health checks, just like any production service.
Schedule reliability matters more than raw speed. Staggering the schedule with explicit, distinct start times turned a flaky overlap problem into a predictable rhythm. Turn budgets need instrumentation before enforcement: knowing which ticket types run long is the prerequisite to setting ceilings that don't cut legitimate work short. Robust session handling prevents cascading failures that can bring the whole fleet to a halt. And expectations around automatic data capture should be calibrated to the reality of best-effort inference: design for the case where a piece of data is missing, and your downstream processes will be far more resilient.
If you are a data engineer or AI practitioner considering an autonomous agent fleet, these lessons are your maintenance checklist. Start with a well-defined schedule, monitor turn counts from day one, and make your data persistence resilient to crashes. The effort you invest in these foundations pays off in smoother runs and fewer surprise outages -- which is exactly what makes a recurring series worth reading. This post is the first in an ongoing maintenance log. I will keep updating it as new failure modes appear and new fixes take hold.
If you are considering building an agentic data pipeline from scratch, the LangGraph implementation guide covers the architectural decisions that precede the operational ones covered here. And if you already have a fleet running and want a second pair of eyes on the design, reach out. We have been through the growing pains and know which shortcuts are worth taking.