Files
agent0/agents/gerhard-hermes/cron

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.

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:

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 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:

{
  "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
  • ${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.