Appearance
Schedules
A schedule is an automated timer pinned to a workflow. It tells the kitchen manager to make a specific dish on a specific cadence: every weekday at 8 AM, every Tuesday at 4 PM, every fifteen minutes if you really want. The manager checks the clock every minute to see if anything is due, then fires the workflow.
If your machine was off when a fire was supposed to happen, you decide what comes next: skip the missed fire entirely (the default), or hurry up and replay everything you missed. If a fire is due but the same workflow is still running from a previous fire, Z.E.N. notes the conflict and waits. The schedule keeps ticking either way.
You can attach a schedule two ways. The simplest is dropping a schedule: block at the top of the workflow file; the next time Z.E.N. reads the file, the schedule is live. The other way is the zen schedule CLI, which lets you add, pause, fire, and remove schedules without editing the YAML.
Advanced: cron syntax, catch-up behavior, and the schedule API
Z.E.N. workflows can run on a cron schedule, either declared in YAML and reconciled on workflow load, or managed imperatively via the CLI and HTTP API. The runner lives inside the server process and ticks every 60 seconds against ~/.zen/zen.db.
YAML schedule (recommended)
Pin a schedule to a workflow by adding a schedule: block:
yaml
name: nightly-review
description: Review yesterday's PRs and post a summary
schedule:
cron: "0 4 * * *" # 5-field cron; minute hour day month weekday
timezone: "America/New_York"
catch_up: skip-to-now # or: replay-missed-fires
enabled: true
nodes:
- id: summarize
type: prompt
prompt: |
Review yesterday's merged PRs in this repo. Post a summary.On workflow load (server boot or hot reload), Z.E.N. reconciles the YAML schedule into the database; new entries are created, changes are picked up, and removed schedule: blocks orphan their DB rows so they stop firing. The schedule is bound to {codebase, workflow_name}.
cron
Standard 5-field cron (minute hour day-of-month month day-of-week). Powered by croner; handles ranges, lists, steps, DST transitions correctly.
timezone
Any IANA zone (America/New_York, Europe/Berlin, UTC, …). DST shifts are handled per-zone; missing local times never fire twice.
catch_up
What happens when the server is down across one or more fire times:
skip-to-now(default); record the missed window inskip_count, fire once at next scheduled time.replay-missed-fires; fire once per missed window, back-to-back. Use sparingly: a server down for a week with a* * * * *schedule will fire 10,080 times.
enabled
Default true. Set to false to keep the schedule in YAML but stop firing.
CLI
bash
# Add an ad-hoc schedule (not pinned to YAML)
zen schedule add nightly-review --cron "0 4 * * *" --timezone "America/New_York"
# List all schedules across codebases
zen schedule list
# Pause / resume
zen schedule disable <id>
zen schedule enable <id>
# Force a fire now (respects the working-path lock)
zen schedule fire <id>
# Inspect recent fires + skip counts
zen schedule history <id>
# Delete
zen schedule remove <id>The CLI talks to the local SQLite by default. Set ZEN_REMOTE_URL=http://localhost:3090 to route through the launchd-managed daemon.
HTTP API
The server exposes /api/schedules for programmatic use:
GET /api/schedules # list
POST /api/schedules # create
GET /api/schedules/:id # detail
PATCH /api/schedules/:id # update
DELETE /api/schedules/:id # delete
GET /api/schedules/:id/history # last N fires
POST /api/schedules/:id/fire # force fire nowPayload shape mirrors the YAML block plus workflow_name, codebase, and ID fields. See API Reference.
How fires interact with the working-path lock
Scheduled fires use the same dispatch path as CLI and chat triggers; they go through dispatchBackgroundWorkflow, hit validateAndResolveIsolation, and respect the working-path lock. If a workflow is already active on the same (working_path, workflow_name), the fire is cancelled with Workflow already active on this path status (not failed) and skip_count is incremented.
For schedules on a shared cwd that need to run in parallel with other workflows, set concurrency.allowParallelWorkflows: true in .zen/config.yaml; the lock scopes by (working_path, workflow_name) so different-name workflows on the same path no longer block each other.
Storage
- SQLite (default): table
workflow_schedules+ indexidx_workflow_schedules_dueon(enabled, next_fires_at). - Postgres: same shape; see migration
023_workflow_schedules.sql.
Schedules survive restarts. The runner skips ticks when one is already in-flight, and snapshots its dependencies at tick entry so a stopScheduleRunner() during a long fire is safe.