Skip to content

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.

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 = true
model = "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 = true
FieldTypeDefaultMeaning
idstring— (required)Stable identifier; the DB primary key and the merge key across restarts
namestringunsetHuman-readable label
job_typestring"shell""shell", "agent", or "workflow"
scheduleinline table— (required){ kind = "cron"|"every"|"at", ... } (see below)
commandstringRequired for shell and workflow jobs
promptstringRequired for agent jobs
enabledbooltrueSet false to declare a paused job
modelstringunsetModel override for agent jobs
allowed_toolsstring[]unsetTool allowlist for agent jobs (omit = all tools)
session_targetstring"isolated""isolated" (fresh context) or "main" (shared session)
deliveryinline tableunset{ 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 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-cleanup
triggers:
- 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.

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.

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 = true
max_tasks = 64 # jobs fetched per polling cycle
max_concurrent = 4 # jobs run in parallel
KeySectionTypeDefaultMeaning
enabled[scheduler]booltrueMaster switch for the scheduler loop
max_tasks[scheduler]int64Max jobs pulled per polling cycle (not total stored jobs)
max_concurrent[scheduler]int4Max jobs executed in parallel
scheduler_poll_secs[reliability]int15Polling 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.

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 # default

Set 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.

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 = 2
provider_backoff_ms = 500
KeyTypeDefaultMeaning
scheduler_retriesint2Max retry attempts for a failed job execution
provider_backoff_msint500Base backoff in ms (floored at 200, doubled per retry, capped at 30 s)

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 = 100

Read or change this at runtime without restarting via the settings endpoint:

GET /api/cron/settings
PATCH /api/cron/settings
Authorization: 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=20

limit 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.

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 via revka cron add-at always set this; agent one-shots default to true but 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.
  • at schedule without delete_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:

  1. The command’s binary is in the autonomy.allowed_commands allowlist.

  2. The command’s risk tier (low / medium / high) satisfies the approval requirement.

  3. No path argument points outside the workspace root or into system directories.

  4. No input redirection reads from a forbidden path.

  5. 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.

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 = false

It 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.