Remote triggers
Run a task on a daemon on another machine, from a script, a CI job, or your laptop, and get its output and exit code back:
export RUNWISP_PASSWORD='…' # the remote daemon's passwordrunwisp run backup --url https://runwisp.example.comThis logs in, starts backup, streams its output to your terminal, and exits
with the task’s exit code. So runwisp run backup --url … && ./deploy.sh stops
when the backup fails. The run is stored in the daemon’s history like any
scheduled run.
What the daemon needs
Section titled “What the daemon needs”- It must be reachable. The daemon listens on loopback by default. Bind it
with
--host 0.0.0.0and turn on TLS or put it behind a reverse proxy. Without TLS, the session token and the task output cross the network in plain text. - The task must allow manual runs. That’s the default. With
manual_trigger = false, the trigger gets403.
With the runwisp CLI
Section titled “With the runwisp CLI”Put the runwisp binary on the machine that triggers the task. It doesn’t need a
daemon or a data directory there.
export RUNWISP_URL=https://runwisp.example.comexport RUNWISP_PASSWORD='…'
runwisp run backup # wait and stream outputrunwisp run export-org-data --param ORG_ID=acme # fill in a parameterrunwisp run backup --detach # print the run ID and exit- The CLI caches the session token in your user cache directory
(
~/.cache/runwisp/on Linux,~/Library/Caches/runwisp/on macOS) and reuses it, so frequent triggers don’t hit the login rate limit. - Prefer
RUNWISP_PASSWORDto--password. It keeps the password out of shell history and the process list. --jsonprints the outcome as one JSON document. All flags are inrunwisp run.
With curl
Section titled “With curl”When you can’t install the binary, use the REST API. This script needs curl,
jq, and python3:
#!/usr/bin/env bash# runwisp-trigger.sh TASK: run TASK on a RunWisp daemon and wait for it.# Needs RUNWISP_URL and RUNWISP_PASSWORD exported.set -euo pipefailTASK=$1
# 1. Log in: answer the challenge with PBKDF2-HMAC-SHA256(password, nonce).NONCE=$(curl -sSf "$RUNWISP_URL/api/auth/challenge" | jq -r .nonce)RESP=$(python3 -c 'import hashlib, os, sysprint(hashlib.pbkdf2_hmac("sha256", os.environ["RUNWISP_PASSWORD"].encode(), sys.argv[1].encode(), 600000).hex())' "$NONCE")TOKEN=$(curl -sSf -X POST "$RUNWISP_URL/api/auth/login" \ -H 'Content-Type: application/json' \ -d "$(jq -nc --arg n "$NONCE" --arg r "$RESP" '{nonce: $n, response: $r}')" \ | jq -r .token)
# 2. Start the task and wait for it to finish.RUN=$(curl -sSf -X POST "$RUNWISP_URL/api/tasks/$TASK/run?wait=true" \ -H "Authorization: Bearer $TOKEN")
jq '{id, status, endReason, exitCode}' <<<"$RUN"[ "$(jq -r .endReason <<<"$RUN")" = succeeded ]- The response is the run:
id,status,endReason,exitCode, and more. The script exits non-zero unless the run succeeded. wait=trueholds the request open for up towaitTimeoutseconds (default120, max240). If the run is still going by then, you get it back withstatus: "running".- The token is valid for 24 hours. Reuse it across calls instead of logging in each time, or you’ll hit the rate limit.
Passing parameters
Section titled “Passing parameters”If the task declares parameters, send their values in a
params object:
curl -sSf -X POST "$RUNWISP_URL/api/tasks/export-org-data/run?wait=true" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"params": {"ORG_ID": "acme", "--region": "eu", "--include-attachments": "true"}}'The keys are what you declared: the env or arg name, or the option or
flag token. For each key:
- a string (even
"") passes that value, nullleaves the parameter out, even if it has a default,- a missing key uses the declared default.
Values reach your command as arguments or environment variables, never as shell code.
Long runs: poll or stream
Section titled “Long runs: poll or stream”For runs longer than your HTTP timeout (or your proxy’s), start the run without
wait and poll it:
RUN_ID=$(curl -sSf -X POST "$RUNWISP_URL/api/tasks/backup/run" \ -H "Authorization: Bearer $TOKEN" | jq -r .id)
until [ "$(curl -sSf "$RUNWISP_URL/api/runs/$RUN_ID" \ -H "Authorization: Bearer $TOKEN" | jq -r .status)" = ended ]; do sleep 5doneFor live output, follow GET /api/runs/{id}/log/stream, a server-sent event
stream with line events and a final done event. The
API reference has every endpoint.
Deploy hooks from CI
Section titled “Deploy hooks from CI”A deploy hook is a task with no cron that your CI starts after a build. Every
deploy is stored as a run with its output and exit code, and anyone can re-run
it from the Web UI or TUI.
[tasks.deploy-app]group = "Deploys"description = "Pull the new image, migrate, restart"on_overlap = "kill" # a new deploy replaces one that's still runningkeep_for = "180d"notify = ["slack-ops"]params = [{ env = "VERSION", required = true }]run = """cd /srv/appdocker pull "ghcr.io/example/app:$VERSION"docker run --rm --env-file=/etc/app/migrate.env "ghcr.io/example/app:$VERSION" migrate updocker tag "ghcr.io/example/app:$VERSION" ghcr.io/example/app:currentdocker compose up -d --no-deps appcurl -fsS --max-time 10 https://app.example.com/healthz"""
[[route]]match = { task = "deploy-app", kinds = ["succeeded"] }notifiers = ["slack-deploys"]- No
cron: the task runs only when triggered.runwisp listshows it as(manual). on_overlap = "kill": the newest deploy wins. The older run ends asstopped.params: CI passes the version to deploy. Seeparams.notifyand[[route]]:notifyalerts on failures. The route also announces successes to the team channel. See[[route]].
In CI, trigger it with runwisp run. In GitHub Actions:
- name: Deploy env: RUNWISP_URL: https://runwisp.example.com RUNWISP_PASSWORD: ${{ secrets.RUNWISP_PASSWORD }} run: npx --yes runwisp run deploy-app --param VERSION=${{ github.sha }}The step fails when the deploy fails, and its output shows in the CI log. If the
runner can’t run the binary, call the curl script with a
params body instead.
To run migrations on their own, use a second manual task with
on_overlap = "skip". Killing a migration halfway is dangerous; with skip, a
trigger that arrives while one is running is recorded as skipped and doesn’t
start.
Secrets the task needs live on the RunWisp host, not in CI. Keep the data
directory and any env_file readable only by
the daemon’s user.