mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-05 02:31:47 +00:00
* feat(cron): add no_agent mode for script-only cron jobs (watchdog pattern)
Adds a no_agent=True option to the cronjob system. When enabled, the
scheduler runs the attached script on schedule and delivers its stdout
directly to the job's target — no LLM, no agent loop, no token spend.
This is the classic bash-watchdog pattern (memory alert every 5 min,
disk alert every 15 min, CI ping) reimplemented as a first-class Hermes
primitive instead of a systemd timer + curl + bot token triplet living
outside the system.
## What
hermes cron create "every 5m" \
--no-agent \
--script memory-watchdog.sh \
--deliver telegram \
--name memory-watchdog
Agent tool:
cronjob(action='create',
schedule='every 5m',
script='memory-watchdog.sh',
no_agent=True,
deliver='telegram')
Semantics:
- Script stdout (trimmed) → delivered verbatim as the message
- Empty stdout → silent tick (no delivery; watchdog pattern)
- wakeAgent=false gate → silent tick (same gate LLM jobs use)
- Non-zero exit/timeout → delivered as an error alert
(broken watchdogs shouldn't fail silently)
- No LLM ever invoked; no tokens spent; no provider fallback applied
## Implementation
cron/jobs.py
* create_job gains no_agent: bool = False
* prompt becomes Optional (no_agent jobs don't need one)
* Validation: no_agent=True requires a script at create time
* Field roundtrips via load_jobs / save_jobs / update_job
cron/scheduler.py
* run_job: new short-circuit branch at the top that runs the script,
wraps its output into the (success, doc, final_response, error)
tuple downstream delivery already expects, and returns before any
AIAgent import or construction
* _run_job_script: picks interpreter by extension — .sh/.bash run
under /bin/bash, anything else under sys.executable (Python).
Shell support unlocks the bash-watchdog pattern without wrapping
scripts in Python. Extension is explicit; we deliberately do NOT
trust the file's own shebang. Path-containment guard (scripts dir)
unchanged.
tools/cronjob_tools.py
* Schema: new no_agent boolean property with clear trigger guidance
* cronjob() accepts no_agent and validates mode-specific shape:
- no_agent=True requires script; prompt/skills optional
- no_agent=False keeps the existing 'prompt or skill required' rule
* update path rejects flipping no_agent=True on a job without a script
* _format_job surfaces no_agent in list output
* Handler lambda forwards no_agent from tool args
hermes_cli/main.py, hermes_cli/cron.py
* 'hermes cron create --no-agent' and edit's --no-agent / --agent
pair for toggling at CLI parity with the agent tool
* Existing --script help text updated to describe both modes
* List / create / edit output now shows 'Mode: no-agent (...)' when set
## Tests
tests/cron/test_cron_no_agent.py — 18 tests covering:
* create_job: no_agent shape, validation, field persistence
* update_job: flag roundtrip across reload
* cronjob tool: schema validation, update toggling, mode-specific
requirements, prompt-relaxation rule
* run_job short-circuit:
- success path delivers stdout verbatim
- empty stdout → SILENT_MARKER (no delivery downstream)
- wakeAgent=false gate → silent
- script failure → error alert
- run_job does NOT import AIAgent (verified via mock)
* _run_job_script:
- .sh executes via bash (no shebang required)
- .bash executes via bash
- .py still runs via sys.executable (regression)
- path-traversal still blocked (security regression)
All 18 new tests pass. 341/342 pre-existing cron tests still pass; the
one failure (test_script_empty_output_noted) was already broken on main
and is unrelated to this change.
## Docs
website/docs/guides/cron-script-only.md — new dedicated guide covering
the watchdog pattern, interpreter rules, delivery mapping, worked
examples (memory / disk alerts), and the comparison table vs hermes send,
regular LLM cron jobs, and OS-level cron.
website/docs/user-guide/features/cron.md — new 'No-agent mode' section
in the cron feature reference, cross-linked to the guide.
website/docs/guides/automate-with-cron.md — new tip box pointing users
to no-agent mode when they don't need LLM reasoning.
## Compatibility
- Existing jobs: unchanged. no_agent defaults to False, existing code
paths untouched until the flag is set.
- Schema additive only; older jobs.json without the field load fine
via .get() with False default.
- New CLI flags are opt-in and don't alter existing flag behavior.
* fix(cron): lazy-import AIAgent + SessionDB so no_agent ticks pay zero
The unconditional `from run_agent import AIAgent` + SessionDB() init at
the top of run_job() meant every no_agent tick still paid the full agent
module load cost (~300ms + transitive imports + DB open) even though it
never touched any of that machinery.
Move both to live under the default (LLM) path, after the no_agent
short-circuit has returned. Now a no_agent tick's sys.modules stays
clean — verified end-to-end:
assert 'run_agent' not in sys.modules # before
run_job(no_agent_job)
assert 'run_agent' not in sys.modules # after
The existing mock-based unit test (test_run_job_no_agent_never_invokes_aiagent)
kept passing because patch() replaces the class AFTER import; the leak
was only visible via real subprocess-style verification. End-to-end
demo confirmed: agent calls cronjob(no_agent=True) → script runs →
stdout delivered → no LLM machinery loaded.
* docs(cron): tighten no_agent tool schema — defaults, silent semantics, pick rule
Previous description buried the important bits in one long sentence.
Agents could plausibly miss three things an LLM-facing schema should
make unmissable:
1. What the default is — now first sentence + JSON Schema `default: false`
2. What 'silent run' actually means for the user — now spelled out:
'nothing is sent to the user and they won't see anything happened'
3. When to pick True vs False — now a concrete decision rule with
examples on both sides (watchdogs/metrics/pollers → True;
summarize/draft/pick/rephrase → False)
Also adds explicit 'prompt and skills are ignored when True' since the
agent could otherwise still pass them out of habit.
No behavior change — schema text only.
471 lines
17 KiB
Markdown
471 lines
17 KiB
Markdown
---
|
||
sidebar_position: 5
|
||
title: "Scheduled Tasks (Cron)"
|
||
description: "Schedule automated tasks with natural language, manage them with one cron tool, and attach one or more skills"
|
||
---
|
||
|
||
# Scheduled Tasks (Cron)
|
||
|
||
Schedule tasks to run automatically with natural language or cron expressions. Hermes exposes cron management through a single `cronjob` tool with action-style operations instead of separate schedule/list/remove tools.
|
||
|
||
## What cron can do now
|
||
|
||
Cron jobs can:
|
||
|
||
- schedule one-shot or recurring tasks
|
||
- pause, resume, edit, trigger, and remove jobs
|
||
- attach zero, one, or multiple skills to a job
|
||
- deliver results back to the origin chat, local files, or configured platform targets
|
||
- run in fresh agent sessions with the normal static tool list
|
||
|
||
:::warning
|
||
Cron-run sessions cannot recursively create more cron jobs. Hermes disables cron management tools inside cron executions to prevent runaway scheduling loops.
|
||
:::
|
||
|
||
## Creating scheduled tasks
|
||
|
||
### In chat with `/cron`
|
||
|
||
```bash
|
||
/cron add 30m "Remind me to check the build"
|
||
/cron add "every 2h" "Check server status"
|
||
/cron add "every 1h" "Summarize new feed items" --skill blogwatcher
|
||
/cron add "every 1h" "Use both skills and combine the result" --skill blogwatcher --skill maps
|
||
```
|
||
|
||
### From the standalone CLI
|
||
|
||
```bash
|
||
hermes cron create "every 2h" "Check server status"
|
||
hermes cron create "every 1h" "Summarize new feed items" --skill blogwatcher
|
||
hermes cron create "every 1h" "Use both skills and combine the result" \
|
||
--skill blogwatcher \
|
||
--skill maps \
|
||
--name "Skill combo"
|
||
```
|
||
|
||
### Through natural conversation
|
||
|
||
Ask Hermes normally:
|
||
|
||
```text
|
||
Every morning at 9am, check Hacker News for AI news and send me a summary on Telegram.
|
||
```
|
||
|
||
Hermes will use the unified `cronjob` tool internally.
|
||
|
||
## Skill-backed cron jobs
|
||
|
||
A cron job can load one or more skills before it runs the prompt.
|
||
|
||
### Single skill
|
||
|
||
```python
|
||
cronjob(
|
||
action="create",
|
||
skill="blogwatcher",
|
||
prompt="Check the configured feeds and summarize anything new.",
|
||
schedule="0 9 * * *",
|
||
name="Morning feeds",
|
||
)
|
||
```
|
||
|
||
### Multiple skills
|
||
|
||
Skills are loaded in order. The prompt becomes the task instruction layered on top of those skills.
|
||
|
||
```python
|
||
cronjob(
|
||
action="create",
|
||
skills=["blogwatcher", "maps"],
|
||
prompt="Look for new local events and interesting nearby places, then combine them into one short brief.",
|
||
schedule="every 6h",
|
||
name="Local brief",
|
||
)
|
||
```
|
||
|
||
This is useful when you want a scheduled agent to inherit reusable workflows without stuffing the full skill text into the cron prompt itself.
|
||
|
||
## Running a job inside a project directory
|
||
|
||
Cron jobs default to running detached from any repo — no `AGENTS.md`, `CLAUDE.md`, or `.cursorrules` is loaded, and the terminal / file / code-exec tools run from whatever working directory the gateway started in. Pass `--workdir` (CLI) or `workdir=` (tool call) to change that:
|
||
|
||
```bash
|
||
# Standalone CLI (schedule and prompt are positional)
|
||
hermes cron create "every 1d at 09:00" \
|
||
"Audit open PRs, summarize CI health, and post to #eng" \
|
||
--workdir /home/me/projects/acme
|
||
```
|
||
|
||
```python
|
||
# From a chat, via the cronjob tool
|
||
cronjob(
|
||
action="create",
|
||
schedule="every 1d at 09:00",
|
||
workdir="/home/me/projects/acme",
|
||
prompt="Audit open PRs, summarize CI health, and post to #eng",
|
||
)
|
||
```
|
||
|
||
When `workdir` is set:
|
||
|
||
- `AGENTS.md`, `CLAUDE.md`, and `.cursorrules` from that directory are injected into the system prompt (same discovery order as the interactive CLI)
|
||
- `terminal`, `read_file`, `write_file`, `patch`, `search_files`, and `execute_code` all use that directory as their working directory (via `TERMINAL_CWD`)
|
||
- The path must be an absolute directory that exists — relative paths and missing directories are rejected at create / update time
|
||
- Pass `--workdir ""` (or `workdir=""` via the tool) on edit to clear it and restore the old behaviour
|
||
|
||
:::note Serialization
|
||
Jobs with a `workdir` run sequentially on the scheduler tick, not in the parallel pool. This is deliberate — `TERMINAL_CWD` is process-global, so two workdir jobs running at the same time would corrupt each other's cwd. Workdir-less jobs still run in parallel as before.
|
||
:::
|
||
|
||
## Editing jobs
|
||
|
||
You do not need to delete and recreate jobs just to change them.
|
||
|
||
### Chat
|
||
|
||
```bash
|
||
/cron edit <job_id> --schedule "every 4h"
|
||
/cron edit <job_id> --prompt "Use the revised task"
|
||
/cron edit <job_id> --skill blogwatcher --skill maps
|
||
/cron edit <job_id> --remove-skill blogwatcher
|
||
/cron edit <job_id> --clear-skills
|
||
```
|
||
|
||
### Standalone CLI
|
||
|
||
```bash
|
||
hermes cron edit <job_id> --schedule "every 4h"
|
||
hermes cron edit <job_id> --prompt "Use the revised task"
|
||
hermes cron edit <job_id> --skill blogwatcher --skill maps
|
||
hermes cron edit <job_id> --add-skill maps
|
||
hermes cron edit <job_id> --remove-skill blogwatcher
|
||
hermes cron edit <job_id> --clear-skills
|
||
```
|
||
|
||
Notes:
|
||
|
||
- repeated `--skill` replaces the job's attached skill list
|
||
- `--add-skill` appends to the existing list without replacing it
|
||
- `--remove-skill` removes specific attached skills
|
||
- `--clear-skills` removes all attached skills
|
||
|
||
## Lifecycle actions
|
||
|
||
Cron jobs now have a fuller lifecycle than just create/remove.
|
||
|
||
### Chat
|
||
|
||
```bash
|
||
/cron list
|
||
/cron pause <job_id>
|
||
/cron resume <job_id>
|
||
/cron run <job_id>
|
||
/cron remove <job_id>
|
||
```
|
||
|
||
### Standalone CLI
|
||
|
||
```bash
|
||
hermes cron list
|
||
hermes cron pause <job_id>
|
||
hermes cron resume <job_id>
|
||
hermes cron run <job_id>
|
||
hermes cron remove <job_id>
|
||
hermes cron status
|
||
hermes cron tick
|
||
```
|
||
|
||
What they do:
|
||
|
||
- `pause` — keep the job but stop scheduling it
|
||
- `resume` — re-enable the job and compute the next future run
|
||
- `run` — trigger the job on the next scheduler tick
|
||
- `remove` — delete it entirely
|
||
|
||
## How it works
|
||
|
||
**Cron execution is handled by the gateway daemon.** The gateway ticks the scheduler every 60 seconds, running any due jobs in isolated agent sessions.
|
||
|
||
```bash
|
||
hermes gateway install # Install as a user service
|
||
sudo hermes gateway install --system # Linux: boot-time system service for servers
|
||
hermes gateway # Or run in foreground
|
||
|
||
hermes cron list
|
||
hermes cron status
|
||
```
|
||
|
||
### Gateway scheduler behavior
|
||
|
||
On each tick Hermes:
|
||
|
||
1. loads jobs from `~/.hermes/cron/jobs.json`
|
||
2. checks `next_run_at` against the current time
|
||
3. starts a fresh `AIAgent` session for each due job
|
||
4. optionally injects one or more attached skills into that fresh session
|
||
5. runs the prompt to completion
|
||
6. delivers the final response
|
||
7. updates run metadata and the next scheduled time
|
||
|
||
A file lock at `~/.hermes/cron/.tick.lock` prevents overlapping scheduler ticks from double-running the same job batch.
|
||
|
||
## Delivery options
|
||
|
||
When scheduling jobs, you specify where the output goes:
|
||
|
||
| Option | Description | Example |
|
||
|--------|-------------|---------|
|
||
| `"origin"` | Back to where the job was created | Default on messaging platforms |
|
||
| `"local"` | Save to local files only (`~/.hermes/cron/output/`) | Default on CLI |
|
||
| `"telegram"` | Telegram home channel | Uses `TELEGRAM_HOME_CHANNEL` |
|
||
| `"telegram:123456"` | Specific Telegram chat by ID | Direct delivery |
|
||
| `"telegram:-100123:17585"` | Specific Telegram topic | `chat_id:thread_id` format |
|
||
| `"discord"` | Discord home channel | Uses `DISCORD_HOME_CHANNEL` |
|
||
| `"discord:#engineering"` | Specific Discord channel | By channel name |
|
||
| `"slack"` | Slack home channel | |
|
||
| `"whatsapp"` | WhatsApp home | |
|
||
| `"signal"` | Signal | |
|
||
| `"matrix"` | Matrix home room | |
|
||
| `"mattermost"` | Mattermost home channel | |
|
||
| `"email"` | Email | |
|
||
| `"sms"` | SMS via Twilio | |
|
||
| `"homeassistant"` | Home Assistant | |
|
||
| `"dingtalk"` | DingTalk | |
|
||
| `"feishu"` | Feishu/Lark | |
|
||
| `"wecom"` | WeCom | |
|
||
| `"weixin"` | Weixin (WeChat) | |
|
||
| `"bluebubbles"` | BlueBubbles (iMessage) | |
|
||
| `"qqbot"` | QQ Bot (Tencent QQ) | |
|
||
|
||
The agent's final response is automatically delivered. You do not need to call `send_message` in the cron prompt.
|
||
|
||
### Response wrapping
|
||
|
||
By default, delivered cron output is wrapped with a header and footer so the recipient knows it came from a scheduled task:
|
||
|
||
```
|
||
Cronjob Response: Morning feeds
|
||
-------------
|
||
|
||
<agent output here>
|
||
|
||
Note: The agent cannot see this message, and therefore cannot respond to it.
|
||
```
|
||
|
||
To deliver the raw agent output without the wrapper, set `cron.wrap_response` to `false`:
|
||
|
||
```yaml
|
||
# ~/.hermes/config.yaml
|
||
cron:
|
||
wrap_response: false
|
||
```
|
||
|
||
### Silent suppression
|
||
|
||
If the agent's final response starts with `[SILENT]`, delivery is suppressed entirely. The output is still saved locally for audit (in `~/.hermes/cron/output/`), but no message is sent to the delivery target.
|
||
|
||
This is useful for monitoring jobs that should only report when something is wrong:
|
||
|
||
```text
|
||
Check if nginx is running. If everything is healthy, respond with only [SILENT].
|
||
Otherwise, report the issue.
|
||
```
|
||
|
||
Failed jobs always deliver regardless of the `[SILENT]` marker — only successful runs can be silenced.
|
||
|
||
## Script timeout
|
||
|
||
Pre-run scripts (attached via the `script` parameter) have a default timeout of 120 seconds. If your scripts need longer — for example, to include randomized delays that avoid bot-like timing patterns — you can increase this:
|
||
|
||
```yaml
|
||
# ~/.hermes/config.yaml
|
||
cron:
|
||
script_timeout_seconds: 300 # 5 minutes
|
||
```
|
||
|
||
Or set the `HERMES_CRON_SCRIPT_TIMEOUT` environment variable. The resolution order is: env var → config.yaml → 120s default.
|
||
|
||
## No-agent mode (script-only jobs)
|
||
|
||
For recurring jobs that don't need LLM reasoning — classic watchdogs, disk/memory alerts, heartbeats, CI pings — pass `no_agent=True` at creation time. The scheduler runs your script on schedule and delivers its stdout directly, skipping the agent entirely:
|
||
|
||
```bash
|
||
hermes cron create "every 5m" \
|
||
--no-agent \
|
||
--script memory-watchdog.sh \
|
||
--deliver telegram \
|
||
--name "memory-watchdog"
|
||
```
|
||
|
||
Semantics:
|
||
|
||
- Script stdout (trimmed) → delivered verbatim as the message.
|
||
- **Empty stdout → silent tick**, no delivery. This is the watchdog pattern: "only say something when something is wrong".
|
||
- Non-zero exit or timeout → an error alert is delivered, so a broken watchdog can't fail silently.
|
||
- `{"wakeAgent": false}` on the last line → silent tick (same gate LLM jobs use).
|
||
- No tokens, no model, no provider fallback — the job never touches the inference layer.
|
||
|
||
`.sh` / `.bash` files run under `/bin/bash`; anything else under the current Python interpreter (`sys.executable`). Scripts must live in `~/.hermes/scripts/` (same sandboxing rule as the pre-run script gate).
|
||
|
||
See the [Script-Only Cron Jobs guide](/docs/guides/cron-script-only) for worked examples.
|
||
|
||
## Provider recovery
|
||
|
||
Cron jobs inherit your configured fallback providers and credential pool rotation. If the primary API key is rate-limited or the provider returns an error, the cron agent can:
|
||
|
||
- **Fall back to an alternate provider** if you have `fallback_providers` (or the legacy `fallback_model`) configured in `config.yaml`
|
||
- **Rotate to the next credential** in your [credential pool](/docs/user-guide/configuration#credential-pool-strategies) for the same provider
|
||
|
||
This means cron jobs that run at high frequency or during peak hours are more resilient — a single rate-limited key won't fail the entire run.
|
||
|
||
## Schedule formats
|
||
|
||
The agent's final response is automatically delivered — you do **not** need to include `send_message` in the cron prompt for that same destination. If a cron run calls `send_message` to the exact target the scheduler will already deliver to, Hermes skips that duplicate send and tells the model to put the user-facing content in the final response instead. Use `send_message` only for additional or different targets.
|
||
|
||
### Relative delays (one-shot)
|
||
|
||
```text
|
||
30m → Run once in 30 minutes
|
||
2h → Run once in 2 hours
|
||
1d → Run once in 1 day
|
||
```
|
||
|
||
### Intervals (recurring)
|
||
|
||
```text
|
||
every 30m → Every 30 minutes
|
||
every 2h → Every 2 hours
|
||
every 1d → Every day
|
||
```
|
||
|
||
### Cron expressions
|
||
|
||
```text
|
||
0 9 * * * → Daily at 9:00 AM
|
||
0 9 * * 1-5 → Weekdays at 9:00 AM
|
||
0 */6 * * * → Every 6 hours
|
||
30 8 1 * * → First of every month at 8:30 AM
|
||
0 0 * * 0 → Every Sunday at midnight
|
||
```
|
||
|
||
### ISO timestamps
|
||
|
||
```text
|
||
2026-03-15T09:00:00 → One-time at March 15, 2026 9:00 AM
|
||
```
|
||
|
||
## Repeat behavior
|
||
|
||
| Schedule type | Default repeat | Behavior |
|
||
|--------------|----------------|----------|
|
||
| One-shot (`30m`, timestamp) | 1 | Runs once |
|
||
| Interval (`every 2h`) | forever | Runs until removed |
|
||
| Cron expression | forever | Runs until removed |
|
||
|
||
You can override it:
|
||
|
||
```python
|
||
cronjob(
|
||
action="create",
|
||
prompt="...",
|
||
schedule="every 2h",
|
||
repeat=5,
|
||
)
|
||
```
|
||
|
||
## Managing jobs programmatically
|
||
|
||
The agent-facing API is one tool:
|
||
|
||
```python
|
||
cronjob(action="create", ...)
|
||
cronjob(action="list")
|
||
cronjob(action="update", job_id="...")
|
||
cronjob(action="pause", job_id="...")
|
||
cronjob(action="resume", job_id="...")
|
||
cronjob(action="run", job_id="...")
|
||
cronjob(action="remove", job_id="...")
|
||
```
|
||
|
||
For `update`, pass `skills=[]` to remove all attached skills.
|
||
|
||
## Toolsets available to cron jobs
|
||
|
||
Cron runs each job in a fresh agent session with no chat platform attached. By default the cron agent gets **the toolset you configured for the `cron` platform in `hermes tools`** — not the CLI default, not everything under the sun.
|
||
|
||
```bash
|
||
hermes tools
|
||
# → pick the "cron" platform in the curses UI
|
||
# → toggle toolsets on/off just like you would for Telegram/Discord/etc.
|
||
```
|
||
|
||
Tighter per-job control is available via the `enabled_toolsets` field on `cronjob.create` (or on an existing job via `cronjob.update`):
|
||
|
||
```text
|
||
cronjob(action="create", name="weekly-news-summary",
|
||
schedule="every sunday 9am",
|
||
enabled_toolsets=["web", "file"], # just web + file, no terminal/browser/etc.
|
||
prompt="Summarize this week's AI news: ...")
|
||
```
|
||
|
||
When `enabled_toolsets` is set on a job it wins; otherwise the `hermes tools` cron-platform config wins; otherwise Hermes falls back to the built-in defaults. This matters for cost control: carrying `moa`, `browser`, `delegation` into every tiny "fetch news" job bloats the tool-schema prompt on every LLM call.
|
||
|
||
### Skipping the agent entirely: `wakeAgent`
|
||
|
||
If your cron job attaches a pre-check script (via `script=`), the script can decide at runtime whether Hermes should even invoke the agent. Emit a final stdout line of the form:
|
||
|
||
```text
|
||
{"wakeAgent": false}
|
||
```
|
||
|
||
…and cron skips the agent run entirely for this tick. Useful for frequent polls (every 1–5 min) that only need to wake the LLM when state actually changed — otherwise you pay for zero-content agent turns over and over.
|
||
|
||
```python
|
||
# pre-check script
|
||
import json, sys
|
||
latest = fetch_latest_issue_count()
|
||
prev = read_state("issue_count")
|
||
if latest == prev:
|
||
print(json.dumps({"wakeAgent": False})) # skip this tick
|
||
sys.exit(0)
|
||
write_state("issue_count", latest)
|
||
print(json.dumps({"wakeAgent": True, "context": {"new_issues": latest - prev}}))
|
||
```
|
||
|
||
When `wakeAgent` is omitted, the default is `true` (wake the agent as usual).
|
||
|
||
### Chaining jobs: `context_from`
|
||
|
||
A cron job can consume the most recent successful output of one or more other jobs by listing their names (or IDs) in `context_from`:
|
||
|
||
```text
|
||
cronjob(action="create", name="daily-digest",
|
||
schedule="every day 7am",
|
||
context_from=["ai-news-fetch", "github-prs-fetch"],
|
||
prompt="Write the daily digest using the outputs above.")
|
||
```
|
||
|
||
The referenced jobs' most recent completed outputs are injected above the prompt as context for this run. Each upstream entry must be a valid job ID or name (see `cronjob action="list"`). Note: chaining reads the *most recent completed* output — it does not wait for upstream jobs that are running in the same tick.
|
||
|
||
## Job storage
|
||
|
||
Jobs are stored in `~/.hermes/cron/jobs.json`. Output from job runs is saved to `~/.hermes/cron/output/{job_id}/{timestamp}.md`.
|
||
|
||
Jobs may store `model` and `provider` as `null`. When those fields are omitted, Hermes resolves them at execution time from the global configuration. They only appear in the job record when a per-job override is set.
|
||
|
||
The storage uses atomic file writes so interrupted writes do not leave a partially written job file behind.
|
||
|
||
## Self-contained prompts still matter
|
||
|
||
:::warning Important
|
||
Cron jobs run in a completely fresh agent session. The prompt must contain everything the agent needs that is not already provided by attached skills.
|
||
:::
|
||
|
||
**BAD:** `"Check on that server issue"`
|
||
|
||
**GOOD:** `"SSH into server 192.168.1.100 as user 'deploy', check if nginx is running with 'systemctl status nginx', and verify https://example.com returns HTTP 200."`
|
||
|
||
## Security
|
||
|
||
Scheduled task prompts are scanned for prompt-injection and credential-exfiltration patterns at creation and update time. Prompts containing invisible Unicode tricks, SSH backdoor attempts, or obvious secret-exfiltration payloads are blocked.
|