BUILD · PHASE 12 · LESSON 82

Loops — Scheduled and Recurring

A loop is a timer bound to a live session. It fires between your turns, it

Type
Build
Prerequisites
Phase 04 Lesson 30 (Sessions, `/clear`, `/compact`), Lesson 78 (Dynamic Workflows)
Time
~40 minutes

Learning Objectives

  • Run a prompt on a fixed interval, and on an interval Claude chooses
  • Read a scheduled task back — its ID, its schedule, and when it fires next
  • Prove the two behaviours that surprise people: no catch-up, and session scope
  • Replace a bare /loop with your own default prompt using loop.md
  • Decide between /loop, /goal, and a cloud routine for a given job

The Problem

A deploy was rolling out. The plan was sensible: check it every five minutes, and act if something went red.

So the loop was set — /loop 5m check the deploy and tell me what happened — and the laptop was closed while everyone went to lunch. Thirty minutes later it was opened again, and nothing had run. Not the checks, not one of them.

Two things had been assumed that were not true. The first is that a loop is a daemon. It is not: a scheduled task fires while the session is open, so closing the terminal stops it firing, exactly as it stops the session. The second is that a missed fire is made up later. It is not: a task whose time passed while Claude was busy fires once when Claude next goes idle, not once per missed interval.

That second one is the subtle one, because it is invisible on a quiet day. Set a loop, do nothing, and every iteration appears. Give the session one long request that spans three iterations and you get one fire, and no message telling you that two were skipped.

Separately: a loop that had been running for a fortnight had quietly stopped some time in its second week. Recurring tasks expire seven days after creation — they fire a final time, then delete themselves.


The Concept

Three ways to put Claude Code on a schedule. They are not variations of each other; they run in different places and survive different things.

Cloud routinesDesktop scheduled tasks/loop (this lesson)
Runs onCloud, Anthropic-managed by defaultYour machineYour machine
Machine must be onNoYesYes
Session must be openNoNoYes
Survives a restartYesYesRestored on --resume, with exceptions
Access to local filesNo (fresh clone)YesYes
MCP serversConfigured per taskConfig files and connectorsInherits from the session
Permission promptsNone — runs autonomouslyConfigurable per taskInherits from the session
Minimum interval1 hour1 minute1 minute

The column that decides most cases is "session must be open". /loop is the one for polling during work you are doing. Everything durable is elsewhere, and that is Phase 17's territory.

Three shapes of /loop

What you pass decides which of these you get.

You provideWhat happens
An interval and a prompt — /loop 5m check the deployYour prompt runs on a fixed cron schedule
A prompt only — /loop check the deployClaude picks a delay between one minute and one hour after each iteration, based on what it saw. It prints the delay and the reason
An interval only, or nothing — /loopThe built-in maintenance prompt runs, or your loop.md if one exists

Intervals take s, m, h, and d. Seconds round up to the nearest minute, because cron has one-minute granularity. Intervals that do not map to a clean cron step — 7m, 90m — are rounded to the nearest one that does, and Claude tells you which it picked.

You can pass a skill as the prompt — /loop 20m /review-pr 1234. A scheduled fire only runs skills Claude is allowed to invoke on its own. Built-in commands like /permissions or /clear, skills marked disable-model-invocation: true, and MCP prompts all arrive as plain text instead.

One-shot reminders are the same machinery

remind me at 3pm to push the release branch
in 45 minutes, check whether the integration tests passed

Claude pins a fire time with a cron expression, creates a single-fire task, and the task deletes itself after it runs.

Underneath all of it are three tools: CronCreate (a five-field cron expression, the prompt, and whether it recurs), CronList, and CronDelete. Each task has an eight-character ID, and one session can hold up to 50.

How a scheduled fire actually happens

The scheduler checks every second and queues due tasks at low priority. A scheduled prompt fires between your turns, never mid-response. If Claude is busy when a task comes due, the prompt waits for the turn to end.

All times are local. 0 9 * is 9am wherever you are, not UTC.

And fire times are shifted on purpose. If every session ran its tasks exactly on schedule, they would all hit the API in the same instant, so:

  • A recurring task starts each run late — up to half the gap to its next run, and at most 30 minutes. Hourly and daily tasks are at the top of that range
  • The exception is a task written */5 , which is what /loop 5m creates. That one starts 15 seconds early, because a run exactly five minutes later would begin just after a five-minute prompt cache expired
  • A one-shot task scheduled for :00 or :30 runs up to 90 seconds early

CronList shows a next run time, which is the only sane way to know what you actually got.

The two things that bound a loop

Seven days. A recurring task expires seven days after creation. It fires one final time and deletes itself. To go longer, cancel and recreate before it expires, or use a cloud routine.

No catch-up. Missed fires are not replayed. One fire when the session next goes idle, not one per missed interval.

Both of these are brakes on a forgotten loop, and both of them mean the loop cannot be your reliability mechanism.

Writing your own default prompt

A bare /loop uses a built-in maintenance prompt unless a loop.md exists:

PathScope
.claude/loop.mdProject. Takes precedence
~/.claude/loop.mdUser. Applies wherever the project has no file

It is plain Markdown with no required structure — write it as if you were typing the prompt. It defines one default prompt for bare /loop, not a list of tasks, and it is ignored whenever you supply a prompt on the command line. Edits take effect on the next iteration, so you can steer a loop while it runs. Content past 25,000 bytes is truncated, so keep it tight.

The reframe: a loop is a timer bound to a live session rather than a

daemon — it fires between your turns, it cannot catch up on what it missed, and

it deletes itself after seven days, so it buys you fewer keystrokes, not

uptime.


Build It

1. Start with the smallest loop: one fire

in 2 minutes, tell me the current git branch and whether the tree is clean

Then:

what scheduled tasks do I have?

✅ You pass when CronList shows a single-fire task with a next run time, and the task is gone from the list after it fires.

2. Fixed interval

/loop 5m check whether CI passed and tell me what happened

✅ You pass when Claude confirms a cadence and a job ID, and CronList shows the schedule as a cron expression rather than as 5m.

3. Let Claude choose the interval

/loop check whether CI passed and address any review comments

Read the end of the first iteration. A dynamically scheduled loop prints the delay it chose and why.

✅ You pass when you can say what Claude saw in the first iteration that made it pick the delay it picked.

4. Prove no catch-up

This is the experiment the opening incident is about. Set a short interval, then immediately give the session a request that takes several intervals to finish — something genuinely long, not a sleep. Count the fires while it runs, then count what arrives when the turn ends.

✅ You pass when you can state how many fires were due, how many actually happened, and which behaviour that demonstrates rather than a bug.

5. Stop it two ways

For a self-paced loop, press Esc while it is waiting for its next iteration.

Then, for a loop you created by asking Claude directly, cancel it by ID:

cancel the deploy check job

✅ You pass when Esc clears a pending wakeup without touching your other scheduled tasks, and CronDelete removes the one you named.

6. Own the default prompt

Create .claude/loop.md in this repo:

Check the current branch's pull request. If CI is red, pull the failing job log,
diagnose, and push a minimal fix. If new review comments arrived, address each
one and resolve the thread. If everything is green and quiet, say so in one line.

Run a bare /loop. While it is waiting for its next iteration, edit the file.

✅ You pass when the next iteration reflects the edit, and you can say where the project file wins over the user file.

7. Prove session scope, and then check what comes back

Two halves, and the second one is the interesting half.

First: with a loop running, close the session. Reopen it. Nothing is firing.

Second: restart with claude --resume. Now ask what tasks you have.

✅ You pass when you can name which of your tasks came back and which did not — and specifically whether a self-paced loop returned.

Ordering, jitter, and the seven-day expiry are all version-gated, and the

jitter numbers in particular have been tuned more than once. Verified against

the scheduled tasks page on

2026-10-15. Check https://code.claude.com/docs/llms.txt before you promise

anyone a fire time.


Use It

NeedReach forWhy not the next thing
Poll a deploy for the next hour/loop 5m <prompt>Predictable cadence, and you are at the keyboard
Babysit a PR with irregular activity/loop <prompt>, no intervalClaude stretches the delay when nothing is happening
Leave a standing prompt for bare /loop.claude/loop.mdDefines the default, not a list of jobs
One reminderNatural languageA single-fire task that deletes itself
Something that must survive next weekA cloud routine, or Desktop tasks/loop expires in seven days and needs an open session
React to an event rather than pollingChannelsA push beats a timer for anything that has a real signal
Keep working until a condition holds/goalCondition-based persistence, not an interval
Stream a long script's outputThe Monitor toolAvoids polling altogether when one is available
See what is scheduledwhat scheduled tasks do I have?CronList under the hood, with IDs
Turn scheduling off entirelyCLAUDE_CODE_DISABLE_CRON=1Disables the tools and stops pending tasks

Exercises

  1. Set a fixed loop with an interval that does not map cleanly to cron — 7m — and read what Claude says about the interval it actually chose. ✅ You pass when you can name the cadence it picked and why yours was not representable.
  1. Run a self-paced loop for three iterations and record the delay and reason from each. Then change the thing it is watching — make CI go red, or leave a review comment — and record the next delay. ✅ You pass when you can point at the observation that changed the delay.
  1. Predict, in writing, which of your scheduled tasks survive a --resume and which do not. Then run it and compare. ✅ You pass when your prediction matches the docs and the observation, or your note explains the rule you had backwards.
  1. Take one job you currently poll by hand and decide, with reasons, between /loop, /goal, and a cloud routine. ✅ You pass when the deciding reason is about where the work runs and what has to be open — not about which command you already know.
  1. A task is due at 9:00. The session is idle at 9:00 and again at 10:00. For an hourly task, write down when it actually fires and how long that is after 9:00. ✅ You pass when your answer is inside the documented range and you can say which direction the shift goes.

Key Terms

TermWhat people sayWhat it actually means
/loop"a cron job"A session-scoped scheduled prompt. Fires between your turns while the session is open
scheduled task"the timer"A CronCreate entry with an 8-character ID, a 5-field cron schedule, and a prompt. Max 50 per session
self-paced loop"the smart one"A /loop with no interval. Claude chooses 1–60 minutes after each iteration and prints why
loop.md"the config"The default prompt for bare /loop. Project file wins over user file. Truncated past 25,000 bytes
catch-up"it'll run late"It will not. Missed fires collapse to one when the session next goes idle
jitter"the drift"A deliberate shift so sessions do not hit the API together. Recurring tasks run late; */5 runs 15s early
seven-day expiry"the limit"A recurring task fires a final time and deletes itself seven days after creation
Esc"stop the loop"Clears a self-paced loop's pending wakeup. Does not affect tasks you asked for directly
cloud routine"the durable one"A schedule that runs without your machine or session. Phase 17

What this lesson skipped

Cloud routines and Desktop scheduled tasks in depth. The comparison table above is the decision; the configuration is Phase 17, along with --schedule and the GitHub Actions path.

Channels. The event-driven answer to "stop polling". Lesson 83 shows where it sits next to hooks; the MCP contract behind it is Phase 17.

/goal in detail. Keeping Claude working toward a condition is a different mechanism with its own stopping rules — a model judges it impossible, or an error you must fix clears it. Worth knowing it exists so that you do not reach for a one-minute loop to express a condition.

The Monitor tool. When it is available, a self-paced loop may use it to stream a script's output instead of polling. That is a token-efficiency choice, and it is documented on the tools reference rather than here.

Cron expression syntax beyond five fields. L, W, ?, and name aliases are not supported. When both day-of-month and day-of-week are constrained, a date matches if either matches — standard vixie-cron behaviour, and a genuine trap if you are used to a different cron.

Check yourself

6 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.

  1. You run /loop 5m check the deploy, then close the laptop for 30 minutes. What did the loop do?

    1. Nothing — a scheduled task fires only while Claude Code is running, so closing the session stops it
    2. It fired six times and queued the output for when you reopened
    3. It kept running in the background, because scheduled tasks are independent of any session
    4. It fired six times, but discarded the output because there was no terminal attached
    Show answer

    A. Nothing — a scheduled task fires only while Claude Code is running, so closing the session stops it

    Scheduled tasks are session-scoped: they fire while Claude Code is running and idle, and closing the terminal stops them. The independent-of-any-session answer is the belief the opening incident is about, and it is the whole reason the table of three scheduling options exists — cloud routines are the ones that survive your machine being off. The queued and discarded variants both assume the fires happened at all, which is the thing being tested.

  2. An hourly task is due at 9:00. At 9:00 the session is in the middle of a long request that doesn't finish until 10:40. What happens?

    1. It fires once when the turn ends, not once per missed interval
    2. It fires twice as the turn ends, once for each missed hour
    3. It is skipped entirely, since its scheduled time has passed
    4. It interrupts the running request at the next safe point to fire on time
    Show answer

    A. It fires once when the turn ends, not once per missed interval

    There is no catch-up: a task whose time passed while Claude was busy fires once when the session next goes idle. The fires-twice answer is the natural expectation from cron, and it is the specific behaviour the docs rule out. Skipping entirely is the other half-truth — a one-shot task past its time is indeed not restored on resume, but a recurring task still gets its single fire. Nothing interrupts a running turn; the prompt waits for it.

  3. You run /loop 7m check the deploy. What does that interval become?

    1. A nearby cadence that maps to a clean cron step, with Claude telling you which it picked
    2. Exactly seven minutes, since cron accepts any minute value
    3. An error, because the interval does not divide evenly into an hour
    4. Five minutes, silently, because that is the default step
    Show answer

    A. A nearby cadence that maps to a clean cron step, with Claude telling you which it picked

    Intervals that do not map to a clean cron step are rounded to the nearest one that does, and Claude reports the choice. The 'cron accepts any minute value' answer is the trap for anyone who has written 7 before — that is a valid schedule, but this path converts an interval into a recurring step, and a step of seven minutes is not representable that way. Nothing errors, and the rounding is announced rather than silent.

  4. A self-paced loop is waiting for its next iteration, and you also have two tasks you scheduled by asking Claude directly. You press Esc. What is cancelled?

    1. The self-paced loop's pending wakeup only; your two explicit tasks stay
    2. Every scheduled task in the session, including the two explicit ones
    3. Nothing — Esc is a display key for the progress view
    4. The two explicit tasks, since Esc cancels the oldest first
    Show answer

    A. The self-paced loop's pending wakeup only; your two explicit tasks stay

    Esc clears the pending wakeup of a self-paced loop so it does not fire again, and the docs are explicit that tasks you scheduled by asking Claude directly are not affected. The cancel-everything answer is the plausible misreading, because a loop feels like one thing and Esc feels like a stop button. Esc is not a display key here, and cancellation is by task ID — there is no oldest-first ordering.

  5. Both .claude/loop.md and ~/.claude/loop.md exist, with different content. You run a bare /loop. What runs, and what is the file?

    1. The project file's prompt, which is a single default prompt for bare /loop rather than a list of tasks
    2. The user file's prompt, since personal configuration takes precedence
    3. Both prompts, concatenated, as two separate scheduled tasks
    4. One task per heading in the project file, since headings delimit jobs
    Show answer

    A. The project file's prompt, which is a single default prompt for bare /loop rather than a list of tasks

    The project file wins when both exist, and it defines one default prompt for a bare /loop. The user-file-wins answer is the usual precedence intuition from settings, which is exactly why the docs call out the opposite here. The file is not a job list: headings do not create tasks, and it is ignored entirely when you supply a prompt on the command line.

  6. You created a recurring task ten days ago and have kept the session alive through resumes. You resume again today. What do you find?

    1. The task is gone — it expired after seven days, having fired a final time
    2. The task is still listed and firing, since you never cancelled it
    3. The task is listed but will never fire again until you recreate it
    4. The task fires three times in a row to catch up the three missed days
    Show answer

    A. The task is gone — it expired after seven days, having fired a final time

    A recurring task expires seven days after creation: it fires one final time, then deletes itself, which bounds how long a forgotten loop can run. The still-listed answers describe other systems' behaviour and collide with the documented expiry. The catch-up answer is the same trap as question two and is ruled out twice over — there is no catch-up, and there is nothing left to catch up with.