3.8 KiB
Gerhard scheduled jobs
Hermes stores live scheduled jobs in:
agents/gerhard-hermes/cron/jobs.json
That file is read by hermes cron list, hermes cron create, and the gateway scheduler.
It also contains mutable runtime state, such as:
next_run_atlast_run_atlast_status- repeat counters
- delivery errors
Because of that, jobs.json is not the best long-term source-of-truth for Git.
It will change merely because time passes or a job runs.
Recommended convention
Keep desired jobs in version control here:
agents/gerhard-hermes/cron/desired-jobs.json
Then materialize them into Hermes runtime jobs when bootstrapping Gerhard.
This gives us two layers:
- Declarative schedule intent, tracked in Git.
- Runtime scheduler state, allowed to mutate locally.
Live Hermes commands
Inside the Gerhard container, with HERMES_HOME=/opt/data:
hermes cron list --all
hermes cron create "every 1d" "Your self-contained prompt here" --name "Daily reflection" --deliver local
hermes cron status
hermes cron run <job_id>
hermes cron pause <job_id>
hermes cron resume <job_id>
hermes cron remove <job_id>
From the host:
docker compose exec gerhard hermes cron list --all
docker compose exec gerhard hermes cron create "every 1d" "Your self-contained prompt here" --name "Daily reflection" --deliver local
Schedule formats
Hermes accepts:
30m— one-shot in 30 minutes2h— one-shot in 2 hoursevery 30m— recurring intervalevery 2h— recurring interval0 9 * * *— cron expression2026-02-03T14:00:00— one-shot timestamp
Desired job format
Add entries to desired-jobs.json like this:
{
"version": 1,
"jobs": [
{
"name": "Daily conceptual hygiene",
"schedule": "0 8 * * *",
"deliver": "local",
"prompt": "Review Glitch University knowledge graph notes. Identify one concept that is vague, one relation that needs evidence, and one useful next action. Write concise output in Gerhard voice.",
"skills": [],
"enabled_toolsets": ["file", "terminal"],
"workdir": "/workspace"
}
]
}
Important:
- Prompts must be self-contained. Cron jobs run without chat context.
- Avoid secrets in prompts.
- Use
deliver: localunless the target platform/channel is intentionally configured. - Use absolute
workdirvalues. In Gerhard's container, the mounted workspace is/workspace. - Keep runtime outputs under
cron/output/, not in Git.
Gerhard gutask identity
The hourly orientation job expects the Gerhard container to have gutask credentials in its environment:
API_URLCONTENT_API_KEYAGENT_IDAGENT_NAMEAGENT_PASSWORD
The Gerhard compose service uses env_file: .env, so these values come from the host-local .env on Omega13. Do not put the password, content API key, or tokens in Git.
The Gerhard compose service also mounts the sibling checkout read-write so Gerhard can improve the tool and push changes when asked:
../gutasktool->/opt/gutasktool${HOME}/.ssh->/root/.ssh
Gerhard has a wrapper at /opt/data/bin/gutask that runs /opt/gutasktool/gutasktool/cli.py. Cron prompts should call the absolute wrapper path, for example:
/opt/data/bin/gutask orient --agent "$AGENT_ID"
When modifying gutasktool, Gerhard should work in /opt/gutasktool, commit normally, and push to the configured Gitea origin remote. Secrets stay in environment/SSH config, never in Git.
Future improvement
Add a small bootstrap/sync script that reads desired-jobs.json and reconciles it into jobs.json by stable job name.
That lets Omega13 do:
git pull
docker compose up -d gerhard gerhard-dashboard
# optional: docker compose exec gerhard python /opt/data/cron/sync_desired_jobs.py
For now, desired-jobs.json is the version-controlled schedule manifest.