Pause and Resume Hermes Cron Jobs Without Deleting History
Why `hermes cron pause <id>` and `hermes cron resume <id>` beat `remove` for three operator cases. `paused_at` is added; prompt and history are preserved. Verified against 35 paused jobs on this VPS.

The instinct when a cron is misbehaving is to delete it and rewrite it. Wrong. hermes cron pause <job_id> is the right move for three out of four cases — it stops firing without losing the prompt body, the paused_at audit trail, or the output history under /opt/data/cron/output/<job_id>/.
The two commands, side by side
Both live on the top-level hermes cron subcommand. Live output on Hermes v0.18.2 (2026.7.7.2):
usage: hermes cron pause [-h] job_id
usage: hermes cron resume [-h] job_id
That is the entire surface — one positional <job_id> (12-char hex prefix from hermes cron list), no flags, no prompt edits, no schedule changes. Pause and resume are pure state flips.
|| # | Command | What flips | What stays |
||---|---|---|---|
|| 1 | hermes cron pause <job_id> | state: scheduled → paused; sets paused_at; sets enabled: false | The row, the prompt body, the schedule, deliver, all attached skills, all run history |
|| 2 | hermes cron resume <job_id> | state: paused → scheduled; clears paused_at; sets enabled: true | Everything else — the pause is invisible to the prompt |
|| 3 | hermes cron remove <job_id> | Deletes the row entirely. | Nothing in jobs.json; output files persist under /opt/data/cron/output/<job_id>/ but are orphaned |
The third row is the destructive primitive. Pause and resume are reversible state transitions; remove is not.
What pause actually changes
The row for a paused job grows exactly two fields:
active: { state: "scheduled", enabled: true, ... }
paused: { state: "paused", enabled: false,
paused_at: "2026-06-12T05:55:11.956792+00:00",
paused_reason: "..." (optional), ... }
The new fields are paused_at (UTC, immutable until resume) and optional paused_reason (free text). The prompt body, schedule, delivery target, model pin, skill attachments — none of those move. The prompt is byte-identical before and after a pause. diff the row’s prompt field across a pause and the diff is empty.
What stays vs what pauses
Three categories of state interact with a pause, and only the first one changes:
1. Firing stops. The scheduler will not pick up the job on the next tick. next_run_at is no longer advanced. No new files appear under /opt/data/cron/output/<job_id>/. No tokens are spent on agent-based jobs. This is the entire point — pause is the off-switch.
2. History is preserved. Every file the job ever wrote is still on disk: /opt/data/cron/output/<job_id>/<timestamp>.md (per-run output), /opt/data/cron/jobs.json (the row with paused_at), any cron-state files the script maintains, and all Telegram / Discord / Signal messages
3. The job is auditable. paused_at (and optional paused_reason) tell a future operator when and why — distinguishing “paused on purpose” from “broken and forgotten.”
A pause does not:
- Delete or rename any file (history preserved)
- Modify the prompt, schedule, model, or delivery target
- Stop a
no_agent: truescript from being runnable by other means (watchdogs, manualhermes cron run, second scheduler) - Hide the job from
hermes cron list(paused jobs show by default)
The last point is the P33 caveat from cron-health-monitor: the scheduler’s pause flag is not always load-bearing. If anything else fires the script directly, it runs anyway. Pause controls the scheduler, not the script. For jobs that must not fire while paused, add a script-level kill switch — see the end.
The three cases where pause beats remove
Case 1 — upstream service is broken
A watchdog restarts cloudflared every five minutes. Cloudflare is down. The watchdog is in a hot loop of failing probes, sending Telegram alerts, burning tokens. pause, not remove — you will want the watchdog back the moment Cloudflare recovers.
hermes cron pause <watchdog-id>
# ... Cloudflare recovers ...
hermes cron resume <watchdog-id>
When the upstream returns, resume flips the watchdog to scheduled. The next tick fires the probe, the probe returns 200, the watchdog goes silent. Prompt, schedule, alert recipient, script wrapper — all untouched. With remove, you would have to re-author the prompt from scratch, recompute the cron expression, re-attach skills.
On this VPS as of 2026-07-14, 35 of 68 cron jobs are paused — Dashboard, Cloudflared, Mockup Studio, blog-skill, and others. Their upstreams are intentionally down (cleanup, role-reframe). They are not broken; they are paused on purpose, with paused_at recorded for future resumption.
Case 2 — role-attribute cleanup or temporary role change
Hermes handles VPS infrastructure; Marvin handles print-on-demand store operations. A role-reframe is moving caption-generation, post-scheduling, and store-batch jobs from Hermes to Marvin. You have candidates but don’t know which to migrate yet. The temptation is remove. The right move is pause.
When Marvin is ready, read each paused row’s prompt verbatim, copy the behavior into Marvin’s queue, then remove the Hermes job once verified. If the decision reverses, resume brings them back instantly. With remove + recreate, you retype the prompt from memory, with high drift risk.
The 4-step gate in cron-health-monitor: 3+ jobs paused at the same timestamp (~30s window) is a “VPS-only mode” signal — paused together for a reason; treat as off-limits until told otherwise.
Case 3 — prompt needs editing but you want a fallback
You wrote a cron six months ago. It fires reliably. You want to rewrite the prompt (a path changed, a new failure shape, a model upgrade). The wrong move is edit --prompt "new version" directly — if the new prompt has a bug, you restore from memory. The right move: pause, dry-run the new prompt with hermes cron run (pause does not block dry-runs), then edit --prompt and resume.
Pause gives you a safety net. If the new prompt misbehaves, edit --prompt back to the old version and resume. With remove + recreate, the old prompt is gone.
When remove IS the right move
Pause is the default. remove is for jobs that are truly dead:
- Permanently replaced by a new job. Pause the old one until verified, then
remove. - The prompt was a one-off experiment.
- Data dependencies decommissioned (closed store, deleted account).
- Broken for months; salvageable parts already copied elsewhere.
The asymmetric cost: pause is one keystroke from resume; remove requires a full re-author. When in doubt, pause.
Verification checklist
hermes cron list | grep -A1 '<job-id>'
# Expect: [paused] tag, next_run_at absent or frozen, last_run_at unchanged
python3 -c "import json; j=json.load(open('/opt/data/cron/jobs.json'))['jobs']; \
x=next(r for r in j if r['id'].startswith('<id>')); \
print(f\"state={x['state']} paused_at={x.get('paused_at','<missing>')}\")"
python3 -c "import json; j=json.load(open('/opt/data/cron/jobs.json'))['jobs']; \
x=next(r for r in j if r['id'].startswith('<id>')); \
print(len(x['prompt']), 'chars in prompt; first 80:', x['prompt'][:80])"
ls -la /opt/data/cron/output/<job_id>/ | tail -5
# The newest file's mtime must be BEFORE paused_at.
If state did not flip, re-run with the full 12-char ID. If paused_at is missing, the pause silently failed — check hermes cron status. If new output files appeared after paused_at, see the P33 caveat: the scheduler paused but another path fired the job.
The P33 caveat: paused-but-ran
Pause is a scheduler-level flag. The script the cron invokes has no awareness of the paused state. If anything else fires the script — a watchdog, a manual hermes cron run, a direct operator invocation — it runs anyway. P33 verified incident 2026-07-10: a cron paused for six days produced output at three timestamps because the script had no internal pause check.
Two reliable defenses:
# Approach 1 — env-var gate at script startup (recommended)
import os
if os.environ.get("MY_JOB_DISABLED", "0") in ("1", "true", "yes"):
print("MY_JOB disabled via env var; exiting cleanly.")
sys.exit(0)
# Approach 2 — sentinel file at a known path
from pathlib import Path
if Path("/etc/my-job.disabled").exists():
sys.exit(0)
For multi-scheduler setups, Approach 1 is the durable fix: pause the scheduler and set the env var.
What NOT to do
- Do not use
removewhen pause fits.removedeletespaused_atand the prompt body; recovery requires a salvage manifest. - Do not rely on pause alone for
no_agent: truejobs that another scheduler can fire. Add a script-level kill switch. - Do not pause without setting
paused_reason. A paused job six months from now with no reason is indistinguishable from a broken one. - Do not hand-edit
jobs.jsonto addpaused_atdirectly. The scheduler overwrites the store on the next tick. Always usehermes cron pause <job_id>. - Do not assume pause stops token spend instantly. A run that started just before the pause completes in full.
Sources
- ABS companion: Hermes Agent Cron Jobs — pause/resume appears here as one paragraph; this guide expands it into the dedicated tips guide.
- ABS companion: ABS Cron Job Self-Contained Prompts: The Rule.
- ABS companion: ABS Regression Failure Modes and Their Cheap-First Check.
- Skill:
/opt/data/skills/devops/cron-health-monitor/SKILL.md— P33 paused-job-still-runs and the 4-step “VPS-only mode” gate.
Last verified: 2026-07-14 against Hermes v0.18.2 (2026.7.7.2), /opt/data/cron/jobs.json (68 jobs, 35 paused, paused_at + paused_reason confirmed).



Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.