Guides · how-to

Run a deterministic cron agent by scheduling a recorded replay

▸ What you'll do

A scheduled agent that re-reasons every wake-up pays the model bill on every single tick — the heartbeat tax. A deterministic cron agent inverts that: instead of re-deriving the workflow, the schedule fires a replay of a run that already worked. The deterministic core replays at 0 tokens, and reelier diff turns each scheduled run into a standing, dated gate that exits non-zero the moment the upstream drifts.

This guide wires that up two ways with the reelier CLI: on GitHub Actions cron and on plain system cron. Every command below is real and every output is verbatim from an actual run on 2026-07-22.

Before you start

  • Reelier installed — npm i -g reelier (or use npx -y reelier@latest in place of reelier in every command below).
  • One compiled, read-only SKILL.md you want to run on a schedule. Don't have one yet? Compile it first — a scheduled job runs a SKILL.md file, never a prompt.
  • For the GitHub Actions path: the skill committed to a repo. For system cron: a machine with cron and Node.

Pick a skill that's safe to run unattended

The thing you schedule is a compiled SKILL.md, and the one property that matters for an unattended schedule is: it must be read-only. Nobody is watching a 3 a.m. cron run, so it must not re-fire writes. Reelier replay is read-only by default — a recorded write step never re-executes unless you pass --allow-writes (which unlocks idempotent writes only; a destructive step still needs --yes). On a schedule, you pass neither.

reelier from-session (and reelier scan) tell you this up front — look for ✓ all N steps are read-only — safe to replay repeatedly. If it warns that steps are side-effectful instead, that skill is a poor scheduling target; pick a read-only data-pull or health check. (Only about 1.1% of scanned sessions are read-only end-to-end, so this is also where you learn which workflows are schedulable at all.)

How you produce the SKILL.md from a recorded session is its own how-to; this guide picks up once you have one.

terminal
reelier from-session daily-health-check.jsonl
Reelier · output
Scanned daily-health-check.jsonl: 1 replayable call(s) found.


Wrote daily-health-check.skill.md
  steps:   1
  asserts: 1
  binds:   0

✓ all 1 steps are read-only — safe to replay repeatedly

Open questions (1):
  - Step 1: no narration — describe what this step is for.
verbatim output

Prove the replay locally before you schedule it

Run it once by hand. reelier run re-executes the recorded tool calls and checks each assertion. At the default --max-level 0, an LLM client is never even constructed — so the deterministic core replays at 0 tokens, and the run prints no LLM tokens line precisely because there were none to report.

That 0 is the deterministic core only — a step that needs judgment needs a model, the opt-in --max-level 1|2 BYOK escalation. You never escalate an unattended job; keep the schedule at Level 0: same steps, every run, no bill.

The compiler wrote one assertion here — status == 200, the clean recorded result — and it asserts a shape, not a brittle value pin, so a fresh response body on each honest run does not count as drift.

terminal
reelier run daily-health-check.skill.md
Reelier · output
✓ Step 1 — Call http.get [passed] 163ms

PASSED: 1/1 steps ok, 0 failed, 163ms total
verbatim output

Make the scheduled run a gate: reelier diff

A cron job that only runs the skill tells you it ran — not whether it still does the same thing. Run the skill a second time, then reelier diff <name> compares the last two runs in .reelier/runs/<name>.jsonl and reports SAME (exit 0) or DRIFTED (exit 1), step by step. That exit code is the whole point: it is what upgrades a scheduled replay from a heartbeat into a standing gate.

On an ordinary day the verdict is SAME and the schedule stays green. On the day the upstream changes — a field disappears, a 200 becomes a 500 — the diff flips to DRIFTED, exits 1, and whatever scheduled it surfaces the failure instead of a false green.

terminal
reelier diff daily-health-check
Reelier · output
  = Step 1 "Call http.get": passed, unchanged.

✓ SAME — 1 step(s), every outcome and escalation level identical to the baseline.
verbatim output

Schedule it on GitHub Actions cron

The only cron-specific line is the trigger. Add a schedule block with a cron expression — - cron: "0 8 * * *" runs daily at 08:00 (GitHub cron is always UTC) — and keep a workflow_dispatch: {} alongside it for a manual "Run workflow" button. The job itself is the published action: uses: seldonframe/reelier@v1 with skill: pointed at your .skill.md and max-level: "0" for the 0-token deterministic replay.

The action sets the job's pass/fail from the replay's own exit code, so a red check on a public repo is a genuine, dated drift signal. The full workflow anatomy — job summary, receipt push, the matrix pattern for several skills — is the CI guide's job; wire the schedule here and hand off there.

Or schedule it on plain system cron

No CI runner? A single crontab entry (crontab -e) runs the same two commands on the same schedule. Chain them with && so the diff gate only runs after a passing replay — then any non-zero exit, whether the replay failed or diff found drift, is your one signal to act on. Pipe that into your alerting (|| curl ...your-webhook...) so a drift pages you instead of dying silently in the cron log.

cron sends stdout and stderr to the crontab user's mail spool (or syslog) by default, so the receipt and any DRIFTED verdict land there unless you redirect them somewhere you'll see. Same two reelier commands as the CI path — only the scheduler differs.

terminal
0 8 * * * cd /srv/agent-checks && npx -y reelier@latest run daily-health-check.skill.md && npx -y reelier@latest diff daily-health-check

The receipt

Reelier · replay receipt
$ reelier run daily-health-check.skill.md
✓ Step 1 — Call http.get [passed] 163ms

PASSED: 1/1 steps ok, 0 failed, 163ms total

$ reelier run daily-health-check.skill.md
✓ Step 1 — Call http.get [passed] 137ms

PASSED: 1/1 steps ok, 0 failed, 137ms total

$ reelier diff daily-health-check
  = Step 1 "Call http.get": passed, unchanged.

✓ SAME — 1 step(s), every outcome and escalation level identical to the baseline.
verbatim output

What each scheduled tick runs, verbatim: the compiled daily-health-check skill replayed twice, then diffed. Produced on 2026-07-22 with reelier v0.14.0 in a scratch directory; the skill was compiled by reelier from-session from a one-call Claude Code-style transcript (a single http.get to the npm registry, shaped per reelier/test/session.test.ts). No model was constructed at any point; reelier diff exited 0 (a drift would exit 1 and fail the schedule).

FAQ

Doesn't running the agent on a cron still cost tokens every time?

Not the deterministic core. At --max-level 0 (the default) no LLM client is constructed, so a scheduled replay is 0 tokens — the run prints no token line because there were none. Judgment steps need a model, but that is the opt-in --max-level 1|2 BYOK escalation, which you don't turn on for an unattended job.

What happens on the day the upstream actually changes?

reelier diff flips from SAME to DRIFTED and exits 1, so GitHub Actions or cron surfaces a failure instead of a silent green. Update the baseline: edit the failing assertion in the SKILL.md (it's markdown in your repo), or re-record and recompile — Reelier only builds steps from a run that actually happened, never from instruction text.

Can I schedule a skill that writes data?

Replay is read-only by default, so recorded write steps do not re-fire on a schedule unless you pass --allow-writes (idempotent writes only; a destructive step still needs --yes). For an unattended cron, prefer a read-only skill and pass neither flag — reelier from-session/scan tell you which of your runs are read-only up front.

Reading as an agent? This page has a markdown twin: /guides/deterministic-cron-agents/md