Skip to content

Cron overview & expressions

Scheduling concepts, the three schedule types, the cron job types, and crontab expression syntax with timezone support.

Revka’s cron subsystem runs tasks for you automatically — repeating on a cron expression, repeating on a fixed interval, or firing once at a future moment. Each scheduled job runs as a shell command, an agent prompt, or a workflow trigger. Jobs are persisted in a SQLite database and fired by a scheduler loop inside the daemon.

This page is the conceptual entry point: it explains the three schedule types, the three job types, how to turn the whole subsystem on or off with cron.enabled, where jobs are stored, and the full crontab expression syntax — including normalization, weekday renumbering, and IANA timezone support. For the command-line workflow see revka cron; for agent-specific behavior see Agent jobs & delivery; for config-file jobs and scheduler tuning see Declarative jobs & scheduler config.

Reach for cron when you want work to happen on a clock or a timer without you being present — a nightly report, an hourly health check, a one-time reminder, or a recurring workflow run. For one-off work you are about to run yourself, just run the command or prompt directly. The whole subsystem can be surfaced through four interfaces, all backed by the same database:

Every job uses exactly one of three mutually exclusive schedule shapes, selected by a kind discriminator. In the tool and config surfaces the schedule is a JSON/TOML object; the CLI and gateway API expose each shape through dedicated subcommands or fields.

kindBehaviorRepeats?Timezone-aware?Required field
cronFires on a crontab expressionYesYes (tz)expr
everyFires every N millisecondsYesNoevery_ms
atFires once at an absolute UTC timeNoNo (always UTC)at
// Cron: weekdays at 09:00 New York time
{"kind": "cron", "expr": "0 9 * * 1-5", "tz": "America/New_York"}
// Every: once an hour
{"kind": "every", "every_ms": 3600000}
// At: once, then auto-removed
{"kind": "at", "at": "2026-12-31T23:59:00Z"}

Field reference:

FieldApplies toType / rules
exprcron (required)5-, 6-, or 7-field crontab expression
tzcron (optional)IANA timezone name; defaults to UTC
every_msevery (required)Positive integer milliseconds (must be > 0)
atat (required)RFC 3339 UTC datetime; must be in the future at creation

An at schedule is a one-shot. After it fires successfully it is removed from the database (for at jobs, delete_after_run defaults to true). If a one-shot job fails, it is disabled rather than deleted so its output is preserved for debugging — remove it manually once you have looked. If you set delete_after_run = false on an at job, it is disabled (not deleted) after running, to stop it re-triggering with a past fire time; you handle the disabled state yourself.

Independent of its schedule, every job has one of three execution backends.

job_typeRunsNotes
shellA shell command via sh -c <command> in the workspace directory120 s timeout; validated against the security policy
agentThe Revka agent with a promptInjects memory context; blocked in read-only mode; honors session target, model override, and tool allowlist
workflowA named Kumiho workflow via POST /api/workflows/run/:nameManaged automatically by workflow YAML triggersnot creatable through cron_add
Terminal window
# Shell job
revka cron add '*/15 * * * *' 'echo ok'
# Agent job (trailing argument is a prompt)
revka cron add '*/15 * * * *' --agent 'Check server health'

workflow jobs exist only because a workflow YAML declared a cron trigger. They are synced into the cron store automatically and carry IDs like __wf_cron_<slug>_<index>. For everything about authoring those, see Variables, expressions & triggers. For session targets, model overrides, tool allowlists, and delivering output to a chat channel, see Agent jobs & delivery.

You write standard 5-field crontab expressions:

┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12 or JAN–DEC)
│ │ │ │ ┌───── day of week (0–6, 0 = Sunday — see below)
│ │ │ │ │
* * * * *

All the usual field syntax is supported:

SyntaxMeaningExample
* / ?Any value* * * * * (every minute)
a-bRange1-5 (Mon–Fri in the weekday field)
a,b,cList0,6 (Sun and Sat)
*/nStep over the whole range*/5 (every 5th)
a-b/nStep within a range1-5/2
Named valuesMonths and days by nameMON-FRI, JAN

Internally Revka stores cron schedules in a 6-field format (sec min hour day month weekday), so a 5-field expression is normalized before it is saved. This is why the expression printed back by revka cron add and shown in revka cron list differs from what you typed. Two transformations apply to 5-field input:

  1. A seconds field is prepended. 0 9 * * * becomes 0 0 9 * * * — fire at second 0.
  2. The weekday column is renumbered (see below).
You write: 0 9 * * 1-5 (weekdays at 09:00)
Stored as: 0 0 9 * * 2-6

6- and 7-field expressions (which already include seconds, and optionally a year) are assumed to be in the internal format already and are passed through unchanged — they are not renumbered.

Standard vixie-cron uses 0 or 7 for Sunday, 1 for Monday, … 6 for Saturday. Revka’s internal cron engine uses a different convention: 1 = Sunday, 2 = Monday, … 7 = Saturday. The normalizer shifts your 5-field weekday values accordingly, which is why 1-5 (Mon–Fri) is stored as 2-6.

You writeMeaning
*/5 * * * *Every 5 minutes
0 9 * * *Every day at 09:00
0 9 * * 1-5Weekdays at 09:00
0 9 * * MON-FRIWeekdays at 09:00 (named days)
0 0,12 * * *Midnight and noon every day
0 */2 * * *Every 2 hours, on the hour
0 2 * * 0Sundays at 02:00
30 8 1 * *The 1st of every month at 08:30

Add a tz field (CLI: --tz) to localize a cron expression to any IANA timezone. The scheduler computes the next fire time in that zone — correctly across daylight-saving transitions — and converts it to UTC for storage. So 0 9 * * * with tz = "America/Los_Angeles" fires at 09:00 Los Angeles time year-round, not at a fixed UTC offset.

Terminal window
revka cron add '0 9 * * *' --tz America/Los_Angeles 'echo morning'
{"kind": "cron", "expr": "0 9 * * *", "tz": "America/Los_Angeles"}

The timezone name is validated by chrono-tz at creation time; an unknown name is rejected.

cron.enabled is the global on/off switch for the entire subsystem. When it is false, the scheduler loop does not execute jobs and the cron tools and API endpoints return an error. It defaults to true.

[cron]
enabled = true # default; set false to disable all scheduling

You can also toggle it at runtime over the API:

PATCH /api/cron/settings
Authorization: Bearer <pairing-token>
Content-Type: application/json
{"enabled": false}

All cron jobs and their run history live in a SQLite database at:

<workspace_dir>/cron/jobs.db

It uses two tables — cron_jobs (definitions) and cron_runs (execution records) — and is created automatically on first use. Schema migrations are applied automatically when the database is opened (newer columns such as allowed_tools and source are added to older databases on open), so upgrading Revka does not require a manual migration. Deleting a job cascades to its cron_runs rows via ON DELETE CASCADE.

Because every surface writes to this one database, jobs created from the CLI, the agent tools, the gateway API, and declarative config all appear together in revka cron list and GET /api/cron.