Skip to content

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:

Terminal window
export RUNWISP_PASSWORD='…' # the remote daemon's password
runwisp run backup --url https://runwisp.example.com

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

  • It must be reachable. The daemon listens on loopback by default. Bind it with --host 0.0.0.0 and 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 gets 403.

Put the runwisp binary on the machine that triggers the task. It doesn’t need a daemon or a data directory there.

Terminal window
export RUNWISP_URL=https://runwisp.example.com
export RUNWISP_PASSWORD='…'
runwisp run backup # wait and stream output
runwisp run export-org-data --param ORG_ID=acme # fill in a parameter
runwisp 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_PASSWORD to --password. It keeps the password out of shell history and the process list.
  • --json prints the outcome as one JSON document. All flags are in runwisp run.

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 pipefail
TASK=$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, sys
print(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=true holds the request open for up to waitTimeout seconds (default 120, max 240). If the run is still going by then, you get it back with status: "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.

If the task declares parameters, send their values in a params object:

Terminal window
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,
  • null leaves 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.

For runs longer than your HTTP timeout (or your proxy’s), start the run without wait and poll it:

Terminal window
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 5
done

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

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 running
keep_for = "180d"
notify = ["slack-ops"]
params = [{ env = "VERSION", required = true }]
run = """
cd /srv/app
docker pull "ghcr.io/example/app:$VERSION"
docker run --rm --env-file=/etc/app/migrate.env "ghcr.io/example/app:$VERSION" migrate up
docker tag "ghcr.io/example/app:$VERSION" ghcr.io/example/app:current
docker compose up -d --no-deps app
curl -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 list shows it as (manual).
  • on_overlap = "kill": the newest deploy wins. The older run ends as stopped.
  • params: CI passes the version to deploy. See params.
  • notify and [[route]]: notify alerts 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.