Skip to content

Scheduling

Give a task a cron expression and RunWisp starts it on that schedule. Every scheduled start becomes a run in the history, with its exit code and output.

[tasks.heartbeat]
cron = "*/5 * * * *"
run = "/usr/local/bin/heartbeat"

A task without cron runs only when you start it. Scheduled runs use each parameter’s default value.

The usual 5 fields: minute, hour, day of month, month, day of week.

Expression Runs
* * * * * every minute
*/5 * * * * every 5 minutes
0 * * * * at the top of every hour
0 2 * * * every day at 02:00
30 9 * * 1-5 at 09:30, Monday to Friday
0 0 1 * * at midnight on the 1st of every month
@hourly at the top of every hour
@daily / @midnight every day at 00:00
@weekly Sundays at 00:00
@monthly the 1st at 00:00
@yearly / @annually January 1st at 00:00
@every 1h30m every 90 minutes

Fields take lists (1,15), ranges (1-5), and steps (*/10). In the day of week field, both 0 and 7 mean Sunday. @every is a fixed interval that doesn’t line up with the clock.

For more than once a minute, add a seconds field in front. Plain 5-field expressions run at second :00.

Expression Runs
*/30 * * * * * every 30 seconds (:00, :30)
15 * * * * * once a minute, at second :15
0 */5 * * * * every 5 minutes, at second :00

runwisp validate checks every expression without starting anything.

Cron expressions use the host’s timezone unless you set one. Set it for the whole daemon, or per task:

[daemon]
timezone = "Europe/Bratislava" # every task without its own timezone
[tasks.nightly-backup]
cron = "30 2 * * *"
timezone = "Atlantic/Faroe" # this task only

See [daemon] timezone and timezone. An unknown zone name (a typo, or a container image without tzdata) stops the config from loading; RunWisp never falls back to UTC silently. The TUI start screen and the Web UI connection indicator show which zone is in use.

A schedule runs once per wall-clock time, even across a clock change:

  • Clocks go back: a time like 02:30 happens twice, but the task runs once. The second one is recorded in the history as dst_skipped.
  • Clocks go forward: a time that doesn’t exist that day (02:00 when the clock jumps from 01:59 to 03:00) runs once at 03:00, instead of being skipped until the next day.

@every schedules are not affected. They count real elapsed time, not wall-clock times, so @every 30m keeps running every 30 minutes through a clock change.

When the daemon was down (a reboot, a deploy, a crash), scheduled runs didn’t happen. catch_up decides how many of them run when the daemon starts again:

[tasks.metrics-rollup]
cron = "*/15 * * * *"
catch_up = 1 # default: run the most recent missed one

Use 0 for probes that only care about fresh data, and a higher number for jobs where each run handles its own slice of time. A value above 1 needs on_overlap = "queue", or the config is rejected. RunWisp counts missed runs from the task’s last run, or from when it first saw the task, so a new install doesn’t replay old history.

If more runs were missed than catch_up allows, the oldest are dropped and the daemon logs a warning with the task name and how many were dropped.

A missed run counts as a failure by default. On the restart that finds the gap, RunWisp records one run with end_reason = "missed" and sends a failure alert naming the task and how many runs were missed. It goes to the bell and to every channel that gets the task’s failures. This happens whatever catch_up is set to, and the alert always reports the full count.

For a task that is expected to miss runs (a laptop that’s off at night), take missed out of its failures:

[tasks.daytime-sync]
cron = "*/30 9-17 * * 1-5"
failures = ["-missed"] # still recorded, but not a failure and no alert

Put the same line in [defaults] to do it for every task.

When many tasks share a start time (say, 0 3 * * *), they all start at the same second. jitter lets RunWisp spread them out:

[tasks.backup-a]
cron = "0 3 * * *"
jitter = "30m"
[tasks.backup-b]
cron = "0 3 * * *"
jitter = "30m"

RunWisp starts jittered tasks one at a time. If nothing else jittered is running, a task starts right on time. If several are due together, they take turns, and none starts later than its jitter. Each task’s latest start time is fixed when the daemon starts, so the same config always gives the same order. The run history shows both the scheduled time and the real start.

Pause a task to stop its schedule without editing runwisp.toml, for example while something it depends on is down:

Terminal window
runwisp pause nightly-backup
runwisp resume nightly-backup

In the Web UI, click the schedule next to the task name at the top of its page. In the TUI, press p on the task. The REST API has POST /api/tasks/{task}/pause and .../resume.

While a task is paused, its scheduled times are skipped. They add no runs and are not caught up when you resume or when the daemon restarts. You can still run the task by hand. runwisp status lists paused tasks.

The pause survives runwisp reload and restarts. Pausing needs manual_trigger on, and a reload that removes the task’s cron or sets manual_trigger = false clears the pause with a warning. A task cron still runs (held) can’t be paused.

On a normal shutdown, every run gets a final status. After a hard crash (power loss, kill -9) the daemon can’t do that, so on the next start every run still marked as running is ended as crashed with exit code -2. These runs are not resumed. Catch-up may then start a fresh run. No run is ever left stuck on “running”.

Jobs read from a crontab with include_cron follow cron’s rules: a missed job is recorded but never run later.

RunWisp reads runwisp.toml at startup and on runwisp reload, never on its own. A task added by a reload gets no catch-up. See Reload & restart.