Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Cron Framework — Subagent Cron Scheduler for Claude Code

Schedule Claude subagents to run recurring tasks. Each job spawns claude -p as a cold worker, scores every run by pattern-matching its log, and auto-resolves prior failures when jobs stabilise.

Part of The Agent Crafting Table — standalone agent system components for Claude Code.

Architecture

flowchart TD
    A([cron-runner.js\ndaemon starts]) --> B[Load jobs.json]
    B --> C{Hot-reload\nwatcher active}
    C -->|every 60s| D[tick]
    C -->|jobs.json\nchanged| B

    D --> E[Check trigger\nfiles]
    E -->|trigger file\nfound| F[Run job\nimmediately]
    E -->|no trigger| G[cronMatches?\nschedule + tz]
    G -->|no match| D
    G -->|match| H{preCommand\ndefined?}

    H -->|yes| I[Run preCommand\n15s timeout]
    I -->|exit 0| J[Generate\nbrief]
    I -->|non-zero\nexit| D
    H -->|no| J

    J --> K{runner type?}
    K -->|shell| L[Spawn /bin/sh\nwith shellCommand]
    K -->|model\ndefault| M[Spawn\nclaude -p]

    L --> N[Stream stdout\nto log file]
    M --> N
    N --> O{HEARTBEAT_OK\nin output?}
    O -->|yes| P[✅ success]
    O -->|no| Q[⚠️ partial / fail]
    P --> D
    Q --> D
Loading

What This Solves

  • Silent breakage — without structured scoring, broken cron jobs fail invisibly for weeks. The HEARTBEAT_OK convention + cron-score.js flips this.
  • Cold subagent problemclaude -p workers have no memory. cron-brief.js prepends a short context header so they know the date and what's in flight.
  • "Re-fixing" resolved failurescron-resolve.js marks failures as resolved after 2 consecutive successes so automated patchers don't re-propose the same fix forever.
  • Patch regressioncron-eval.js blocks any prompt patch that removes known success markers or doesn't address the target failure mode.

Files

src/
  cron-runner.js    # Scheduler daemon — ticks every minute, spawns claude -p
  cron-brief.js     # Context brief generator — prepended to every subagent prompt
  cron-score.js     # Log classifier — structured outcomes per run
  cron-resolve.js   # Auto-resolver — marks failures fixed after 2+ successes
  cron-eval.js      # Pre-flight gate — validates prompt patches before apply
examples/
  jobs.json         # Example job definitions
  cron-eval.json    # Example eval ruleset
  start-cron.sh     # Auto-restart wrapper
assets/
  architecture.md   # System design notes

Requirements

  • Node.js 18+
  • claude CLI on $PATH and authenticated (API key or Max subscription)
  • No database, no network services

Setup

1. Drop src/ into your project

All five scripts are standalone — no npm install needed.

2. Create jobs.json

mkdir -p crons
cp examples/jobs.json crons/jobs.json

Edit to add your jobs. Each job needs at minimum:

{
  "id": "my-daily-report",
  "name": "Daily Status Report",
  "enabled": true,
  "schedule": "0 9 * * *",
  "tz": "America/New_York",
  "timeoutSeconds": 300,
  "message": "Check the system status and write a brief report to memory/daily-notes/. Reply HEARTBEAT_OK when done."
}

Reply HEARTBEAT_OK when done. at the end of every prompt is the success signal. Jobs that don't include it will always score as partial/no_heartbeat.

3. Start the daemon

node src/cron-runner.js
# or use examples/start-cron.sh for auto-restart on crash
bash examples/start-cron.sh

4. (Optional) Wire scoring

Schedule a daily job that runs scoring + resolution:

node src/cron-score.js --days=1
node src/cron-resolve.js --apply

See the third entry in examples/jobs.json for how to wire this as a self-healing job.

Job Execution Pipeline

flowchart LR
    subgraph Gate["Pre-Spawn Gate"]
        direction TB
        P1{preCommand\nexits 0?}
        P2[Skip this tick]
        P1 -->|non-zero| P2
    end

    subgraph Brief["Context Brief"]
        direction TB
        B1{skipBrief?}
        B2[Prepend date,\nactive threads,\nminimalBoot flag]
        B1 -->|false| B2
        B1 -->|true| B3[Raw prompt only]
    end

    subgraph Spawn["Spawn Worker"]
        direction TB
        S1{runner}
        S2["claude -p\n--dangerously-skip-permissions"]
        S3["/bin/sh -c\nshellCommand"]
        S1 -->|model| S2
        S1 -->|shell| S3
    end

    Gate -->|exit 0| Brief
    Brief --> Spawn
    Spawn -->|stdout| LOG[(crons/logs/\nYYYY-MM-DD-id.log)]
Loading

Environment Variables

Variable Default Description
WORKSPACE_DIR cwd Root for jobs.json, crons/logs/, data/
CRON_TZ UTC Default timezone for jobs without explicit tz
CRON_MODEL sonnet Default Claude model for subagents
DISCORD_POST Path to a node <script> <channel> <message> helper for timeout/error alerts

Per-Job Fields

Field Required Description
id yes Stable slug or UUID. First 8 chars used as log filename prefix.
name yes Human label for logs.
enabled yes Boolean.
schedule yes 5-field cron (min hour dom month dow). Supports , - /.
tz no Per-job timezone override.
timeoutSeconds no Default 300. SIGTERM at timeout, SIGKILL +5s.
message yes Prompt handed to claude -p. End with Reply HEARTBEAT_OK when done.
model no Per-job model override (e.g. opus, haiku).
skipBrief no Skip context brief prepend.
discordChannel no Channel ID for error/timeout alerts via DISCORD_POST.
preCommand no Shell command to run before spawning. Non-zero exit skips the job silently. Use to gate jobs on external state ("only run when there's something to do").
runner no Set to "shell" for deterministic jobs that don't need a model. Spawns /bin/sh with shellCommand instead of claude -p.
shellCommand conditional Required when runner: "shell". The shell command to run.
minimalBoot no If true, prepends MINIMAL_BOOT_MODE: skip-l1 sentinel to the prompt. Pairs with a CLAUDE.md "Minimal-boot escape hatch" section that skips L1 file reads — saves ~5-10k tokens per run for one-shot mechanical jobs.

Token-Savings Patterns

The preCommand, runner: "shell", and minimalBoot fields exist for one reason: stop spinning Claude when you don't need to.

// Only fire when an external gate says yes — exit 0 from the gate runs the
// agent; exit 1 silently skips this tick. No model spin on empty checks.
{
  "id": "ci-fixer",
  "schedule": "*/10 * * * *",
  "preCommand": "scripts/gates/ci-failed.sh",
  "message": "Fix the failing CI run on staging. Reply HEARTBEAT_OK when done."
}

// Pure-shell job: no model spin at all. Use for log rotation, cleanup,
// metrics collection — anything where the logic is deterministic.
{
  "id": "log-rotate",
  "schedule": "0 3 * * *",
  "runner": "shell",
  "shellCommand": "find ./logs -mtime +30 -delete"
}

// Minimal-boot: skip identity/voice loading for one-shot mechanical jobs.
// Combine with skipBrief for jobs that don't need any context.
{
  "id": "metric-collect",
  "schedule": "0 * * * *",
  "minimalBoot": true,
  "skipBrief": true,
  "message": "Run `node scripts/collect-metrics.js` and confirm exit 0. Reply HEARTBEAT_OK when done."
}

--dry-run

node src/cron-runner.js --dry-run

Simulates one tick without spawning anything. Lists which jobs would fire (schedule match or trigger file present) and which are skipped because they're disabled. Useful for verifying schedule + timezone before going live.

Manual Trigger

Drop an empty file at crons/triggers/<jobId>.trigger — the daemon picks it up on next tick (within 60s), fires the job, and deletes the trigger file. No daemon restart needed.

Ad-hoc Single Run

node src/cron-runner.js --run my-daily-report

Fires once, blocks until the subagent exits, then exits. Does not start the scheduler.

Runtime Files (Created by the Framework)

These live under WORKSPACE_DIR and are not shipped:

  • crons/jobs.json — you author this
  • crons/logs/YYYY-MM-DD-<id-prefix>.log — per-run logs, append-only
  • crons/triggers/ — manual trigger drop zone
  • crons/cron-runner.pid — daemon lock file (auto-cleared if owner PID is dead)
  • data/cron-performance.json — rolling 30-run scoring window per job
  • data/running-jobs.json — currently-running jobs with startedAt

Scoring & Self-Healing Loop

cron-score.js reads logs and classifies each run:

  • success — log contains HEARTBEAT_OK
  • partial — ran but no success marker (e.g. timed out mid-task)
  • fail — exit non-zero, spawn error, or matched a known failure pattern

Failure modes (e.g. timeout, module_not_found, script_error) are matched from log content.

flowchart TD
    L[(crons/logs/)] -->|read recent runs| SC[cron-score.js]
    SC -->|classify each run| PF[(data/cron-\nperformance.json)]

    PF -->|rolling 30-run\nwindow per job| RS[cron-resolve.js]
    RS --> RQ{2+ consecutive\nsuccesses since\nlast failure?}
    RQ -->|yes| MR[Mark failure\nas resolved]
    RQ -->|no| KEEP[Leave open]

    PF -->|failure detected| PA[Patch agent\nreads performance.json]
    PA --> PROP[Proposes prompt\npatch]
    PROP --> EV[cron-eval.js\nvalidates patch]
    EV --> EQ{Eval result}
    EQ -->|exit 0 — passes| APPLY[Apply patch\nto jobs.json]
    EQ -->|exit 1 — blocked| BLOCK[Reject: removes\nsuccess markers or\ndoesn't fix failure]
    EQ -->|exit 2 — skip| SKIP[Skip: unrelated\nto this failure]
    APPLY --> L
Loading

Safety Notes

  • cron-runner.js spawns claude --dangerously-skip-permissions. Only schedule jobs whose prompts come from trusted sources you control.
  • The subagent runs with the same filesystem access as the cron daemon. Use a dedicated low-privilege OS user, a container with bind-mounted workspace, or a restricted WORKSPACE_DIR to limit blast radius.
  • Logs are plain text and may contain sensitive output. Restrict crons/logs/ with chmod 700 if needed.

About

Subagent cron scheduler for Claude Code — schedule, score, auto-resolve

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages