Hermes Agent Cron Jobs: Authoring, Scheduling, and the Self-Contained Prompt Rule
Author Hermes cron jobs that survive a fresh session: prompt self-containment, schedule patterns, dry-run, pause/resume, run-once, and the difference between agent and no_agent modes.

A scheduled Hermes job runs in a fresh session — no chat history, no current “you,” no in-flight conversation. The prompt body has to be the operator. This guide walks that constraint through every step: authoring a job that survives being woken up alone, attaching a skill, scheduling it, dry-running it, pausing it, and removing it cleanly. It is the cron companion to the setup guide.
What “self-contained” means
A cron prompt is run in a fresh session. It has no access to the chat, the current “you”, or any in-flight conversation. The prompt itself is the operator. If it depends on context outside the prompt — a URL you mentioned yesterday, a script path you remember, a flag you debugged last week — the cron job either errors or returns nonsense. Every fact the job needs (URLs, names, paths, scripts, schedules, expected outputs) goes inside the prompt body.
The shape that works is three short paragraphs: what this job does, the exact steps to take, what success looks like and what to do on failure. Anything inside has to be paths and identifiers a fresh agent can read off disk, not references to “the conversation we had.” The daily cloudflared restart prompt on this VPS (job 360e63a4ffc3) follows that shape: probe URL, path to inspect, SIGTERM/SIGKILL escalation, silent-on-healthy exit. Nothing relies on the operator’s memory.
Anatomy of a job
hermes cron list shows the live state. The canonical store is $HERMES_HOME/jobs.json — on this install it resolves to /opt/data/cron/jobs.json (a dict with a jobs array and updated_at). Each row is a job:
- a short ID (first 12 hex chars of a UUID), a unique human name
- a prompt body — only on agent-based jobs; the most-edited field
- an optional script path (filename only; see “no_agent mode” below)
- a schedule —
{"display": "0 6 * * *", "expr": "0 6 * * *", "kind": "cron"}for cron syntax, or{"display": "every 5m", "kind": "interval", "minutes": 5}for intervals - a
statefield —scheduledorpaused; an independentenabledflag - a
deliverfield —local,origin,telegram,telegram:<chat_id>,discord, orsignal - optional skills,
no_agent: true|false, plus scheduler-maintainedlast_run_at,last_status,last_error
State and last_run_at are what you check first when triaging.
Create vs edit
hermes cron create takes the schedule as the first positional argument, not a flag. The shape:
hermes cron create "30 11 * * *" \
--name "my-job" \
--prompt "..." \
--deliver local
--prompt is the agent’s prompt body. With --no-agent, pass --script <name> instead — prompt is rejected when no_agent: true because “the script IS the job.” Edit an existing job with hermes cron edit <job_id> --prompt "..." (the ID is the short hex). Always edit prompts via the cron tool, never by hand-editing jobs.json — the store is the source of truth and hand edits get clobbered on the next scheduler tick.
Schedules
5-field cron (minute hour day-of-month month day-of-week) is the default. Use */N for every-N intervals and comma lists for unions. The every <N>m shorthand resolves to the same job as */N. Cron runs in UTC — set the timezone inside the prompt (MDT_OFFSET = timedelta(hours=-6)) or convert in the script, don’t fight the scheduler. Long schedules (0 15 1 * * for the AI Model Matrix Monthly Rebuild) and comma unions (0 12,18,0 * * * for the ABS news cron’s three MDT windows) both work without special handling.
Pause and resume
hermes cron pause <job_id> flips a job to state: paused. It stops firing but stays in the store. hermes cron resume <job_id> flips it back. Pause is the right move when an upstream service is broken (don’t keep restarting cloudflared, just pause the restart loop) or when you’re doing role-attribute cleanup. Right now on this VPS (2026-07-14), several watchdogs are paused at the same timestamp — Dashboard (02fb666c7f40), Cloudflared (f0d9b90e151e), Mockup Studio (3c987e8db1cf), blog-skill (f0690c3b821c) — because upstream services are intentionally down. They are not “broken”; they are paused on purpose. Always check state before proposing any cron action.
A pause flag in the scheduler is not always load-bearing — output under /opt/data/cron/output/<job_id>/ is the ground truth for “did this job actually run.” Treat that directory, not the state UI, as the answer.
Run-now (dry run)
hermes cron run <job_id> fires the job synchronously in a fresh session and prints the result. Use it to verify a new prompt works without waiting for the schedule. The fired run writes to /opt/data/cron/output/<job_id>/<timestamp>.md and updates last_run_at. For no_agent: true jobs the same command runs the script and surfaces its stdout — empty stdout produces only the local file, non-empty is what the wrapper allows through. The --accept-hooks flag is for environments where you don’t want to answer shell-hook prompts by hand.
For agent-based jobs, the dry-run tells you whether the prompt is self-contained. If it asks a clarifying question back, the prompt is broken for cron.
Attach a skill (or don’t)
hermes cron create <schedule> --skill <name> (repeat to attach multiple) boots the prompt with that skill loaded. Each skill costs context tokens every run, so only attach what the prompt uses. On this VPS, lint is attached to the weekly Senior-dev profile weekly lint cron (12a49d2b110d) and memory-trim + harness-self-maintenance are attached to the weekly Johnny5 memory-trim job (22633f9d11ca). Both attach only what the prompt needs. Without --skill, the prompt runs with the default loaded skills — verify with hermes cron list that the loaded set matches the prompt’s intent.
no_agent mode
hermes cron create <schedule> --script <name> --no-agent skips the chat-agent layer entirely. The script’s stdout is the job’s output. With deliver: telegram, non-empty stdout routes to Telegram and empty stdout is silent — the canonical watchdog pattern (memory alerts, disk alerts, CI pings). A no_agent: true job that wants to suppress output on healthy runs uses the [SILENT] token: the wrapper prints [SILENT] on success and the alert body on failure. hermes cron list reports last_status: ok only when the script exited 0.
Two constraints to know:
--scriptmust be a filename only, not an absolute path. The script must live under~/.hermes/scripts/(or be a symlink the resolver finds there). Absolute paths are rejected with “Script path must be relative to ~/.hermes/scripts/.”--no-agentrejects--prompt. The script IS the job; there is no agent in the loop.
Commands, briefly
| Job | Command |
|---|---|
| List (active only) | hermes cron list |
| List (including disabled) | hermes cron list --all |
| Create (agent-based) | hermes cron create "<schedule>" --name "<name>" --prompt "<prompt>" --deliver <target> |
| Create (script-based) | hermes cron create "<schedule>" --name "<name>" --script <name> --no-agent --deliver <target> |
| Edit | hermes cron edit <job_id> --prompt "<new prompt>" |
| Run once | hermes cron run <job_id> |
| Pause | hermes cron pause <job_id> |
| Resume | hermes cron resume <job_id> |
| Remove | hermes cron remove <job_id> |
<job_id> is the short hex prefix shown in hermes cron list. <schedule> is the cron expression or every <N>m interval, positional.
Verification checklist
| # | Command | What it proves |
|---|---|---|
| 1 | hermes cron list | Job visible with state, schedule, deliver, mode |
| 2 | hermes cron run <job_id> | Prompt runs and returns the expected shape |
| 3 | ls -t /opt/data/cron/output/<job_id>/ | head -3 | Output file landed with fresh timestamp |
| 4 | hermes cron pause <job_id> && sleep 1 && hermes cron list | State flip to paused |
| 5 | hermes cron resume <job_id> && sleep 1 && hermes cron list | State flip back to scheduled |
| 6 | grep '"state":' /opt/data/cron/jobs.json | wc -l | Store count matches hermes cron list | wc -l |
If any of these returns something you didn’t expect, read the output file from step 3 to see what the script actually did, and the last_error / last_delivery_error fields in jobs.json for what the scheduler thinks went wrong.
What not to do
- Don’t put session-bound context in a cron prompt. URL paths, file paths, chat IDs, expected outputs — all of it has to be inside the prompt body or inside a file the prompt reads.
- Don’t hand-edit
jobs.json. The cron tool is the API; edits get clobbered on the next scheduler tick, and a malformed entry can breakhermes cron listfor every job. - Don’t chain
--no-agentwith--prompt. Withno_agent: truethe script IS the job — there is no chat for the prompt to go to. - Don’t write a “silent on empty” prompt and rely on the LLM to produce exactly
[SILENT]. The model writes reasoning before the token and the cron subsystem wraps the entire stdout — use ano_agentscript that prints[SILENT]only on healthy runs.
Sources
- Hermes Agent cron docs
- ABS companion: Hermes Agent setup v2
- ABS companion: ABS Regression Failure Modes
Last verified: 2026-07-14 against Hermes v0.18.2 and the live cron store on this VPS (68 jobs; 35 paused; 33 scheduled at the time of verification).


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.