Cron jobs — scheduled agents
🔵 Agent OS can run an agent on a schedule, without a user. A cron job injects a prompt into the template's session and runs one turn — just like a chat request, so budgets, hooks, tools, and the event store behave the same way. The resulting response is delivered to one or more sinks.
- Job configs live in YAML and are hot-reloaded.
- Dynamic jobs are created at runtime from scripts (
schedule_cron) and are persisted tologs/cron.db, surviving restarts.
Where they are defined
1. On an agent — the cron: list in config/agents/<agent>/agent.yaml
(template defaults to the agent itself):
# config/agents/rss/agent.yaml
cron:
- name: morning-digest
schedule: "0 0 9 * * *"
prompt: "Резюмируй сегодняшние заголовки."
notify: [{ type: log }]
2. Globally — config/cron.yaml (each job names template):
# config/cron.yaml
jobs:
- name: morning-digest
template: rss
schedule: "0 0 9 * * *"
prompt: "Резюмируй сегодняшние заголовки."
On a name collision, config/cron.yaml wins over agent.yaml. Both sources are
hot-reloaded; dynamic (script-created) jobs are independent of both.
Job reference
| Field | Type | Meaning |
|---|---|---|
name |
string (required) | unique job id |
template |
string | template to run (in agent.yaml — implicit) |
schedule |
string | cron expression: sec min hour dom month dow (UTC) |
timezone |
string | IANA timezone (e.g. Europe/Moscow) in which schedule is interpreted; defaults to UTC |
interval_secs |
int | fixed interval (alternative to schedule) |
oneshot_at |
string | RFC 3339 time — fire once, then go away |
prompt |
string (required) | user message injected into the session |
persistent |
bool | true reuses the cron:<name> session (keeps context); defaults to false |
timeout_secs |
int | per-run limit |
overlap |
string | skip (default) | queue | parallel |
retry |
map | { max, backoff_secs } (reserved) |
enabled |
bool | defaults to true |
notify |
list | where the result is delivered (sinks below) |
Exactly one of schedule / interval_secs / oneshot_at must be set.
Sinks
A job can have any combination of sinks. Without notify, the result is available
only through the run history (GET /v1/cron, cron.db).
type |
What it does | Fields |
|---|---|---|
log |
writes the response to the server log | — |
webhook |
POSTs the result (JSON) to a URL | url, token_env (optional bearer from env) |
telegram |
sends the response to a Telegram chat | chat_id, token_env (opt.; defaults to the template's bot) |
vk |
sends the response to a VKontakte chat | peer_id, token_env (opt.; defaults to the template's community token) |
yandex_messenger |
sends the response to Yandex Messenger | chat_id (chat/channel id or user login), token_env (opt.; defaults to the template's bot token) |
messages_db |
persists the response to the message store | scope (defaults to cron:run:<name>) |
agent |
feeds the response to another agent as a message | template, session (opt.), run (defaults to true), prompt_wrap (opt., {result}) |
email |
sends the response via SMTP | to, subject (opt.; defaults to the job name), smtp |
workflow |
starts a workflow (ignores the agent run) | name, input |
storage |
writes the response as a file to storage | storage (backend), key ({source}/{ts}) |
Example with multiple sinks:
notify:
- type: log
- type: webhook
url: "https://hooks.example.com/ingest"
token_env: "CRON_WEBHOOK_SECRET"
- type: telegram
chat_id: 123456789
- type: vk
peer_id: 41848219
- type: messages_db
scope: "cron:reports"
- type: agent
template: summarizer
session: "pipeline:reports"
run: true
prompt_wrap: "Новый отчёт:\n{result}"
- type: email
to: "ops@example.com"
subject: "Cron: reports"
smtp:
host: "smtp.example.com"
username: "agent@example.com"
password_env: "SMTP_PASSWORD"
from: "agent@example.com"
Script API
Tool and hook scripts can schedule jobs at runtime (the contract
is shared between Rhai and JS). These jobs are dynamic and persist across restarts.
const r = schedule_cron({
name: "watch-price",
template: "hello-agent",
interval_secs: 3600,
prompt: "Проверь цену и сообщи.",
});
// r = { name: "watch-price", ok: true }
const gone = cancel_cron("watch-price"); // -> bool
const jobs = list_cron(); // -> массив статусов заданий
schedule_cron returns { name, ok, error? }; cancel_cron(name) — bool;
list_cron() — an array of statuses (name, template, schedule, last run). Errors
are thrown only when the cron service is not connected (e.g. agent-os hook without a server).
History and monitoring
Every run is written to logs/cron.db (table cron_runs) with a status
(success / error / budget_exhausted / rate_limited / timeout),
token count, cost estimate, and timings.
GET /v1/cron(admin) — list of jobs with schedule and a summary of the last run.POST /v1/cron/:name/run(admin) — manually run a job "right now", without waiting for the schedule (goes through the same path: overlap, run record, sinks).- Raw history — SQL queries against
logs/cron.db→cron_runs.
Notes
- Budgets: a scheduled job draws from the template's budget group like
a regular session. A frequent job on a shared group can "starve" live
users — configure
budget/budget_group. - Time is UTC by default; local time can be set with the
timezonefield (IANA name, e.g.Europe/Moscow).interval_secsandoneshot_atdo not depend on the timezone. - No catch-up: missed ticks (server was down, overlap) are skipped, not replayed.
- Sinks are best-effort: a failed sink logs a warning and does not fail the run.
agentsinks compose: chains ofA → B → Cwork because each hop is a regular session turn.