guide · computers

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.

July 14, 2026 · By Alastair Fraser

A retro robot at a punch-card-style wall calendar with the punch cards feeding in from the left, an old CRT screen above showing a job log scrolling, and a stamp clock showing today's date.

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 state field — scheduled or paused; an independent enabled flag
  • a deliver field — local, origin, telegram, telegram:<chat_id>, discord, or signal
  • optional skills, no_agent: true|false, plus scheduler-maintained last_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:

  • --script must 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-agent rejects --prompt. The script IS the job; there is no agent in the loop.

Commands, briefly

JobCommand
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>
Edithermes cron edit <job_id> --prompt "<new prompt>"
Run oncehermes cron run <job_id>
Pausehermes cron pause <job_id>
Resumehermes cron resume <job_id>
Removehermes 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

#CommandWhat it proves
1hermes cron listJob visible with state, schedule, deliver, mode
2hermes cron run <job_id>Prompt runs and returns the expected shape
3ls -t /opt/data/cron/output/<job_id>/ | head -3Output file landed with fresh timestamp
4hermes cron pause <job_id> && sleep 1 && hermes cron listState flip to paused
5hermes cron resume <job_id> && sleep 1 && hermes cron listState flip back to scheduled
6grep '"state":' /opt/data/cron/jobs.json | wc -lStore 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 break hermes cron list for every job.
  • Don’t chain --no-agent with --prompt. With no_agent: true the 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 a no_agent script that prints [SILENT] only on healthy runs.

Sources

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).

Sources

#hermes#cron#automation#scheduled-tasks#playbook

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.

Your email address will not be published. Required fields are marked.