Skip to content

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.

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 in skip_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 now

Payload 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 + index idx_workflow_schedules_due on (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.

AI that follows a recipe, not a conversation.