Claude Code Scheduled Tasks: Loops, Cron and Routines

The scheduler adds a deterministic jitter offset so sessions do not all hit the API at the same wall-clock moment. Recurring tasks fire up to 30 minutes after the scheduled time, or up to half the interval for sub-hourly tasks. The offset comes from the task ID, so it is consistent — schedule for a minute that is not the top or bottom of the hour to avoid it.
Recurring session-scoped tasks expire seven days after creation: the task fires one final time, then deletes itself. This bounds how long a forgotten loop can run. For durable scheduling that outlives a session, use cloud routines, Desktop scheduled tasks, or a schedule trigger in CI.
With an interval, your prompt runs on a fixed cron schedule. Without one, Claude chooses each delay itself — between one minute and one hour — based on what the last iteration observed, and prints the delay and its reasoning each time. With neither an interval nor a prompt, it runs the built-in maintenance prompt, or your loop.md if one exists.
No. Session-scoped tasks only fire while Claude Code is running and idle, so closing the terminal stops them. Backgrounding the session carries its loop tasks over to a background session that keeps running without a terminal. For scheduling that survives independently, use routines or Desktop scheduled tasks.
No. If a task's scheduled time passes while Claude is busy on a long-running request, it fires once when Claude becomes idle, not once per missed interval. A five-minute poll overlapping a twenty-minute turn produces a single fire, which is usually what you want but is worth knowing.

Key Takeaway
Claude Code offers three ways to schedule work: session-scoped loops that need an open session, Desktop scheduled tasks that need your machine on, and cloud routines that need neither. Session tasks live in the current conversation, expire seven days after creation, and fire between turns rather than interrupting one.
I asked a session to check a deployment every five minutes and came back an hour later to find it had checked nine times, not twelve. Nothing had failed. The scheduler had been adding a deterministic offset to every fire time on purpose, and I had been reading a clock that was never going to match.
Scheduling in Claude Code is more opinionated than it looks, and the opinions are mostly good ones once you know they exist. This post covers the three mechanisms and when each is right, the forms a loop can take, the cron dialect that is supported and the parts that are not, and the two behaviours — jitter and a seven-day expiry — that exist to stop scheduled work becoming a problem.
The question that separates them is not how often something runs, it is what has to be true for it to run at all. A session-scoped loop needs a session open. A Desktop task needs the machine on. A cloud routine needs neither, at the cost of a fresh clone rather than your local files.
What each one requires and gives up:
| Property | Cloud routines | Desktop tasks | Session loops |
|---|---|---|---|
| Requires machine on | No | Yes | Yes |
| Requires an open session | No | No | Yes |
| Access to local files | No — a fresh clone | Yes | Yes |
| Minimum interval | One hour | One minute | One minute |
Both the interval and the prompt are optional, and which you supply changes the behaviour completely rather than just the cadence.
# Three forms, and what you supply decides the behaviour.
/loop 5m check if the deployment finished and tell me what happened
# -> fixed cron schedule. Seconds round UP to the nearest minute.
# 7m and 90m round to the nearest clean cron step, and Claude
# tells you what it picked.
/loop check whether CI passed and address any review comments
# -> Claude picks each delay itself, between one minute and one
# hour, based on what it just observed. It prints the delay
# and the reason at the end of every iteration.
/loop
# -> the built-in maintenance prompt, at a self-chosen interval:
# finish unfinished work, tend the branch's PR (review
# comments, failed CI, merge conflicts), then cleanup passes.
/loop 20m /review-pr 1234
# -> a skill as the prompt. Only skills Claude is allowed to
# invoke on its own actually run; built-in commands such as
# /permissions or /model arrive as PLAIN TEXT instead.
# Esc clears the pending wakeup while a loop is waiting.The self-paced form is the one I use most. With no interval, Claude picks each delay between one minute and one hour based on what it just observed — short waits while a build is finishing, longer ones once a PR goes quiet — and prints both the delay and its reasoning at the end of every iteration. That turns the interval from a guess you make in advance into a decision informed by the last result.
When you ask for a self-paced loop, Claude may reach for the Monitor tool instead. Monitor runs a background script and streams each output line back, which avoids polling altogether — it is both more token-efficient and more responsive than re-running a prompt on a timer. If what you are actually waiting on produces output lines, that is the better mechanism, and it is worth asking for by name.
A bare loop runs a built-in maintenance prompt: continue unfinished work, tend the current branch's pull request, then run cleanup passes when nothing else is pending. Claude deliberately does not start new initiatives outside that scope, and irreversible actions such as pushing only proceed when they continue something the transcript already authorised. A loop.md file replaces that prompt with your own — it defines one default, not a list of tasks, and is ignored entirely whenever you supply a prompt on the command line.
# .claude/loop.md — replaces the built-in maintenance prompt
# for a bare /loop. Project-level wins over ~/.claude/loop.md.
# Plain Markdown, no required structure. Keep it under 25,000
# bytes; beyond that it is truncated.
Check the release/next PR. If CI is red, pull the failing job log,
diagnose, and push a minimal fix. If new review comments have
arrived, address each one and resolve the thread. If everything is
green and quiet, say so in one line.
# Edits take effect on the NEXT iteration, so you can refine the
# instructions while the loop is still running.Under the hood Claude uses three tools you never call directly — one to create a task, one to list them with their IDs and schedules, and one to cancel by ID. Each task gets an eight-character ID, and a session holds up to 50 at once. The expressions are standard five-field cron, with two behaviours worth internalising before you write one.
# CronCreate takes a standard 5-field expression:
# minute hour day-of-month month day-of-week
*/5 * * * * every 5 minutes
0 * * * * every hour on the hour
7 * * * * every hour at 7 minutes past
0 9 * * * every day at 9am LOCAL — not UTC
0 9 * * 1-5 weekdays at 9am local
30 14 15 3 * March 15 at 2:30pm local
# Day-of-week is 0 or 7 for Sunday through 6 for Saturday.
# Extended syntax — L, W, ? and aliases like MON or JAN — is
# NOT supported.
#
# When both day-of-month and day-of-week are constrained, a date
# matches if EITHER field matches. Standard vixie-cron semantics,
# and a reliable source of surprise.
CLAUDE_CODE_DISABLE_CRON=1 # kill the scheduler entirelyTo stop every session in the world hitting the API at the same wall-clock moment, the scheduler adds a deterministic offset to fire times. It is derived from the task ID, so the same task always gets the same offset — it is not random per fire, it is a fixed shift you can work around:
If exact timing genuinely matters, pick a minute that is neither the top nor the bottom of the hour. Scheduling for seven minutes past instead of on the hour skips the one-shot jitter entirely, and it is a one-character change to the expression. This is the single most useful thing to know about the scheduler and it is easy to miss.
Recurring session tasks expire seven days after creation. The task fires one final time, then deletes itself. This is a deliberate bound on how long a forgotten loop can run, and it is the right default — a polling loop nobody remembers creating is a slow leak of tokens and attention. If you need something durable, that is what routines, Desktop tasks and a scheduled CI trigger are for.
Tasks only fire while Claude Code is running and idle. Closing the terminal stops them. There is also no catch-up: if a scheduled time passes while Claude is busy on a long request, the task fires once when Claude becomes idle, not once per missed interval. A five-minute poll that overlaps a twenty-minute turn produces one fire, not four.
Session scoping is the whole design, so the resume rules follow from it:
The rule I settled on is to match the mechanism to what must be true for the work to matter. Polling something while I am already sitting there is a session loop. Something that must happen whether or not I am at my desk is a routine or a CI schedule. And when the thing I am waiting on emits output lines rather than requiring a fresh look, the answer is not scheduling at all — it is a monitor, which tells me the moment something happens instead of asking every five minutes whether it has.
Sources & further reading