Schedule a workflow
Add a cron schedule to a workflow manifest, register it via the Cori desktop app (or the schedule store), and let the cron driver fire it.
Two things have to happen for a workflow to run on a schedule:
- The manifest declares a default
schedule(and optionallyschedule_tz). - Someone registers the schedule — either through the Cori desktop app or by writing an entry into
~/.cori/schedules/directly. Registration is explicit: aschedule:field in the manifest is inert until something records its intent.
The cron driver — inside the Cori desktop app or cori work — then fires registered schedules whose identity matches its task queue.
1. Add a schedule to the manifest
Add schedule (a cron expression) and schedule_tz (an IANA timezone) to the workflow's manifest frontmatter:
---
id: translate-product-sheets-fr
name: Translate product sheets to French
description: Reads a product CSV and translates descriptions to French.
version: "1.0.0"
created: "2025-01-15"
schedule: "0 3 * * *"
schedule_tz: "Europe/Paris"
parameters:
- name: input_file
type: path
default: ./products.csv
---This declares 3:00 AM Paris time daily as the workflow's default cadence. The fields are validated at manifest-parse time — invalid cron or unknown IANA timezone is a parse error.
Cron syntax
5-field POSIX (minute hour dom month dow) or 6-field with a leading seconds column (sec min hour dom month dow).
┌───── minute (0-59)
│ ┌───── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌───── month (1-12)
│ │ │ │ ┌───── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *| Expression | Meaning |
|---|---|
0 3 * * * | Every day at 3:00 AM |
0 9 * * 1-5 | Weekdays at 9:00 AM |
0 */6 * * * | Every 6 hours |
30 8 1 * * | First of each month at 8:30 AM |
*/30 * * * * * | Every 30 seconds (6-field — note the leading seconds column) |
2. Register the schedule
Open the Cori desktop app, go to Schedules, click New schedule, enter the workflow source. Cron + timezone fall back to the manifest defaults if you leave them blank. The app writes ~/.cori/schedules/<id>.json (id = sha256(source + schedule)[..12]) tagged with your current identity.
You can also create the file by hand:
mkdir -p ~/.cori/schedules
cat > ~/.cori/schedules/$(echo -n "./translate_product_sheets_fr0 3 * * *" | shasum -a 256 | cut -c1-12).json <<'EOF'
{
"source": "./translate_product_sheets_fr",
"schedule": "0 3 * * *",
"schedule_tz": "Europe/Paris",
"identity": { "Person": { "user_id": "<your-os-user>" } },
"enabled": true
}
EOFThe cron driver picks it up on its next 30-second tick.
Consent and version pinning (remote workflows)
A schedule runs unattended, so what it runs is decided when you create it, not silently later:
- Creating a schedule for a remote ref requires the ref to be trusted — the desktop app surfaces its usual first-run trust dialog if it isn't.
- The schedule pins the exact commit you consented to (
resolved_sha). Every fire runs that pin, even if the upstream ref (@v1, a branch) moves. - When the upstream ref does move, the schedule pauses and a "schedule changed" item appears in the desktop app's Inbox showing the old and new commits and the capabilities the new version declares. Approving trusts the new commit, re-pins, and resumes the schedule; declining keeps it paused. Nothing unreviewed ever fires.
- Scheduled fires never auto-approve consent: an untrusted source fails the
fire with
consent_requiredin the schedule's status instead of running.
Two drivers scanning at once (the desktop app and a cori work
terminal) fire each due tick exactly once — fires are claimed atomically.
Timezone semantics
The cron expression is evaluated on the wall clock of schedule_tz
(IANA name, e.g. Europe/Paris); without it, UTC. "Every day at 3:00" with
Europe/Paris fires at 3:00 Paris time year-round, across DST changes.
3. Keep a driver online
The cron driver scans ~/.cori/schedules/ every 30 seconds. For each enabled entry whose identity matches its task queue, it fires the workflow on the due cron tick via cori_run::run_workflow (the same code path as cori run).
A driver is online whenever one of these is running:
- The Cori desktop app (the preferred path — tray-resident, schedules + the worker stay alive as long as the app isn't quit).
- A terminal
cori workprocess (headless; useful on org-infra machines).
Missed fires while no driver is online are lost. v1 does not catch up — if the laptop sleeps through 3:00 AM, the 3:00 AM run does not happen. For unattended schedules, keep the desktop app (or cori work) running on a machine that stays online, or use the operating system's keep-alive (launchd, systemd, etc.).
Identity gating
Each schedule belongs to the identity of whoever created it (your OS user → cori.user.<you>, or a service pool → cori.service.<pool>). Only a driver running under the same identity fires it. To manage shared-pool schedules, run cori work --shared <pool> on the pool host — the desktop app only manages your personal identity's schedules.
Scheduled parameters
Scheduled fires use empty parameters in v1 — the cron driver invokes cori_run::run_workflow with params: {}. The workflow then resolves defaults from the manifest's parameters: block at start time, so every parameter that doesn't have a manifest default will arrive as undefined. For scheduled workflows, give every parameter a sensible default in the manifest.
Checking scheduled run history
cori runs listScheduled runs appear in the run list with trigger: schedule in their trace. Filter by workflow in the desktop app's Runs view.

