"Run my agent every morning" sounds like one feature. In Claude Code it's three, and they fail in different ways. Pick the wrong one and your "nightly" job quietly stops firing the first time you close the terminal — or a week later, when it silently expires. Here's the decision, and the handful of gotchas that bite unattended jobs specifically.
The three mechanisms#
There is no single scheduler. There are three, and the right question is what has to be true for this to keep running.
/loopis session-scoped. It re-runs a prompt on an interval while the conversation stays open, and it's restored on--resumefor up to seven days. Close the terminal and it stops. This is a polling tool, not automation.- Routines run on Anthropic-managed cloud infrastructure. No machine on, no open session, persistent across restarts. The trade: a Routine works from a fresh clone with no access to your local files, and its minimum interval is one hour.
- Desktop scheduled tasks run on your machine with full local-file access, down to a one-minute interval — but the machine has to be on.
The one-line heuristic: /loop for quick polling inside a session, Routines for unattended work that must run with your laptop shut, Desktop tasks for local-file work that needs your own tools.
The scheduler you want is the one whose "requires" column you can actually guarantee at 3am. Everything else is a job that looks scheduled and isn't.
Driving /loop#
/loop is the fastest to reach for. Both the interval and the prompt are optional, and what you supply changes the behavior:
/loop 5m check if the deployment finished and tell me what happened
Give it an interval and a prompt and it runs on a fixed cadence — units are s, m, h, d, and the interval can lead as a bare 30m or trail as every 2 hours. Give it a prompt only and Claude picks the delay itself each iteration (short while a build is finishing, longer when things go quiet), printing the delay and its reasoning at the end of each pass. Give it nothing and it runs a built-in maintenance prompt — or your .claude/loop.md if you've written one.
You can also schedule a skill: /loop 20m /review-pr 1234 re-runs that skill each iteration. This pairs naturally with a forked skill — see how to run a Claude skill in the background — so the loop kicks off heavy, self-contained work and stays responsive.
The gotcha that eats scheduled skills#
Here's the one that produces a job which appears to run and accomplishes nothing. As of Claude Code v2.1.196, a scheduled fire only runs skills Claude is allowed to invoke on its own. Anything Claude can't auto-invoke reaches it as plain text instead of executing:
- Skills marked
disable-model-invocation: true— including the bundled/verifyand/code-review - Built-in commands like
/permissions,/model,/clear - Skills withheld by a
skillOverridessetting or aSkilldeny rule, and MCP prompts like/mcp__github__list_prs
So /loop 30m /code-review doesn't run a review on a schedule — it hands Claude the literal string /code-review and lets it decide. If your scheduled skill quietly does nothing, check this first.
Cron, jitter, and the seven-day cliff#
Under the hood, natural-language scheduling compiles to CronCreate with a standard 5-field expression — minute hour day-of-month month day-of-week — supporting , single values, steps (/15), ranges (1-5), and lists. Two things surprise people:
All times are local. 0 9 is 9am wherever you run Claude Code*, not UTC. Extended syntax (L, W, name aliases like MON) isn't supported.
Jitter is deterministic and can be large. To avoid every session hammering the API at the same wall-clock second, the scheduler offsets each fire by an amount derived from the task ID. A recurring hourly job scheduled for :00 can fire anywhere up to :30; one-shots at the top or bottom of the hour fire up to 90 seconds early. If exact timing matters, schedule an odd minute — 3 9 , not 0 9 — and the one-shot jitter won't apply.
And the cliff: recurring tasks expire seven days after creation. They fire one final time, then delete themselves — a deliberate bound on forgotten loops. There's also no catch-up: a fire due while Claude is mid-request runs once when it goes idle, not once per missed interval.
Which to pick#
If the job must survive a closed laptop, it cannot be /loop or a Desktop task — use a Routine (or a GitHub Actions schedule trigger) and accept the fresh-clone, no-local-files constraint. If it needs your local files and tools, it's a Desktop task, and you own keeping the machine awake. If you just want to babysit a deploy or a PR for the next hour, it's /loop — and press Esc to stop it. Match the mechanism to the "requires" you can guarantee, not to the sentence "every morning," and your scheduled agent will still be firing next week.



