sidebar_position: 11 title: “Cron Internals” description: “How Hermes stores, schedules, edits, pauses, skill-loads, and delivers cron jobs”
Cron Internals
The cron subsystem provides scheduled task execution — from simple one-shot delays to recurring cron-expression jobs with skill injection and cross-platform delivery.
Key Files
| File | Purpose |
|---|---|
cron/jobs.py | Job model, storage, atomic read/write to jobs.json |
cron/scheduler.py | Scheduler loop — due-job detection, execution, repeat tracking |
tools/cronjob_tools.py | Model-facing cronjob tool registration and handler |
gateway/run.py | Gateway integration — cron ticking in the long-running loop |
hermes_cli/cron.py | CLI hermes cron subcommands |
Scheduling Model
Four schedule formats are supported:
| Format | Example | Behavior |
|---|---|---|
| Relative delay | 30m, 2h, 1d | One-shot, fires after the specified duration |
| Interval | every 2h, every 30m | Recurring, fires at regular intervals |
| Cron expression | 0 9 * * * | Standard 5-field cron syntax (minute, hour, day, month, weekday) |
| ISO timestamp | 2025-01-15T09:00:00 | One-shot, fires at the exact time |
The model-facing surface is a single cronjob tool with action-style operations: create, list, update, pause, resume, run, remove.
Job Storage
Jobs are stored in ~/.hermes/cron/jobs.json with atomic write semantics (write to temp file, then rename). Each job record contains:
{
"id": "job_abc123",
"name": "Daily briefing",
"prompt": "Summarize today's AI news and funding rounds",
"schedule": "0 9 * * *",
"skills": ["ai-funding-daily-report"],
"deliver": "telegram:-1001234567890",
"repeat": null,
"state": "scheduled",
"next_run": "2025-01-16T09:00:00Z",
"run_count": 42,
"created_at": "2025-01-01T00:00:00Z",
"model": null,
"provider": null,
"script": null
}
Job Lifecycle States
| State | Meaning |
|---|---|
scheduled | Active, will fire at next scheduled time |
paused | Suspended — won’t fire until resumed |
completed | Repeat count exhausted or one-shot that has fired |
running | Currently executing (transient state) |
Backward Compatibility
Older jobs may have a single skill field instead of the skills array. The scheduler normalizes this at load time — single skill is promoted to skills: [skill].
Scheduler Runtime
Tick Cycle
The scheduler runs on a periodic tick (default: every 60 seconds):
tick()
1. Acquire scheduler lock (prevents overlapping ticks)
2. Load all jobs from jobs.json
3. Filter to due jobs (next_run <= now AND state == "scheduled")
4. For each due job:
a. Set state to "running"
b. Create fresh AIAgent session (no conversation history)
c. Load attached skills in order (injected as user messages)
d. Run the job prompt through the agent
e. Deliver the response to the configured target
f. Update run_count, compute next_run
g. If repeat count exhausted → state = "completed"
h. Otherwise → state = "scheduled"
5. Write updated jobs back to jobs.json
6. Release scheduler lock
Gateway Integration
In gateway mode, the scheduler tick is integrated into the gateway’s main event loop. The gateway calls scheduler.tick() on its periodic maintenance cycle, which runs alongside message handling.
In CLI mode, cron jobs only fire when hermes cron commands are run or during active CLI sessions.
Fresh Session Isolation
Each cron job runs in a completely fresh agent session:
- No conversation history from previous runs
- No memory of previous cron executions (unless persisted to memory/files)
- The prompt must be self-contained — cron jobs cannot ask clarifying questions
- The
cronjobtoolset is disabled (recursion guard)
Skill-Backed Jobs
A cron job can attach one or more skills via the skills field. At execution time:
- Skills are loaded in the specified order
- Each skill’s SKILL.md content is injected as context
- The job’s prompt is appended as the task instruction
- The agent processes the combined skill context + prompt
This enables reusable, tested workflows without pasting full instructions into cron prompts. For example:
Create a daily funding report → attach "ai-funding-daily-report" skill
Script-Backed Jobs
Jobs can also attach a Python script via the script field. The script runs before each agent turn, and its stdout is injected into the prompt as context. This enables data collection and change detection patterns:
# ~/.hermes/scripts/check_competitors.py
import requests, json
# Fetch competitor release notes, diff against last run
# Print summary to stdout — agent analyzes and reports
The script timeout defaults to 120 seconds. _get_script_timeout() resolves the limit through a three-layer chain:
- Module-level override —
_SCRIPT_TIMEOUT(for tests/monkeypatching). Only used when it differs from the default. - Environment variable —
HERMES_CRON_SCRIPT_TIMEOUT - Config —
cron.script_timeout_secondsinconfig.yaml(read viaload_config()) - Default — 120 seconds
Provider Recovery
run_job() passes the user’s configured fallback providers and credential pool into the AIAgent instance:
- Fallback providers — reads
fallback_providers(list) orfallback_model(legacy dict) fromconfig.yaml, matching the gateway’s_load_fallback_model()pattern. Passed asfallback_model=toAIAgent.__init__, which normalizes both formats into a fallback chain. - Credential pool — loads via
load_pool(provider)fromagent.credential_poolusing the resolved runtime provider name. Only passed when the pool has credentials (pool.has_credentials()). Enables same-provider key rotation on 429/rate-limit errors.
This mirrors the gateway’s behavior — without it, cron agents would fail on rate limits without attempting recovery.
Delivery Model
Cron job results can be delivered to any supported platform:
| Target | Syntax | Example |
|---|---|---|
| Origin chat | origin | Deliver to the chat where the job was created |
| Local file | local | Save to ~/.hermes/cron/output/ |
| Telegram | telegram or telegram:<chat_id> | telegram:-1001234567890 |
| Discord | discord or discord:#channel | discord:#engineering |
| Slack | slack | Deliver to Slack home channel |
whatsapp | Deliver to WhatsApp home | |
| Signal | signal | Deliver to Signal |
| Matrix | matrix | Deliver to Matrix home room |
| Mattermost | mattermost | Deliver to Mattermost home |
email | Deliver via email | |
| SMS | sms | Deliver via SMS |
| Home Assistant | homeassistant | Deliver to HA conversation |
| DingTalk | dingtalk | Deliver to DingTalk |
| Feishu | feishu | Deliver to Feishu |
| WeCom | wecom | Deliver to WeCom |
| Weixin | weixin | Deliver to Weixin (WeChat) |
| BlueBubbles | bluebubbles | Deliver to iMessage via BlueBubbles |
For Telegram topics, use the format telegram:<chat_id>:<thread_id> (e.g., telegram:-1001234567890:17585).
Response Wrapping
By default (cron.wrap_response: true), cron deliveries are wrapped with:
- A header identifying the cron job name and task
- A footer noting the agent cannot see the delivered message in conversation
The [SILENT] prefix in a cron response suppresses delivery entirely — useful for jobs that only need to write to files or perform side effects.
Session Isolation
Cron deliveries are NOT mirrored into gateway session conversation history. They exist only in the cron job’s own session. This prevents message alternation violations in the target chat’s conversation.
Recursion Guard
Cron-run sessions have the cronjob toolset disabled. This prevents:
- A scheduled job from creating new cron jobs
- Recursive scheduling that could explode token usage
- Accidental mutation of the job schedule from within a job
Locking
The scheduler uses file-based locking to prevent overlapping ticks from executing the same due-job batch twice. This is important in gateway mode where multiple maintenance cycles could overlap if a previous tick takes longer than the tick interval.
CLI Interface
The hermes cron CLI provides direct job management:
hermes cron list # Show all jobs
hermes cron create # Interactive job creation (alias: add)
hermes cron edit <job_id> # Edit job configuration
hermes cron pause <job_id> # Pause a running job
hermes cron resume <job_id> # Resume a paused job
hermes cron run <job_id> # Trigger immediate execution
hermes cron remove <job_id> # Delete a job