scheduled-tasks

Installation
SKILL.md

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)

  1. Is the task recurring? If NO → use TaskCreate instead (cron is for recurring work only).
  2. Can it tolerate up to 15-minute jitter? If NO → use OS cron or GitHub Actions (CronCreate has built-in jitter).
  3. Is it acceptable to lose it on terminal close? If NO → use OS cron (crontab -e) or GitHub Actions for persistence.
  4. Will it complete within the session? If the session may end before the task fires → prefer OS-level scheduling.
  5. Is it for monitoring/health checks? If YES → CronCreate is the right tool (this is its primary use case).
  6. 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 Task or TaskCreate)
  • 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 CronCreate loops 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-orchestrator is responsible for detecting missing loops after session restarts and re-registering them. If you are not using heartbeat-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:

  1. Heartbeat fires at scheduled interval
  2. Read HEARTBEAT.md checklist (cheap file read — no LLM call)
  3. If blank/headers-only → emit HEARTBEAT_OK immediately, no LLM invocation
  4. Read cheap signals (file sizes, agent-health.json, task list) — still no LLM
  5. If ALL signals green → emit HEARTBEAT_OK
  6. 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):

  1. Agent health: Read agent-health.json — any agents status: degraded?
  2. Memory sizes: Is learnings.md > 40KB? Is decisions.md > 80KB?
  3. Stuck tasks: TaskList() — any tasks in in_progress for > 2 hours?
  4. Hook errors: Any hooks with errorRate > 0.05 in agent-health.json?
  5. Reflection queue: Does reflection-reminder.txt exist and is stalled?
  6. 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/TaskCreate instead)
  • 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 schedule trigger: 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

  1. ALWAYS include an auto-reschedule cron task when setting up heartbeats — the 3-day silent expiry will kill the heartbeat without warning
  2. NEVER use cron for tasks requiring time-critical precision — jitter up to 10% of period (max 15 min) makes timing non-deterministic
  3. ALWAYS use cheap file reads before LLM invocation in heartbeat prompts — 60-80% cost reduction for healthy systems
  4. NEVER exceed 10 concurrent scheduled tasks — leave headroom in the 50-task session cap for user-initiated tasks
  5. 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.

Weekly Installs
2
GitHub Stars
25
First Seen
Mar 22, 2026