Declarative jobs & scheduler config
Config-file cron jobs, scheduler polling/concurrency, catch-up, retries, run history, scheduled backups, and shell-job security.
Most of the cron documentation covers jobs you create imperatively — from the CLI, an agent tool, or the gateway API. This page is the opposite: jobs you declare in config.toml so they exist deterministically across every machine and restart, plus the tuning knobs that govern how the scheduler actually runs them.
Use declarative jobs when a schedule belongs to your deployment rather than to a moment — a nightly report, an hourly backup, a workflow trigger that should never drift. Use the scheduler and reliability sections to control polling cadence, parallelism, missed-job catch-up, retries, and how much run history is kept. For creating jobs interactively instead, see revka cron and Agent jobs & delivery.
Declarative cron jobs in config
Section titled “Declarative cron jobs in config”Declare jobs as an array of tables under [[cron.jobs]]. At each startup the scheduler syncs these declarations into the SQLite job store: it inserts new jobs, updates changed ones, and deletes jobs that are no longer in config. Declarative jobs are marked source = "declarative"; imperative jobs (created via CLI, tool, or API) carry a different source and are never touched by the sync.
# An agent job that runs weekdays at 08:00 New York time and posts to Slack.[[cron.jobs]]id = "daily-report"name = "Daily Report"job_type = "agent"schedule = { kind = "cron", expr = "0 8 * * 1-5", tz = "America/New_York" }prompt = "Generate the morning system report"enabled = truemodel = "anthropic/claude-opus-4-5"allowed_tools = ["file_read", "http_request"]session_target = "isolated"
[cron.jobs.delivery]mode = "announce"channel = "slack"to = "#ops-alerts"best_effort = true
# A shell job that runs every hour.[[cron.jobs]]id = "hourly-backup"job_type = "shell"schedule = { kind = "every", every_ms = 3600000 }command = "backup create"enabled = trueFields per [[cron.jobs]] entry
Section titled “Fields per [[cron.jobs]] entry”| Field | Type | Default | Meaning |
|---|---|---|---|
id | string | — (required) | Stable identifier; the DB primary key and the merge key across restarts |
name | string | unset | Human-readable label |
job_type | string | "shell" | "shell", "agent", or "workflow" |
schedule | inline table | — (required) | { kind = "cron"|"every"|"at", ... } (see below) |
command | string | — | Required for shell and workflow jobs |
prompt | string | — | Required for agent jobs |
enabled | bool | true | Set false to declare a paused job |
model | string | unset | Model override for agent jobs |
allowed_tools | string[] | unset | Tool allowlist for agent jobs (omit = all tools) |
session_target | string | "isolated" | "isolated" (fresh context) or "main" (shared session) |
delivery | inline table | unset | { mode, channel, to, best_effort } — announce output to a channel |
The schedule table uses the same shapes as elsewhere in cron:
schedule = { kind = "cron", expr = "0 9 * * 1-5", tz = "America/Los_Angeles" }schedule = { kind = "every", every_ms = 3600000 }schedule = { kind = "at", at = "2025-12-31T23:59:00Z" }tz applies only to kind = "cron"; every and at are timezone-agnostic. See Cron overview & expressions for the expression syntax and weekday-numbering rules.
Workflow cron triggers (YAML)
Section titled “Workflow cron triggers (YAML)”Workflow YAML files can carry their own cron triggers instead of a separate [[cron.jobs]] entry. At startup the scheduler scans ~/.revka/operator_mcp/workflow/builtins/, queries the gateway for Kumiho workflows, and syncs matching cron jobs (marked source = "workflow") into the store.
name: nightly-cleanuptriggers: - cron: "0 2 * * *"The cron: value is a standard 5-field expression; timezone is not currently supported for YAML triggers. These synthesized jobs use the id pattern __wf_cron_<slug>_<index> and fire the workflow via POST /api/workflows/run/:name. They are managed automatically — don’t edit them by hand. Stale workflow jobs (whose workflow has since been removed) are cleaned up on the next restart. For authoring triggers, see Variables, expressions & triggers.
Built-in backup cron job
Section titled “Built-in backup cron job”You don’t need a [[cron.jobs]] entry for scheduled backups. Set backup.schedule_cron and the scheduler synthesizes a virtual job (id = "__builtin_backup") that runs backup create as a shell job on your schedule.
[backup]schedule_cron = "0 3 * * *"schedule_timezone = "America/New_York"This is the recommended way to enable scheduled backups. The __builtin_backup job appears in revka cron list and GET /api/cron like any other declarative job, and is treated identically by retries, run history, and security policy.
Scheduler polling and concurrency
Section titled “Scheduler polling and concurrency”The [scheduler] and [reliability] sections govern how the loop drains due jobs each cycle.
[reliability]scheduler_poll_secs = 15 # how often to check for due jobs
[scheduler]enabled = truemax_tasks = 64 # jobs fetched per polling cyclemax_concurrent = 4 # jobs run in parallel| Key | Section | Type | Default | Meaning |
|---|---|---|---|---|
enabled | [scheduler] | bool | true | Master switch for the scheduler loop |
max_tasks | [scheduler] | int | 64 | Max jobs pulled per polling cycle (not total stored jobs) |
max_concurrent | [scheduler] | int | 4 | Max jobs executed in parallel |
scheduler_poll_secs | [reliability] | int | 15 | Polling cadence in seconds (clamped to a 5-second minimum) |
The poll interval uses a skip-on-lag tick: if a cycle runs long, missed ticks are dropped rather than fired in a burst, so the loop never stampedes after a slow batch. Raise max_concurrent for high-frequency workloads with fast jobs; raise max_tasks if many jobs can come due in the same window.
Startup catch-up
Section titled “Startup catch-up”When the daemon starts or restarts, it can run jobs that came due while it was down (late boot, crash, maintenance). The catch-up pass queries all overdue jobs — ignoring the max_tasks cap — and runs each once before entering the normal polling loop.
[cron]catch_up_on_startup = true # defaultSet catch_up_on_startup = false to skip catch-up; missed jobs then simply wait for their next natural scheduled occurrence. The catch-up phase deliberately bypasses max_tasks because firing every missed job once is the goal — that limit only governs steady-state polling.
Retry logic
Section titled “Retry logic”Failed executions are retried automatically. The number of retries is reliability.scheduler_retries (default 2, i.e. up to 3 total attempts). Between attempts an exponential backoff with jitter is applied, starting from reliability.provider_backoff_ms (a 200 ms floor is enforced) and doubling up to a 30-second cap.
[reliability]scheduler_retries = 2provider_backoff_ms = 500| Key | Type | Default | Meaning |
|---|---|---|---|
scheduler_retries | int | 2 | Max retry attempts for a failed job execution |
provider_backoff_ms | int | 500 | Base backoff in ms (floored at 200, doubled per retry, capped at 30 s) |
Run history persistence
Section titled “Run history persistence”Every execution is recorded in the cron_runs table with start and finish timestamps, status (ok or error), output, and duration in milliseconds. Stored output is capped at 16 KB per run. The number of records retained per job is cron.max_run_history (default 50); older records are pruned in the same transaction as each new insert.
[cron]max_run_history = 100Read or change this at runtime without restarting via the settings endpoint:
GET /api/cron/settingsPATCH /api/cron/settingsAuthorization: Bearer <pairing-token>Content-Type: application/json
{ "enabled": true, "catch_up_on_startup": false, "max_run_history": 100 }Changes are written to the on-disk config and applied to the in-memory config immediately. To read history for a single job:
GET /api/cron/:id/runs?limit=20limit ranges 1–100 (default 20); a missing job id returns 404. See Cron, sessions & attachments API for the full response shape, and Agent jobs & delivery for the cron_runs tool, which truncates output to 500 characters for display.
One-shot auto-delete
Section titled “One-shot auto-delete”A job with an at schedule fires once. What happens next depends on the outcome and on delete_after_run:
- Success +
delete_after_run = true— the job is removed from the DB. Shell jobs created viarevka cron add-atalways set this; agent one-shots default totruebut can override it. - Failure — the job is disabled, not deleted, so its failure output is preserved for debugging. Remove it manually with
revka cron remove <id>once you’ve inspected it. atschedule withoutdelete_after_run— disabled after running, so it can’t re-trigger with a past fire time.
If you want a one-shot job to persist after firing, set delete_after_run = false explicitly and expect it to remain in the disabled state.
Security policy enforcement for shell jobs
Section titled “Security policy enforcement for shell jobs”Shell cron jobs are validated against the security policy at two points: when the job is created or updated, and again at execution time (policy may have changed since creation). Validation checks that:
-
The command’s binary is in the
autonomy.allowed_commandsallowlist. -
The command’s risk tier (low / medium / high) satisfies the approval requirement.
-
No path argument points outside the workspace root or into system directories.
-
No input redirection reads from a forbidden path.
-
The autonomy level permits mutations (not read-only mode).
Agent jobs bypass shell validation entirely — their actions are governed by the agent’s own tool policy and allowed_tools allowlist instead.
[autonomy]level = "supervised"allowed_commands = ["echo", "python3", "git"]To approve a medium-risk command when creating or running a job through a tool, pass "approved": true to cron_add, cron_update, or cron_run. Jobs created through the gateway API are not pre-approved, so medium-risk commands there are rejected.
For the full policy model, see Policy, commands & sandboxing and the Security model.
The cron.enabled master switch
Section titled “The cron.enabled master switch”cron.enabled is the global on/off switch for the entire subsystem. When false, all cron tools and API endpoints return an error and the scheduler loop runs no jobs.
[cron]enabled = falseIt can also be toggled at runtime via PATCH /api/cron/settings. Disabling cron at runtime does not stop jobs that are already executing. Default: true.