Skip to content

[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.

[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.

KeyTypeDefaultWhat it does
table keystringrequiredThe task name. Used in CLI (runwisp exec <name> / runwisp run-task <name>), API, log paths.
runstringrequiredShell command. Multi-line OK with TOML triple-quotes ("""…"""). Trusted input — see note.
descriptionstring(empty)Human-readable description shown in the UI and TUI.
groupstring"Tasks"UI grouping label. Set to share a section header with related tasks.
api_triggerbooltrueAllow manual triggers from CLI / API / UI. Set false to make the task cron-only.
KeyTypeDefaultWhat it does
cronstring(none)5-field cron expression. Supports @hourly, @every 1h30m.
timezonestringinheritedIANA timezone for this task’s cron evaluation (e.g. "America/New_York"). Falls back to [scheduler] timezone, which itself defaults to UTC.
catch_upenum"latest"What to do for missed firings on startup: latest, all, or skip.

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.

KeyTypeDefaultWhat it does
parallelismint1Maximum concurrent runs of this task. Must be > 0.
on_overlapenum"queue"What happens when a new firing arrives at the parallelism limit: queue/skip/terminate.

Most cron-style work wants parallelism = 1. See Concurrency policies for when each policy is right.

KeyTypeDefaultWhat it does
retry_attemptsint0Additional attempts after the initial failure. Must be >= 0.
retry_delayduration5s (when retries are on)Base delay between attempts. Go duration string.
retry_backoffenum"constant"Curve applied to retry_delay: constant, linear, or exponential.
timeoutdurationinherited from [defaults]Per-attempt wall-clock cap. 0 or unset means no timeout.

A retry only fires for failed / timeout / crashed. Manual stops and on_overlap = "terminate" end the chain. Full table of formulas on the Retries & timeouts page.

restart may be set to "never" (default) or "on_failure". Setting restart = "always" on a task is rejected at config load — use [services.*] for an always-on process.

KeyTypeDefaultWhat it does
log_max_sizebyte size100MBPer-run log cap. Units: b, kb, mb, gb, tb. 0 is unbounded.
log_on_fullenum"drop_old"What to do at the cap: drop_new, drop_old, kill_task.
keep_runsintinherited from [defaults]Keep the N most recent runs for this task. 0 (or omitted) inherits the default; -1 is explicit unlimited; positive N keeps N.
keep_fordurationinherited from [defaults]Delete runs older than this. Accepts d and w units (30d, 2w). Omit to inherit; "unlimited" is explicit unlimited.

Both keep_runs and keep_for apply if both are set; the stricter one wins in practice. See Logs & retention for the full picture.

KeyTypeDefaultWhat it does
notify_on_failurestring[](none)Notifier IDs to alert on run.failed / run.timeout / run.crashed.
notify_on_successstring[](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] default_notifiers (default: ["inapp"]) are appended automatically — set default_notifiers = [] to opt out, or ["slack-ops"] to make every failure page Slack instead. See Per-task notification sugar for how this expands.

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.
[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 once
timeout = "9m" # die before the next firing
retry_attempts = 3
retry_delay = "2s"
retry_backoff = "exponential"
keep_runs = 200
keep_for = "14d"
notify_on_failure = ["slack-ops"]
run = """
set -eu
echo "Draining queue at $(date -Iseconds)"
/usr/local/bin/process-queue
"""

The full annotated example file lives at apps/runwisp/runwisp.example.toml or you can drop a fresh copy with runwisp init.