Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Schedules

The worker runs a schedule loop that submits a turn when a schedule is due, so scheduled runs are ordinary durable turns. Schedules live in the store; the worker picks them up without a restart.

Terminal window
pyrula schedules create \
--import app \
--agent nightly_report \
--cron "0 2 * * *" \
--params '{"window": "24h"}'

Give exactly one of --cron or --every.

  • --cron: standard 5-field expression. When both day-of-month and day-of-week are restricted, a day matches if either does (0 0 13 * 5 fires on the 13th and every Friday); if either starts with *, both must match. Out-of-range values and backwards ranges are refused. The next fire is searched 8 years ahead, so 0 0 29 2 * works and 0 0 30 2 * is refused.
  • --every: 90s, 15m, 2h or 1d (bare digits are seconds), 60 seconds to 366 days, on a grid of absolute seconds from creation, so DST and restarts don’t shift it.
  • --params: a JSON object passed as the agent’s inputs.

--agent is the registered name (the function name by default); a schedule for a name no worker serves never fires. create checks it against agents registered in the process, so --import MODULE (repeatable, resolved from --app-dir) loads them first. An unknown name is refused with the nearest matches; --force skips the check when the worker runs elsewhere.

The schedule id is derived from agent, cron or interval, time zone and params. Repeating a create reports already exists and changes nothing; with a different overlap or catch-up policy it prints both and exits 1. Delete and recreate to change a policy.

Terminal window
pyrula schedules create --agent nightly_report --cron "30 2 * * *" --timezone Europe/Berlin

--timezone (alias --tz) is an IANA name, default UTC, and is refused with --every. Cron runs on wall-clock time in that zone:

  • A nonexistent wall time (spring forward) fires once at the first valid instant after the gap (03:00 for 30 2 * * * in Berlin).
  • A repeated wall time (fall back) fires once, at its first occurrence.
  • With * in the hour field, each real matching minute fires, so */15 * * * * keeps its cadence.

An unknown zone is refused at create; a worker that cannot load a stored zone disables the schedule.

--overlap skip (default) drops an occurrence while the previous run is unfinished and logs schedule_skipped_overlap. A run parked for days (an approval, a long sleep) suppresses its schedule. --overlap allow starts it anyway. buffer_one is reserved and refused.

After downtime the schedule fires once for the whole backlog, then moves to the next occurrence after now. --catchup-window 2h (minimum 60 seconds) fires the backlog only if a missed occurrence falls within that window. Default: no window.

Each occurrence is submitted with the idempotency key schedule:{id}:{slot} (slot is its UTC epoch second), so two workers on one tick submit it once. The latest run id is kept as last_run.

Terminal window
pyrula schedules list --agent nightly_report
pyrula schedules pause --schedule-id <id>
pyrula schedules resume --schedule-id <id>
pyrula schedules run-now --schedule-id <id>
pyrula schedules delete --schedule-id <id>
  • list prints id, agent, cron and zone or every ..., next run, overlap, catch-up window and last_run.
  • resume restarts from the first occurrence after now; occurrences missed while paused never fire. A schedule whose cron or zone no longer evaluates stays paused.
  • run-now starts one run with the schedule’s params, paused or not, with trigger manual. Under skip it is refused while the last run is unfinished. It becomes last_run but does not move the next run.

Pyrula Cloud’s Schedules page offers pause, resume and run now with the same rules, sent as control messages to a worker in the schedule’s environment. The worker offers them when its store fires schedules (memory, SQLite and Valkey do), and run now only for agents and workflows it serves. Resending run now does not start a second run.

The worker polls for due schedules (--schedule-poll-interval, 30s default), so a run fires within that window of its occurrence. With no worker running, nothing fires.

View this page as Markdown