[tasks.*]
A task is a unit of work that runs and exits. The TOML key (backup-db
in [tasks.backup-db]) is the task name — it appears in the CLI,
the API, the Web UI, and the on-disk log path. Names must be unique
across both [tasks.*] and [services.*].
Only two fields are truly required: the table itself and run.
Everything else inherits from [defaults]
or has a sensible built-in default.
Minimum example
Section titled “Minimum example”[tasks.heartbeat]cron = "*/5 * * * *"run = "/usr/local/bin/heartbeat"That’s a complete, valid task: it fires every five minutes, captures stdout/stderr to a log file, and persists each run to SQLite.
Identity & metadata
Section titled “Identity & metadata”| Key | Default | What it does |
|---|---|---|
| table key | required | The task name. Used in CLI (runwisp exec <name>), API, log paths. |
run | required | Shell command. Multi-line OK with TOML triple-quotes ("""…"""). |
description | (empty) | Human-readable description shown in the UI and TUI. |
group | "Tasks" | UI grouping label. Set to share a section header with related tasks. |
api_trigger | true | Allow manual triggers from CLI / API / UI. Set false to make the task cron-only. |
Scheduling
Section titled “Scheduling”| Key | Default | What it does |
|---|---|---|
cron | (none) | 5-field cron expression. Supports @hourly, @every 1h30m. |
timezone | inherited | IANA timezone for this task’s cron evaluation (e.g. "Europe/Bratislava"). Falls back to [scheduler] timezone, which itself falls back to the host’s system zone if unset. |
catch_up | "latest" | What to do for missed firings on startup: latest, all, or skip. |
max_catch_up_runs | 100 | Cap on catch-up runs when catch_up = "all". Positive integer. Zero and negatives are rejected at config load. |
Without a cron, the task is manual-only — it runs only when
triggered explicitly. That’s a valid pattern for one-shot deploy hooks
and similar.
See How scheduling works for the full cron grammar and DST behaviour, and the catchup table.
api_trigger = false — cron-only tasks
Section titled “api_trigger = false — cron-only tasks”The default api_trigger = true lets you run the task on demand from
the CLI (runwisp exec <name>), the REST API
(POST /api/tasks/{name}/run), the Web UI’s Run Task button,
and the TUI’s r keybinding.
Setting api_trigger = false says only the scheduler may start this
task. Use it for tasks that are dangerous to fire off-schedule —
overnight rebuilds, idempotency-fragile data jobs, anything that
coordinates with another schedule and would do harm if double-fired by
a curious operator.
[tasks.nightly-merge]cron = "0 4 * * *"api_trigger = false # only fires from cron, never from the UI / API / CLIrun = "/usr/local/bin/merge-shards"What changes when api_trigger = false:
- A
POST /api/tasks/{name}/runreturns 403 Forbidden with the messageAPI triggering disabled for this task. runwisp exec <name>surfaces the same error and exits non-zero.- Cron firings, retries, and
on_overlapbehaviour are unaffected: the scheduler is not “API triggering” and runs the task normally.
api_trigger is independent of cron. A task with api_trigger = false
and no cron is a dead task — it can never start. The loader does
not reject this combination today, but it’s almost certainly a mistake.
Concurrency
Section titled “Concurrency”| Key | Default | What it does |
|---|---|---|
max_concurrent | 1 | Maximum overlapping runs of this task. Positive integer; hard internal cap of 1024. |
on_overlap | "queue" | What happens when a new firing arrives at the max_concurrent limit: queue / skip / terminate. |
queue_max | 100 | When on_overlap = "queue", cap on pending firings. New firings past the cap record end_reason = "queue_full". Positive integer; hard internal cap of 10000. 0 rejected. |
Most cron-style work wants max_concurrent = 1. See
Concurrency policies for when each policy is
right.
Retries & timeout
Section titled “Retries & timeout”| Key | Default | What it does |
|---|---|---|
retry_attempts | 0 | Additional attempts after the initial failure. Non-negative integer; hard internal cap of 100. |
retry_delay | "5s" | Base delay between attempts. Only consulted when retry_attempts > 0. |
retry_backoff | "constant" | Curve applied to retry_delay: constant, linear, or exponential. |
restart | "never" | What to do after a run ends: "never" or "on_failure". "always" is rejected on tasks — use [services.*] for an always-on process. |
timeout | inherited from [defaults] | Per-attempt wall-clock cap. Unset means no timeout. |
graceful_stop | "5s" | SIGTERM grace period before SIGKILL on timeout, on_overlap = "terminate", manual stop, and daemon shutdown. Set "0s" for insta-kill. |
A retry only fires for failed / timeout / crashed. Manual stops
and on_overlap = "terminate" end the chain — see the
terminate ⇄ retries callout. Full
table of formulas on the
Retries & timeouts page.
graceful_stop is process-group-wide: a SIGTERM is delivered to the
task’s process group, every descendant gets the same window, and any
survivors are SIGKILL’d together. If graceful_stop exceeds
[daemon] shutdown_timeout
the daemon emits a boot-time warning — naming the task — because daemon
shutdown will SIGKILL the survivor before the per-task window expires.
Logs & retention
Section titled “Logs & retention”| Key | Default | What it does |
|---|---|---|
log_max_size | 100MB | Per-run log cap. Units: b, kb, mb, gb, tb. Bare 0, negative sizes, and malformed strings are rejected. |
log_on_full | "drop_old" | What to do at the cap: drop_new, drop_old, kill_task. |
keep_runs | inherited from [defaults] | Keep the N most recent runs for this task. Positive integer; hard internal cap of 1 000 000. Omit to inherit; 0 and negatives rejected. |
keep_for | inherited from [defaults] | Delete runs older than this. Omit to inherit; zero and negatives rejected. |
Duration fields throughout runwisp.toml accept s (seconds), m
(minutes), h (hours), d (days), and w (weeks) — e.g. "30s",
"5m", "36h", "30d", "2w". Mixed forms like "1h30m" work too.
Both keep_runs and keep_for apply if both are set; the stricter one
wins in practice. See Logs & retention for the full
picture, including the .prev rotation lifecycle and the fixed
one-hour cleanup cadence.
Notifications
Section titled “Notifications”| Key | Default | What it does |
|---|---|---|
notify_on_failure | (none) | Notifier IDs to alert on run.failed / run.timeout / run.crashed. |
notify_on_success | (none) | Notifier IDs to alert on run.succeeded. |
Each entry is a notifier id declared in a [[notifier]]
block. Whatever channels are listed in [notify] append_notifiers
(default: ["inapp"]) are added automatically — set
append_notifiers = [] to opt out, or ["slack-ops"] to send every
failure to that channel instead. See
Per-task notifications for the field’s full
behaviour.
These two fields are symmetric across [tasks.*] and [services.*] —
same field names, same semantics, same duplicate-removal rules. The
Per-task notifications page covers both
surfaces and is the canonical reference; the
[services.*] notifications section
points back here.
What’s rejected on tasks
Section titled “What’s rejected on tasks”The config loader refuses these at startup so they can’t fail silently later:
restart = "always"— use[services.*].instances = N— services-only field.- A task name that’s also used by
[services.*]— names share one namespace. - Empty or missing
run.
Worked example
Section titled “Worked example”[tasks.process-event-queue]group = "Workers"description = "Drain the queue every 10 minutes with retries"cron = "*/10 * * * *"on_overlap = "skip" # never two queue drainers at oncetimeout = "9m" # die before the next firinggraceful_stop = "30s" # this drainer needs more than the 5s defaultretry_attempts = 3retry_delay = "2s"retry_backoff = "exponential"keep_runs = 200keep_for = "14d"notify_on_failure = ["slack-ops"]run = """set -eutrap 'echo "draining gracefully"; /usr/local/bin/process-queue --drain' TERMecho "Draining queue at $(date -Iseconds)"/usr/local/bin/process-queue"""For a quick start, run runwisp in an empty directory — it offers to
scaffold a minimal runwisp.toml for you.
Where to next
Section titled “Where to next”[services.*]reference — the always-on counterpart.[defaults]reference — what fields fall through.- Concurrency policies, Retries & timeouts, Logs & retention — the conceptual pages behind the keys.