# 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_at` - `last_run_at` - `last_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: 1. Declarative schedule intent, tracked in Git. 2. Runtime scheduler state, allowed to mutate locally. ## Live Hermes commands Inside the Gerhard container, with `HERMES_HOME=/opt/data`: ```bash 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 hermes cron pause hermes cron resume hermes cron remove ``` From the host: ```bash 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 minutes - `2h` — one-shot in 2 hours - `every 30m` — recurring interval - `every 2h` — recurring interval - `0 9 * * *` — cron expression - `2026-02-03T14:00:00` — one-shot timestamp ## Desired job format Add entries to `desired-jobs.json` like this: ```json { "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: local` unless the target platform/channel is intentionally configured. - Use absolute `workdir` values. 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_URL` - `CONTENT_API_KEY` - `AGENT_ID` - `AGENT_NAME` - `AGENT_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` - `./shared/knowledge` -> `/knowledge` - `${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: ```bash /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: ```bash 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.