Scheduled Tasks & Heartbeat Skill
Official docs: https://code.claude.com/docs/en/scheduled-tasks
Primary Interface: /loop Slash Command
The primary user-facing way to invoke scheduled tasks is via the /loop command. This is the
recommended interface for most users.
Syntax
/loop <interval> <prompt> # interval-based
/loop every 5m <prompt> # explicit "every" syntax
/loop 2h <prompt> # shorthand hours
/loop at 8:00am <prompt> # time-based (daily at local time)
Common Intervals
2m, 5m, 10m, 15m, 30m # minutes
1h, 2h, 4h, 6h, 12h # hours
at 8:00am, at 3:00pm # daily time triggers (local timezone)
Examples
/loop 5m Check for new Telegram messages and reply
/loop 2h Distill recent session learnings into .claude/context/memory/learnings.md
/loop at 8:00am Read issues.md and CHANGELOG.md and give morning briefing
/loop every 4h Run pnpm search:code "indexing" to refresh codebase index
/loop 30m Heartbeat: read agent-health.json, check memory sizes, TaskList for stuck tasks
Chaining: Run a Slash Command on a Schedule
/loop 20m /review-pr 1234 # run a slash command on a schedule
/loop 2h /heartbeat-start # restart heartbeat ecosystem every 2 hours
Key Behaviors
| Behavior | Detail |
| ---------------------- | -------------------------------------------------------------------------------- |
| Jitter | Tasks fire up to 10% of their period late (max 15 min) — avoids stampedes |
| No catch-up | Missed fires are NOT replayed after sleep/hibernation |
| Session-scoped | Loops stop automatically when the Claude Code session ends |
| Local timezone | at HH:MMam/pm triggers use local time, not UTC |
| Max 50 loops | Hard cap of 50 concurrent scheduled tasks per session |
| 3-day expiry | All loops silently self-delete 3 days after creation (reschedule before day 2.5) |
| Fire-between-turns | Tasks fire only when Claude is idle, not mid-response |
Heartbeat OS Pattern
Use /loop to build a self-improving agent ecosystem that runs continuously in the background:
Continuous Reflection (every 2h)
/loop 2h Read .claude/context/memory/issues.md and recent session context. Extract patterns, workarounds, and decisions. Append to learnings.md and decisions.md.
Autonomous Evolution (daily at 3am)
/loop at 3:00am Review learnings.md and test failures. Use agent-updater skill to improve agent .md files. Run pnpm validate:full before finalizing.
Morning Briefing (weekdays at 8am)
/loop at 8:00am Read CHANGELOG.md and issues.md. Summarize technical debt, flaky tests, and top 2 tasks for today.
Context Drain (every 15m)
/loop 15m Check if all tasks are done. If TaskList shows zero in_progress tasks, summarize session to memory and surface readiness for /clear to user.
Codebase Indexing (every 4h)
/loop 4h Run pnpm code:index:reindex to refresh the BM25 and semantic search index.
Start the Full Heartbeat Ecosystem
Use /heartbeat-start to launch all 7 loops in one command. See .claude/commands/heartbeat-start.md.
Overview
Provides patterns for scheduling recurring tasks using Claude Code's built-in cron system (CronCreate/CronList/CronDelete). Also implements the Agentic Heartbeat Pattern for keeping the agent ecosystem healthy.
Core distinction:
- Cron tasks: "Run this SPECIFIC task at 3am Tuesday" — deterministic, time-anchored maintenance
- Heartbeat: "Check in periodically; act IF something needs attention" — awareness-based, conditional liveness
Decision Framework: When to Use Cron
Quick Decision Checklist (run through before calling CronCreate)
- Is the task recurring? If NO → use
TaskCreateinstead (cron is for recurring work only). - Can it tolerate up to 15-minute jitter? If NO → use OS cron or GitHub Actions (CronCreate has built-in jitter).
- Is it acceptable to lose it on terminal close? If NO → use OS cron (
crontab -e) or GitHub Actions for persistence. - Will it complete within the session? If the session may end before the task fires → prefer OS-level scheduling.
- Is it for monitoring/health checks? If YES → CronCreate is the right tool (this is its primary use case).
- Are there 10+ other cron tasks already scheduled? If YES → reconsider; stay under 10 concurrent tasks (50-cap headroom).
Decision Tree
Need a recurring task?
├── NO → Use TaskCreate (one-shot) or Task()
└── YES
├── Must survive terminal close?
│ ├── YES → Use OS cron (crontab -e) or GitHub Actions
│ └── NO (session-scoped is fine)
│ ├── Time-critical (< 15 min precision required)?
│ │ ├── YES → Use OS cron or GitHub Actions
│ │ └── NO → CronCreate is appropriate ✓
│ └── Is it monitoring/heartbeat?
│ └── YES → CronCreate is ideal ✓
When to Use CronCreate
- Session-scoped heartbeat monitoring (agent health, memory sizes, stuck tasks)
- Periodic index rebuilds during an active work session
- Scheduled memory rotation checks while actively developing
- Auto-reschedule tasks that keep other cron tasks alive
- Morning briefings and context-drain checks during sessions
When to Use OS Cron / GitHub Actions Instead
- Tasks that must run even when Claude Code is not open
- Production scheduled jobs (backups, deployments, data pipelines)
- Tasks requiring sub-minute precision or hard time guarantees
- Any task where silent expiry (3-day limit) is unacceptable
When NOT to Use CronCreate
- One-time tasks (use
TaskorTaskCreate) - Tasks triggered by events rather than time (use hooks or event listeners)
- Tasks requiring external system persistence across Claude restarts
- Time-critical alerts where 15-minute jitter is unacceptable
Session-Scope Context Note
CRITICAL — Session-Scoped Loops: All
CronCreateloops are registered to the current Claude Code session and are permanently lost when the session ends (terminal close,/clear, or session restart). After any session restart, all loops must be re-registered manually.heartbeat-orchestrator handles this automatically. When the heartbeat ecosystem is running,
heartbeat-orchestratoris responsible for detecting missing loops after session restarts and re-registering them. If you are not usingheartbeat-orchestrator, you must re-register all loops yourself at the start of each new session.To check which loops are active:
CronList(). If loops are missing after a restart, re-run the setup commands or invoke/heartbeat-start.
Quick Reference
// Schedule a recurring task
CronCreate({ schedule: '*/30 * * * *', task: 'heartbeat check prompt' });
// List active cron tasks
CronList();
// Remove a scheduled task
CronDelete({ id: 'abc12345' });
Constraints (CRITICAL — Read Before Using)
| Constraint | Detail |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| Session-scoped | All cron tasks die when the terminal closes — no persistence across restarts |
| 50-task cap | Maximum 50 concurrent scheduled tasks per session |
| 3-day auto-expiry | Recurring tasks self-delete 3 days after creation (SILENT — must reschedule before day 2.5) |
| Fire-between-turns | Tasks fire only when Claude is idle, not mid-response |
| No catch-up | Missed fires are NOT replayed — task fires once at next idle opportunity |
| Jitter | Tasks fire up to 10% of their period late (max 15 min) to avoid API stampedes |
| Timezone | All cron expressions use local system timezone, NOT UTC |
| No extended syntax | L, W, ?, name aliases like MON are NOT supported |
Cron Expression Reference
Standard 5-field syntax: minute hour day-of-month month day-of-week
| Expression | Meaning |
| -------------- | ------------------------ |
| */30 * * * * | Every 30 minutes |
| */5 * * * * | Every 5 minutes |
| 0 * * * * | Every hour on the hour |
| 0 2 * * * | Every day at 2am local |
| 0 3 * * 0 | Every Sunday at 3am |
| 0 6 * * 1-5 | Weekdays at 6am |
| 0 0 */2 * * | Every 2 days at midnight |
Heartbeat Pattern
Design Philosophy (OpenClaw-inspired)
Cheap file reads FIRST, LLM only when needed. Achieve 60-80% cost reduction for healthy-state ticks.
Execution model:
- Heartbeat fires at scheduled interval
- Read
HEARTBEAT.mdchecklist (cheap file read — no LLM call) - If blank/headers-only → emit
HEARTBEAT_OKimmediately, no LLM invocation - Read cheap signals (file sizes,
agent-health.json, task list) — still no LLM - If ALL signals green → emit
HEARTBEAT_OK - Only if signals indicate issues → invoke LLM with full HEARTBEAT.md context
Heartbeat Checklist
Check these signals on each heartbeat tick (in order — cheapest first):
- Agent health: Read
agent-health.json— any agentsstatus: degraded? - Memory sizes: Is
learnings.md> 40KB? Isdecisions.md> 80KB? - Stuck tasks:
TaskList()— any tasks inin_progressfor > 2 hours? - Hook errors: Any hooks with
errorRate > 0.05inagent-health.json? - Reflection queue: Does
reflection-reminder.txtexist and is stalled? - BM25 freshness: Is index mtime > 24 hours?
If ALL healthy → return HEARTBEAT_OK (no further LLM invocation needed)
If ANY unhealthy → spawn appropriate agent to fix, then report
HEARTBEAT.md Template
Create HEARTBEAT.md in the project root as standing instructions for the heartbeat:
# Agent Studio Heartbeat Checklist
## Every Tick (30 minutes)
### Memory Health
- [ ] learnings.md > 40KB? → trigger rotation via node .claude/lib/memory/memory-rotator.cjs
- [ ] decisions.md > 80KB? → warn user
- [ ] issues.md has unresolved P0 items? → surface to user
### Agent Registry Health
- [ ] agent-health.json has any status: degraded? → alert
- [ ] Any hooks have errorRate > 5%? → alert with hook name
### Task Health
- [ ] Any tasks stuck in in_progress for > 2 hours? → surface task IDs
### Reflection Queue
- [ ] reflection-reminder.txt exists? → alert (reflection queue stalled)
## Self-Maintenance
- [ ] This heartbeat task older than 2.5 days? → reschedule before 3-day expiry
Scheduled Maintenance Patterns
Index Rebuild (nightly)
CronCreate({
schedule: '0 2 * * *',
task: 'Rebuild BM25 search index for code discovery. Run: cd /project && pnpm code:index:reindex',
});
Memory Rotation Check (weekly)
CronCreate({
schedule: '0 3 * * 0',
task: 'Check memory file sizes and rotate if needed. Run node .claude/lib/memory/memory-rotator.cjs and report results.',
});
Agent Registry Refresh (daily)
CronCreate({
schedule: '0 6 * * *',
task: 'Regenerate agent registry: pnpm agents:registry and verify 72 agents are registered.',
});
Heartbeat (every 30 minutes)
CronCreate({
schedule: '*/30 * * * *',
task: 'Run heartbeat check: Read HEARTBEAT.md checklist, check agent-health.json, memory file sizes, and stuck tasks. Reply HEARTBEAT_OK if all healthy, otherwise describe issues found.',
});
Auto-Reschedule (every 2 days — prevents 3-day silent expiry)
CronCreate({
schedule: '0 0 */2 * *',
task: 'Re-register all scheduled tasks to prevent 3-day auto-expiry. Call CronList() to check what tasks exist, then recreate any that are missing or older than 2.5 days.',
});
Reschedule a Specific Task (correct order — no scheduling gap)
When replacing a running cron task with updated settings, ALWAYS use CronCreate FIRST, then CronDelete. This prevents a scheduling gap where no cron task exists between delete and create.
// CORRECT ORDER: Create new before deleting old to prevent scheduling gap
// Step 1: Create the new task FIRST (new task is now running)
CronCreate({
schedule: '*/30 * * * *',
task: 'Updated heartbeat prompt with new checks.',
});
// Step 2: THEN delete the old task (old ID obtained from CronList())
// There is never a window where no heartbeat cron exists
CronDelete({ id: 'old-task-id-from-cronlist' });
// WRONG ORDER (creates a scheduling gap — do NOT do this):
// CronDelete({ id: "old-id" }); // <-- gap begins here
// CronCreate({ ... }); // <-- gap ends here (tasks missed during gap)
Weekly Validation (every Sunday)
CronCreate({
schedule: '0 3 * * 0',
task: 'Run full framework validation: pnpm validate:full and report any errors found.',
});
Integration Points
| Infrastructure | Integration Method | Heartbeat Action |
| -------------------- | ------------------------------------------------------- | ------------------------------------ |
| agent-health.json | Read tool → JSON parse | Check status, errorRate per hook |
| learnings.md | Bash: wc -c or Read + check size | Alert if > 40KB threshold |
| decisions.md | Bash: wc -c or Read + check size | Alert if > 80KB threshold |
| Task system | TaskList() | Surface stuck in_progress tasks |
| Memory rotator | Bash: node .claude/lib/memory/memory-rotator.cjs | Trigger when threshold exceeded |
| BM25 index | Check index mtime | Rebuild if mtime > 24h |
| pnpm validate:full | Bash | Weekly validation, surface errors |
| Reflection queue | Read: .claude/context/runtime/reflection-reminder.txt | Alert if reflection queue stalled |
Cost & Performance Controls
| Control | Purpose | Recommendation |
| ------------------------- | ----------------------------------- | ------------------------- |
| Cheap checks first | Read files before invoking LLM | Always check files first |
| HEARTBEAT_OK early exit | No LLM when all healthy | Saves 60-80% API costs |
| haiku model | Cheaper model for status-only runs | Use haiku for heartbeats |
| lightContext | Only HEARTBEAT.md, not full session | Reduces context per tick |
| 30m default interval | OpenClaw proven default | Don't go below 15 minutes |
Anti-Patterns
- Do NOT use cron for one-time tasks (use
Task/TaskCreateinstead) - Do NOT schedule more than 10 concurrent cron tasks (leave room for user tasks in the 50-cap)
- Do NOT mix heartbeat and maintenance in the same cron task — keep them separate
- ALWAYS include auto-reschedule task to prevent silent 3-day expiry
- Do NOT rely on heartbeat for time-critical alerts — jitter means up to 15 min delay
- Do NOT set interval below 5 minutes — creates API stampede risk
- NEVER use extended cron syntax (
L,W,?, named days) — not supported
Usage Examples
Start full heartbeat monitoring
Skill({ skill: 'scheduled-tasks' });
// 1. Heartbeat every 30 minutes
CronCreate({
schedule: '*/30 * * * *',
task: 'Heartbeat: Read .claude/context/memory/agent-health.json, check learnings.md size, run TaskList() for stuck tasks. Reply HEARTBEAT_OK if all healthy.',
});
// 2. Auto-reschedule every 2 days (prevents 3-day expiry)
CronCreate({
schedule: '0 0 */2 * *',
task: 'Self-maintenance: CronList() to check active tasks, recreate any missing scheduled tasks.',
});
// 3. Nightly index rebuild
CronCreate({
schedule: '0 2 * * *',
task: 'Rebuild search index: pnpm code:index:reindex',
});
Check scheduled task health
CronList(); // Verify all expected tasks are registered
// If missing, re-register (3-day expiry may have cleared them silently)
Cancel a specific task
CronList(); // Get task IDs
CronDelete({ id: 'abc12345' }); // Cancel by ID
Durable Alternatives (When Session-Scope Is Not Enough)
For tasks that must survive terminal close:
- Desktop scheduled tasks (OS-level): survives terminal close, graphical setup
- GitHub Actions
scheduletrigger: fully unattended, cloud-run, no terminal required - OS cron (
crontab -e): persistent, runs independently of Claude Code
Use Claude Code's CronCreate for session-scoped monitoring only. Use OS-level scheduling for critical production tasks.
Iron Laws
- ALWAYS include an auto-reschedule cron task when setting up heartbeats — the 3-day silent expiry will kill the heartbeat without warning
- NEVER use cron for tasks requiring time-critical precision — jitter up to 10% of period (max 15 min) makes timing non-deterministic
- ALWAYS use cheap file reads before LLM invocation in heartbeat prompts — 60-80% cost reduction for healthy systems
- NEVER exceed 10 concurrent scheduled tasks — leave headroom in the 50-task session cap for user-initiated tasks
- ALWAYS document that cron tasks are session-scoped and will not survive terminal close — this surprises users who expect persistence
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
| ------------------------------ | ------------------------------------- | ----------------------------------------------- |
| No auto-reschedule task | 3-day expiry silently kills heartbeat | Add 0 0 */2 * * reschedule cron |
| Heartbeat interval < 5 minutes | API stampede risk, burns token budget | Use 30 min default; minimum 5 min |
| LLM invocation on every tick | 100% API cost with no savings | Cheap checks first, LLM only when needed |
| Mixing heartbeat + maintenance | One failure breaks both | Separate cron tasks per concern |
| Extended cron syntax | Not supported — silent failure | Use standard 5-field syntax only |
| Assuming session persistence | Tasks die on terminal close | Document limitation; use OS cron for durability |
Memory Protocol (MANDATORY)
Before starting:
Read .claude/context/memory/learnings.md
After completing:
- New scheduling pattern →
.claude/context/memory/learnings.md - Issue found →
.claude/context/memory/issues.md - Decision made →
.claude/context/memory/decisions.md
ASSUME INTERRUPTION: If it's not in memory, it didn't happen.