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:
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
# 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.