July 11, 2026 · Italo Campilii

Scheduled AI Agents That Work While You Sleep

The most useful agents I run are the ones I never start. Every morning at 7:00am, a job sweeps sources, scores candidates against a written rubric, and writes me a queue file — whether I'm awake or not. A content engine publishes on a 2-hour loop with zero manual steps. The difference between an AI assistant and AI infrastructure is exactly this: does it run on a clock, or does it wait for you?

Here's how I build scheduled agents that actually run, actually stay safe, and actually tell me when something breaks.

The core pattern: headless Claude Code on a scheduler

The mechanism is almost boring. A shell script cd's into the project directory and invokes Claude Code in headless mode — claude -p "<prompt>" — with a prompt that points at a written procedure. The script logs everything it does to a file. Then the operating system's scheduler runs that script on a cadence: launchd on macOS (a plist in ~/Library/LaunchAgents/), cron on Linux.

That's the whole trick. The intelligence lives in the agent; the reliability lives in a 40-year-old scheduling primitive. My real jobs look like this:

Note the mix: some jobs are full agent runs, some are plain scripts. Use the agent where judgment is needed; use a script where it isn't. Agents are the expensive, occasionally-wrong tool — spend them on the steps that need reading and deciding.

Guardrails: what an unattended agent may never do

An agent running with no human watching needs harder rules than an interactive one. Mine are written into the procedures the jobs execute, and they're non-negotiable:

The pattern behind all five: an unattended agent should be able to fail, pause, or under-deliver — but never surprise you. The full drift-prevention layer (written procedures the agent must load) is covered in how CLAUDE.md and skills stop agent drift.

"Armed" is a claim you verify, not assume

Here's the failure mode nobody warns you about: you write the script, you write the plist, you feel done — and the job never runs. Wrong path, wrong permissions, plist never loaded, scheduler rejected it silently. You find out weeks later that your "24/7 automation" ran zero times.

My rule: a scheduled job does not exist until it's verified armed. The checklist I run after installing any job:

  1. Ask the scheduler. launchctl list | grep <label> must show the job (or crontab -l on Linux). If the scheduler can't name it, it isn't scheduled.
  2. Run it manually, end to end. Execute the exact wrapper script the scheduler will call — not the underlying command — and require exit 0. My daily sweep's install wasn't done until a manual run produced a real queue file, real CSV rows, and real logged blockers.
  3. Check the artifacts. The job must leave evidence: a log line, an updated state file, an output file with today's date. Verify each one exists after the test run.
  4. Verify the state file advances. Jobs that process "new items since last run" keep a last-checked timestamp. Confirm it updates, or your job will reprocess everything forever — or nothing ever.
  5. Wait for the first scheduled firing and read the log. Manual success doesn't prove scheduled success — launchd jobs run with a different environment than your shell. Confirm the first real firing in the log the next day.

Heartbeats: silence must be a signal

A job that finds nothing and a job that's broken look identical: no output. That ambiguity will quietly eat your trust in the whole system, so I design it away.

My inbox watcher sends a Telegram digest whenever it finds matches — and on the evening run, if it found nothing all day, it sends a "no replies, heartbeat" message. One guaranteed touch per day. If that heartbeat ever misses, I know the system is down, not the news. Every scheduled job also writes a run log and its stdout/stderr to files, so "did it run?" is always answerable in one command.

Cheap rule of thumb: every scheduled job needs a way to say "I ran and found nothing" that is distinguishable from saying nothing at all.

Failure handling without babysitting

Things break: an upstream site changes, a token expires, the machine was asleep. The design response, in order:

Where this fits

Scheduled agents are one layer of a larger architecture — the clock layer. The operating layer (Claude Code), the procedure layer (skills and CLAUDE.md), the infrastructure layer (Cloudflare), and the verification loops around all of it are laid out in the pillar post: How One Person Runs Five Brands with AI Agent Fleets. But if you build only one thing from this series, build one scheduled job with a heartbeat. The first morning you wake up to work that finished itself — correctly, with a log proving it — the rest of the architecture stops being optional in your mind.

FAQ

Why launchd instead of a cloud scheduler?

Because the jobs' source of truth is local files. My pipeline sweep reads and writes files in a local directory — a cloud sandbox can't touch them. Rule: run the scheduler where the state lives. Local state, local launchd/cron; cloud state, cloud scheduler.

Isn't it risky to run Claude Code with permissions skipped on a schedule?

It's a real tradeoff, which is why the guardrails live in the written procedure instead: draft-never-send, read-only access, no fabrication, blockers-file escalation, scoped credentials. Headless runs can't stop to ask permission, so you remove the dangerous capabilities from the job's reach and constrain it with hard written rules — then verify behavior with a manual end-to-end run before arming it.

How much do scheduled agent runs cost?

An agent run costs real API money; a Python script costs effectively nothing. That's exactly why the mix matters: my twice-daily inbox watcher is plain Python, and the agent runs are reserved for jobs that genuinely need reading and judgment. Audit your schedule for jobs that don't need an LLM at all — usually several don't.

— Italo Campilii. If you're building something that needs this kind of operator, get in touch.