← Back to blog

Scheduled AI Agent Workflows: What Nine Agents on a Timer Taught Us

Every morning, Windows Task Scheduler on one machine starts nine of our agents with nobody at the keyboard. At 08:00 one asks Claude and GPT-6 Sol twenty buyer questions and scores the answers. At 08:30 another checks whether the searches Claude runs return our pages. Between 08:00 and noon two more write the day's X post and X replies, and between 10:00 and 13:00 a third picks a Reddit thread to comment on. On Tuesdays and Fridays three more read Search Console, Bing and the week's numbers; on Mondays and Thursdays one cross-posts to Medium. On October 18 the last result of every one of them was 0. The first week did not look like that.

A script owns the run, the model owns one step

All nine follow the same pattern. Task Scheduler starts a PowerShell wrapper. The wrapper moves a dedicated git worktree to the latest main, runs the steps that have one right answer as plain Node scripts (pull the Search Console snapshot, query the X API), and only then starts a headless Claude Code session with claude -p. That session gets a short list of tools and a turn cap. The X reply agent's session may read files, write files and run two named scripts, for at most 60 turns. It writes drafts and ledger rows. It never runs git, never sends an email and never posts.

Then the wrapper takes over again. It checks that the files it expects exist, commits only the paths it is allowed to commit, pushes, and deploys when the run log has a new record. An agent with nothing to judge gets no session at all: our Medium cross-post is a Node script and a wrapper, because every one of its steps is deterministic.

If you want recurring agent tasks to be predictable without designing a big workflow first, this split is how we got there. Put the predictable parts in a script you can read, and hand the model only the step that needs judgement. The wrapper's exit code is the contract with the scheduler: 0 means the run completed, anything else means a person should look.

Four ways the first runs broke

None of these was a model mistake. All four were plumbing.

  1. On October 11 the first real run of the Google readout got through five of its six steps and died on git. Git printed a line-ending warning on stderr, and under PowerShell's $ErrorActionPreference = 'Stop' a native command's stderr becomes a terminating error. The wrappers now run with Continue and judge native commands by their exit code alone.
  2. On October 13 the X reply task was registered before its scripts had reached main. Its first scheduled run ended with 0xFFFD0000, no transcript and no email, because the wrapper that sends the email was not there to run.
  3. On October 14 the X sessions could not save their drafts. A headless session has no scratchpad directory, and a Write permission scoped to a folder was refused in every spelling we tried. The run wrote four replies and recorded none of them. Write is now allowed outright, and drafts go to a gitignored folder so the checkout stays clean for the next run.
  4. A change to a wrapper takes effect one run late. The wrapper updates its own checkout after PowerShell has already read the old copy of the script, so the first scheduled run after a push still runs the previous version. On October 14 the X post went out on a third, manual run.

The watchdog, and the bug in it

A wrapper can report its own failure. It cannot report a run that never started, or one whose script was missing, which is exactly what happened on October 13. So a tenth task runs every 30 minutes and reads Task Scheduler itself. A non-zero last result, or a run that was due and never started, gets one email, and a small state file remembers what was sent so nothing is sent twice. We had a reason to build it: on the same machine, a daily job for another project had failed fourteen mornings in a row, from late September into October, and nobody noticed until someone went looking.

The watchdog carried a bug of its own for five days. Three of our tasks start at a random minute inside a window, so that posts do not land at the same minute every day. For a trigger with a random delay, Task Scheduler reports the next run time as tomorrow's draw as soon as today's window opens, whether or not today's run has happened yet. On October 17 the X post task showed its next run as October 18 at 09:44, then ran at 11:30. A missed run of those three tasks could never have been seen. The watchdog now works out from the trigger itself when today's run was due, and calls it missed only once the window plus 30 minutes has passed with no start.

Letting a browser script repair itself

Some of the agents drive real web pages through a debug Chrome: Medium, Bing Webmaster Tools, Reddit. When a page changes, the script breaks at 10:30 in the morning with nobody around. Since October 15 the Medium agent repairs itself. When the script exits with 4, meaning an element it expected was not on the page, the wrapper starts up to two repair sessions, and each may edit that one script and run its dry run, nothing else. A gate script then refuses any diff that touches the locked blocks around the publish click, the record write and the exit codes, or any file outside the agent's folder. If the wrapper's own dry run passes, the fix goes to main as a fix(medium): self-repair commit and the real run starts again. An end-to-end test on October 15 went through the whole loop in nine turns for $2.39. A failure at the publish click itself still exits 1 and waits for a person, because a dry run proves nothing about the one step it skips.

Every run goes on a public log

Each agent writes one record per run, and the site builds a public run log from those records at deploy time: the Google readout's is at /agents/google-visibility/log, the X replies' at /agents/x-comment/log. A run that stopped, or that needed a person, is on the log too. The record is a file the run itself writes, a summary's frontmatter or a ledger row, so the log cannot drift from what actually ran.

What we would do again, and where it stops working

Start from the operating system's scheduler (Task Scheduler for us, cron anywhere else) and a wrapper script, not from an agent framework's scheduler. Make the model's step the smallest one in the run, give it the fewest tools that let it do that step, and let the wrapper own every side effect: git, email, posting, deploys. Treat exit codes as the interface. Watch the scheduler from outside, because the failures that cost us most were the runs that left no trace.

The setup has one limit we live with. All 10 tasks run only while one account is signed in on one machine. A task that missed its time catches up a minute or two after sign-in, but if the machine is off for a day, so are the agents. If your runs have to happen whether or not a computer is on, or have to fan out across machines, put the same wrapper behind a cloud scheduler. We have not run ours that way. The wrappers are Windows PowerShell and would need porting; the split between script and model would not.

We run these agents for one purpose, getting a startup's product named and cited in AI answers, and we run them on our own company first. Every one of them is on the growth agents page, with its run log.

ClawWorld runs AI visibility work for B2B startups. Start with a free baseline report: 10 buyer questions, who ChatGPT and Claude name instead of you, and the pages they cite.

Get your free baseline report →