Claude Code has a feature called routines: a cloud session that starts on a cron schedule, clones a GitHub repository, uses the MCP connectors you've attached on claude.ai (Slack, Gmail), and exits. No server, no extra tooling.
I used it to build a watcher that, every morning at 09:00 JST, reads 12 pages of Anthropic's official docs, diffs them against yesterday, posts a Japanese summary to Slack, and, when a change affects my books, pushes a draft article to GitHub. The motivation was embarrassing: I write guides about the Claude API, and I missed the Haiku 5.5 launch for five days. A local script had been posting "changed" notices to Slack every day. Nobody was reading them.
Everything below is as of October 16, 2026. Routines are moving fast; check the official docs for the current shape.
What a routine is
- A 5-field cron expression (UTC) or a one-time timestamp starts a cloud session. Minimum interval is one hour.
- The session starts with your GitHub repository cloned. It cannot see your local files, local environment variables, or local services.
- It can use MCP connectors attached on claude.ai (Slack, Gmail, ...). MCP servers configured only in Claude Code are not available.
- Model, allowed tools, and the prompt go in one JSON object. The prompt is read from scratch every run, so it has to be self-contained.
- Create, update, run-now, and run logs are available through an API (from inside Claude Code, the
RemoteTriggertool). Deleting is web-only.
What I built
volvox-ltd/claude-docs-watch (private)
├── pages.json # 12 official pages (the .md versions on platform.claude.com)
├── watch.py # fetch -> compare with state/ -> diffs/<date>/ + result.json (stdlib only)
├── state/ # yesterday's text
├── diffs/ # unified diffs per day
├── drafts/ # article drafts the routine writes when a change matters
├── log/ # daily summaries
└── ROUTINE_PROMPT.md # the instructions the routine receives
The design decision that matters: Python finds the diff; the model only judges and writes. Asking a model to "read these 12 pages and tell me what changed" means reading ~300k characters every morning and missing things. watch.py produces the diff files; the model reads only those.
The routine definition
{
"name": "claude-docs-watch 09:00 JST",
"cron_expression": "0 0 * * *",
"enabled": true,
"job_config": {"ccr": {
"environment_id": "env_…",
"session_context": {
"model": "claude-sonnet-5-5",
"sources": [{"git_repository": {"url": "https://github.com/volvox-ltd/claude-docs-watch"}}],
"allowed_tools": ["Bash", "Read", "Write", "Edit", "Glob", "Grep",
"mcp__Slack__slack_send_message", "mcp__Slack__slack_search_channels"]},
"events": [{"data": {"uuid": "…", "session_id": "", "type": "user", "parent_tool_use_id": null,
"message": {"role": "user", "content": "<the prompt below>"}}}]}},
"mcp_connections": [{"connector_uuid": "…", "name": "Slack", "url": "https://mcp.slack.com/mcp"}]
}
Sonnet 5.5 is plenty: the daily job is "read a diff and summarize it".
The prompt
The repository https://github.com/volvox-ltd/claude-docs-watch is already cloned in your
working directory (branch main). Configure git with user.name 'docs-watch-bot' and
user.email 'contact@volvox-ltd.com' before committing.
You are the daily docs watcher for persimmoq, an author of Claude API guides. You run
inside a fresh clone of this repository every morning. Work in Japanese for all
human-facing text.
Do these steps in order.
1. Run `git pull --rebase origin main` first (the checkout can be stale). Then run
`python3 watch.py` from the repository root and read `result.json`.
2. If `changed` is empty and `errors` is empty: append one line `<date> no changes` to
`log/<YYYY-MM>.md`, commit it, push to `main`, and send ONE short Slack message to the
private channel #mail (channel_id C0BEY8JUCDR). Then stop.
3. If there are changes: read every file listed under `changed[].diff`. Ignore pure
formatting changes. For the substantive changes, write a summary with, per page: the
page key, 1–3 sentences on what changed, and a judgement on whether it affects the
author's published material (the books on Claude 5 migration and on Managed Agents,
and the articles on those topics). Mark each page as affected / not affected / check.
4. Write that summary to `log/<YYYY-MM>.md` under a heading `## <date>` and post the same
summary to the Slack channel. Keep it under 2,000 characters; link each page URL.
5. If at least one page is affected and the change is a new model, a new price, a new
breaking change, a deprecation/retirement, or a new Managed Agents feature: write ONE
draft article in `drafts/<date>-<short-slug>.md` (front matter with published: false),
quoting the exact new text from the diff where it matters, linking the official page,
and ending with a section "What I verified" containing only a placeholder for the author
to fill in after testing against the live API. Do not invent behavior that the
documentation does not state. Do not claim anything was tested.
6. `git add -A`, commit, push to `main`. If the push fails, retry once after `git pull --rebase`.
7. If any `errors` are present, mention them at the end of the Slack message.
Never publish anything outside this repository. Never edit files under `state/` by hand.
Step 5 is the point. The coworker drafts; it never publishes. The draft ends with an empty "What I verified" section that a human fills in after running the real API. The prompt says "do not claim anything was tested" for the same reason.
What happened on the first two runs (October 16)
Run 1 (23 seconds, 8 turns). Sandbox allocated, repo cloned, watch.py found no changes, log appended, push succeeded, Slack post failed with channel_not_found. The agent searched for the channel, found nothing, and fell back to a mobile push notification. Cause: the Slack connector on claude.ai was attached to a different workspace. I reconnected it and put the channel ID in the prompt.
Run 2 (51 seconds, 9 turns). Slack post succeeded. Then the push was rejected: the sandbox's checkout predated run 1's commit. The agent ran git pull --rebase, saw its own commit was a duplicate line, discarded it, and exited cleanly. Reasonable, but I added git pull --rebase as step 1 so it doesn't happen every morning.
The run log is available from the API with timestamps for every tool call, error, and the final message. You can read everything the coworker did while you were asleep.
Three things that broke
-
The GitHub App must be installed on the organization. Connecting GitHub on claude.ai was not enough for a private repo under an org; creating the routine failed with
repo_access_denieduntil I installed github.com/apps/claude on the org and granted the repo. - A connector points at one workspace. With several Slack workspaces, it was attached to the wrong one. Use channel IDs, not names.
- The checkout is not always fresh. A routine that pushes should pull first.
Cost and limits
- Runs use the allowance of my Claude subscription plan; I have seen no separate API charges. A no-change run is a few thousand tokens. Check the pricing page for your plan.
- Minimum interval one hour. Not a tool for "poll every five minutes".
- No access to your local machine. Anything the job needs must be in the repo or reachable from the cloud.
- Deletion is web-only.
Three ways to build the same thing
| Claude Code routine | Managed Agents | cron + API on your machine | |
|---|---|---|---|
| Runs on | Anthropic's cloud | Anthropic's cloud | your machine |
| Setup | one JSON | Agent + Deployment | plist/crontab + script |
| Billing | plan allowance | tokens + session time ($0.08/h) | API tokens |
| Best for | periodic jobs that fit in a repo + connectors | long-running agents that need a hosted toolbox | jobs that need local files or local credentials |
| Notify via | connectors (Slack/Gmail), push | webhooks | your own |
I use all three. This watcher is a routine; my production agent platform is Managed Agents; the weekly Search Console report, which needs local OAuth tokens, stays on launchd.
Takeaways
- A Claude Code routine gives you a cron'd cloud agent with a repo and Slack, with no infrastructure.
- Split the roles: Python detects, the model judges and writes, a human publishes.
- What breaks is permissions and connections, not the prompt.
The 400-errors-when-migrating guide this watcher protects is here: Claude 5 Migration Guide (PDF, $19).
Top comments (2)
"Python finds the diff; the model only judges and writes" is the part I'd steal. It also keeps the run cheap enough that an hourly schedule wouldn't hurt.
One question about the boundary you hit: since a routine only sees the cloned repo and claude.ai connectors, what do you do when a step only exists on your own machine, like an app with no API or a page that needs your logged-in browser? Do you keep those steps out of routines entirely, or have the routine leave a task file that something local picks up?
(Disclosure: we build a computer-use tool, so the local-only steps are the ones we keep running into.)
Doing the diff in Python before the model wakes up is the only sane way to run this on a schedule. I do a similar pass on upstream API specs, and the main failure mode that caught me early was doc site scaffolding changes. If a platform wraps the markdown in a new layout or reshuffles heading anchors, a raw text diff dumps hundreds of lines of noise into the prompt. Normalizing the markdown into an AST or stripping navigation links before computing the diff stopped the agent from drafting false-alarm guides whenever an upstream docs team touched their layout.