What RunWisp is
RunWisp is an open-source, self-hosted cron job manager and process supervisor. It ships as a single static Go binary with no runtime dependencies.
You trade the usual crond and supervisord setup for one runwisp.toml, plus a web dashboard, a terminal UI, and a REST API.
Each run keeps an exit code, duration, timestamps, and full stdout/stderr. Full docs are at docs.runwisp.com.
What RunWisp does well
RunWisp is built for operations: schedules, retries, timeouts, retention, notifications, overlap policy, and live log streaming.
You point a task at a command. RunWisp runs it on a cron (or keeps a service alive), captures the output, and stores history you can search later.
That layer is the product. Packaging jobs is not.
The gap this project fills
After a while the scheduler stops being the hard part. The hard part is knowing what “the job” actually is.
Shell-only packaging picks up silent assumptions: working directory, required env vars, files that must exist, argv that only half lives in a README. Sometimes that knowledge is only in a teammate’s head.
You find those gaps when the job fails at 2 AM. RunWisp will show the logs. It will not tell you the package was incomplete before the run started.
RunWisp watches execution. Something else has to own a job contract you can check ahead of time.
Why runwisp-jobkit exists
I built runwisp-jobkit so a RunWisp job is a filesystem package instead of a fragile shell one-liner.
A job is a directory: a job.toml manifest plus the files the command needs. Humans and agents share one convention. RunWisp stays the place schedules and history live.
The harness validates the package, then runs the command without a shell. No string splicing, no surprise metacharacter expansion. Arguments land as argv.
What it solves
The failures I kept hitting once jobs left a single laptop:
- Incomplete packages: missing files and blank required env vars fail in
doctorbefore anything executes. - Implicit contracts: cwd, argv, env, and required files live in one typed manifest instead of tribal knowledge.
- Shell glue drift: Python, TypeScript/Bun, Rust, and Shell packages share one command model instead of each growing its own wrapper script.
Why this shape
doctor never runs the job. It only does passive preflight: manifest and confined paths, required env names present and nonblank, required files readable, and executable availability. It does not prove the job will succeed at runtime. Catching contract gaps early beats debugging them in production.
Policy stays out of the harness on purpose. Schedules, secrets, retries, and notifications stay with RunWisp and the deployment environment, so the package stays portable and reviewable.
Shell-free exec keeps process behavior honest. After process replacement, stdout, stderr, exit codes, and signals come from the job, which is what RunWisp’s observability is meant to record.
Install
runwisp-jobkit 0.1.0 is on PyPI. Source, releases, and authoring docs are at github.com/engineersamuel/runwisp-jobkit.
uv tool install --python 3.14 runwisp-jobkit
runwisp-job --help
From a local checkout at the repo root:
uv tool install --force --python 3.14 .
Wire a RunWisp task with an explicit filesystem pointer, for example:
[tasks.example-job]
cron = "0 6 * * *"
run = "runwisp-job run /path/to/jobs/example"
RunWisp still owns schedule and retention. The package owns behavior and declared inputs.
The contract: job.toml
Every job directory has a manifest. Complete example with all seven fields:
schema = 1
id = "example-job"
kind = "command"
cwd = "."
argv = ["python", "run.py"]
required_env = ["EXAMPLE_MESSAGE"]
required_files = ["run.py"]
| Field | Role |
|---|---|
schema |
Manifest version (1 today) |
id |
Nonempty diagnostic label for doctor and error messages; not a registry key |
kind |
Execution kind (command today) |
cwd |
Working directory relative to the package |
argv |
Command and arguments to exec |
required_env |
Env vars that must be present and nonblank |
required_files |
Files that must exist and be readable |
Unknown fields are rejected, so a typo fails closed instead of quietly changing behavior.
cwd and required_files must stay inside the job directory. Absolute paths, parent traversal, and symlink escapes are rejected for those typed paths. The harness is not a sandbox for job code.
CLI: doctor, then run
runwisp-job doctor JOB_DIR
runwisp-job run JOB_DIR [ARG ...]
doctorruns the same passive preflight without executing the job.runappends each supplied argument to the manifest command unchanged, then replaces the harness process with the job process.
That split keeps “does this package pass preflight?” separate from “execute it.”
Everything after JOB_DIR on run is forwarded as argv, so RunWisp parameters can pass through:
runwisp-job run /path/to/job -- --dry-run
There is no shell parsing in the harness. Quotes and metacharacters have no special meaning unless the job itself puts a shell in argv.
Minimal Python job
The repository’s Python template is a tiny package like this:
python/
job.toml
run.py
schema = 1
id = "python-example"
kind = "command"
cwd = "."
argv = ["uv", "run", "--script", "run.py"]
required_env = ["RUNWISP_EXAMPLE_MESSAGE"]
required_files = ["run.py"]
# /// script
# requires-python = ">=3.14"
# ///
import argparse
import os
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
arguments = parser.parse_args()
message = os.environ["RUNWISP_EXAMPLE_MESSAGE"]
if arguments.dry_run:
print(f"dry-run: {message}")
else:
print(message)
Validate and run:
export RUNWISP_EXAMPLE_MESSAGE="hello from runwisp"
runwisp-job doctor ./python
runwisp-job run ./python -- --dry-run
doctor should pass only when the env var is set, run.py is present and readable, and uv is available on PATH. That is how you catch an incomplete package before the schedule fires.
Language templates
The repository includes templates you can run immediately:
- Python
- TypeScript (Bun)
- Rust
- Shell
One command model covers all of them. Language choice stays inside the package; the harness only sees argv.
There is also a sanitized nightly news example based on a production-style job. Some private dependencies are left out on purpose, so treat it as a design reference more than a one-click demo.
What the harness does not do
I kept the surface small on purpose.
The harness does not:
- Install job dependencies
- Discover or register jobs
- Store secrets
- Manage RunWisp schedules, retries, or notifications
- Merge packages into
runwisp.toml
Packages and their deployment environment own those concerns. RunWisp is still the operational control plane. jobkit is the package contract and runner.
Security boundary
Path confinement applies only to the manifest cwd and declared required files. Job code still has the filesystem, network, and environment access of its OS account.
Authors install runtimes and dependencies, declare required inputs, validate forwarded args, and return useful exit codes. The harness fails closed on the contract; it is not a jail.
Exit codes
2: harness response for CLI usage or job configuration failures before execution; a running job can also exit2126: executable became non-runnable or had an invalid executable format at process replacement127: executable missing at process replacement
Other job exit codes pass through unchanged, so RunWisp history matches the real process.
Try it
If your job contracts still live in shell glue, start with one package:
- Drop a
job.tomlnext to the entrypoint - Declare
required_envandrequired_fileshonestly - Run
runwisp-job doctoruntil it is clean - Point a RunWisp task at
runwisp-job run /path/to/job - Trigger once from the UI or TUI and compare the failure mode to bare shell
More detail: project page, GitHub repo, authoring docs. The v0.1.0 release ships one wheel and one sdist.
Install with uv tool install --python 3.14 runwisp-jobkit, doctor a real job directory, and write down every assumption that used to be invisible. That list is why I wrote this.