Feature list
Everything Walis does today, grouped by what you would go looking for. Each line is a feature that exists in the shipping build — not a roadmap.
Jobs and scripts
| Feature | Notes |
|---|---|
| Two authoring modes | Script mode is a shell buffer you edit directly. Flow mode builds the script from actions. |
| 33 built-in actions | Eight categories: general, files, Git, Node, .NET, containers, deployment, checks. See Actions. |
| Custom actions | A parameter declaration plus a shell template, stored as JSON. No code. |
| Compiled, not interpreted | A flow compiles into Jobs.Command on save, so runners never need to know actions exist. |
| AI script review | Sends the script to your configured provider and reports back what looks wrong. |
| Import and export | Jobs and custom actions as JSON, matched by name, with an overwrite switch. Identifiers and build history stay put. |
| Import from Jenkins | Paste a Jenkinsfile (declarative or scripted) or a job's config.xml; Walis converts it to a draft with a warning list and opens it in the editor. Nothing is written until you save. See the mapping table. |
| Build parameters | Up to 50 per job: string, text, choice, boolean, password. Manual runs prompt; schedules use the defaults. Unknown names are rejected, not ignored. |
| Server-side lists | Paging, sorting, searching and filtering run in SQL, so the job list behaves the same at thirty jobs and at three thousand. |
Switching from flow mode back to script mode keeps the compiled script; switching the other way restores the steps. Manual edits made in script mode are not preserved across a switch back to flow.
Builds and steps
| Feature | Notes |
|---|---|
| Per-step status | Every step keeps its own status, output, exit code and duration. |
| Independent shells | Steps share a working directory but not environment variables. Append KEY=VALUE to $WALIS_ENV_FILE to pass values on. |
| Step dependencies | A flow is a directed acyclic graph: a step declares what it depends on, and anything not waiting on anything else runs at the same time — up to 8 steps per build. List view or graph view. |
| Step outputs | A step writes name=value to $WALIS_OUTPUT_FILE; later steps reference it as ${{ steps.ID.NAME }}. Values are injected as environment variables, never spliced into the script text. Recorded on the build and shown in the log. |
| Run conditions on data | An expression over earlier steps' outputs and the environment decides whether a step runs. Two branches with opposite conditions off one parent is an if/else. |
| Run conditions | Each step runs only on success (default), always, or only after a failure — Jenkins' post {}. A failure no longer aborts the build: cleanup and notification steps still run, ordinary ones are marked Skipped, and the result is still Failed. |
| Per-step retries | 0–10 further attempts, each a fresh shell with the earlier output kept above it. Retries draw on the job timeout rather than extending it. |
| One build at a time | Jenkins' disableConcurrentBuilds(). The gate lives in the same transaction as the Queued → Running move, so two nodes racing cannot both win; the loser re-queues instead of dropping the build. |
| Downstream jobs | Up to 20 targets per job, on success / on failure / always, matched by name. The downstream build gets WALIS_UPSTREAM_JOB, _BUILD and _STATUS. Chains stop at ten deep, and the upstream job's run role gates the trigger. |
| Build matrix | {"NODE":["20","22"],"OS":["mac"]} fans out into one build per combination, capped at 50. |
| Build cache | Named paths are saved on success and restored before the next run — after the workspace is cleaned, not before. |
| Artifacts | Copied out on success, capped at 200 files / 64 MB, downloadable from the log window. |
| Live output | stdout and stderr interleave in real order; the log window re-fetches every two seconds and follows the tail until you scroll up. |
| Timeout | A whole-build budget. Each step receives whatever time is left. |
| Retention | Builds, artifacts and audit entries share one retention setting, swept hourly. |
continueOnError on an action lets the build carry on past a failure without the build being marked failed at all.
Triggers
- Manual — a button on the job list and job detail.
- Push —
POST /api/webhooks/pushaccepts GitHub, GitLab and a plain{repository, branch}body, matched against jobs by branch and normalised repository address. Signatures verified viaX-Hub-Signature-256,X-Gitlab-TokenorX-Walis-Tokenonce a webhook secret is set. - Cron — five-field expressions evaluated once a minute in the configured time zone.
These are mutually exclusive — a job has one trigger kind. Running a job by hand is separate and always available, so manual really means "no automatic trigger", not "the only way to start it".
Runners
| Feature | Notes |
|---|---|
| Built-in local runner | Runs in the API process. Concurrency comes from the system setting. |
| Remote agents | walis-runner polls, claims work, and reports back. No runtime to install — it is a self-contained binary. |
| Shared step loop | BuildStepSequence is the same code locally and remotely; only the sink differs (database vs HTTP). |
| Labels and slots | Free-form labels, plus a slot count per runner. |
| Heartbeat | Reported every minute; runners silent for five minutes are marked offline and can raise a notification. |
Databases and high availability
| Provider | Multiple nodes | Notes |
|---|---|---|
| SQLite | No | The default. One file, nothing to run. |
| PostgreSQL | Yes | |
| MySQL / MariaDB | Yes | |
| SQL Server | Yes | Azure SQL included. |
Point several API nodes at one server database and they share the work:
- Atomic build claiming — two nodes can race for the same queued build; exactly one wins the
Queued→Runningtransition. - Lease-based leader election — cron evaluation, offline detection, retention cleanup and scheduled backups run on one node at a time. Leases expire, so a dead holder is replaced without a separate coordination service.
- Cross-node polling — a build queued on one node is picked up by whichever node has capacity.
- Per-node recovery — a restarting node recovers only its own interrupted builds.
GET /health reports the provider, the node name and whether the deployment is clustered. Migrating from SQLite is a backup and a restore, because the archive is engine-agnostic. See High availability.
Isolation and build safety
Builds run as the same user as the service, so these are the defences that matter.
On every platform:
- Allowed working directories — when the list is non-empty, a build whose working directory is outside it fails immediately rather than running somewhere unexpected.
- Environment allow-list — a build does not inherit the service's environment. It receives
PATH,USER,LOGNAME,SHELL, locale, temp directories and the TLS certificate variables, and nothing else, so connection strings and service secrets are not there to be read. Names are matched case-sensitively on POSIX, because treatingPathasPATHwould let a differently-cased secret through. - Workspace cleaning — clears directory contents only, and refuses disk roots, home directories and suspiciously shallow paths.
On macOS:
- Sandbox — with
IsolateBuildson (the default), each step is wrapped insandbox-execand cannot read or write the Walis data directory, the database directory, backup destinations, or any path you list. - Symlink resolution — sandbox paths are resolved segment by segment, so rules on
/tmpand/var/foldersactually bite.
Everywhere else, a build runs unisolated by default. sandbox-exec has no equivalent that Walis uses on Windows or Linux, and a remote runner applies no sandbox of its own, so there a script runs with the permissions of the account that runs it. That is the default so that a first build works on every platform. The build log says so in its first lines, and the settings page shows a warning when it applies to the built-in runner.
An administrator can require a boundary instead, with Refuse to run without an isolation boundary in Settings → Build. From then on:
- The built-in runner on Windows or Linux refuses to start a build: "isolation is required but this runner has no sandbox to apply."
- A remote runner is handed work only if it is isolated by something outside Walis (a container, a VM), declares so, and an administrator lists its name under Externally isolated runners. A runner claiming
externalisolation that is not on that list is refused — the header alone is not trusted.
Neither layer is real isolation — same user, same machine. They stop a build script from casually reading the database or credentials. For stronger boundaries, run builds on a dedicated runner under its own account, or in a container your script starts.
Access control
| Feature | Notes |
|---|---|
| Optional sign-in | Off by default. With it off, everyone is treated as an administrator. |
| Password storage | PBKDF2-SHA256. Sessions live in the database. |
| One-time passwords | TOTP with backup codes; can be required for all accounts. |
| Roles | viewer, operator, administrator. |
| Per-job permissions | Each job has a view role and a run role. |
| Quiet 404s | A job you cannot see returns 404, not 403 — the identifier does not confirm the job exists. |
| API keys | walis_<prefix>_<124-bit secret>, stored only as a domain-separated SHA-256 lookup hash and shown once. Scoped mcp (the safe default, /mcp only), api, or runner — a runner key is bound to one runner name and can only register, heartbeat, claim and report for it. No API key can touch sign-in, accounts or key lifecycle. |
Plugins
Action plugins run in Jint, a pure-C# JavaScript interpreter, and return a shell snippet rather than behaviour — so you can read what a plugin will run before installing it.
- Declared capabilities only — network access is limited to named domains; secrets can only be injected into headers via
withSecret, never read by the plugin. - Resource ceilings — 10 s timeout, 8 MB memory, 500 000 statements, 64 recursion levels.
- AI drafting — generated plugins have permissions cleared, type and version forced, and are never auto-installed.
- Administrator only — installing a plugin means adding code that will run.
Notifications
Email, Slack (incoming webhook) and a custom JSON webhook, each independently switchable and testable. Five events: build failed, build recovered, every successful build, build timed out, runner offline. Quiet hours suppress everything except failures. Delivery history is kept.
AI assistance
Claude, Gemini and Codex. Claude uses the official Anthropic SDK; the other two use their REST endpoints. Used for script review, build failure analysis and plugin drafting. Every call is logged with provider, model, purpose, duration, token usage and result — prompt content is never stored.
Backup and restore
- Scheduled by cron, encrypted with an authenticated format (PBKDF2-SHA256, AES-256-GCM chunks) — a modified or truncated archive is rejected before anything is restored.
- Packs the database (
VACUUM INTO), artifacts, extra paths you list, and optionally PostgreSQL viapg_dump. - Destinations: local path, S3-compatible (self-signed SigV4), Dropbox, Google Drive, OneDrive. None shell out to an external binary.
- Restore is database-agnostic and can target a different database engine than the source.
A passphrase is mandatory once a remote destination exists, because the archive contains password hashes and credentials.
Observability
- Dashboard — active jobs, 7-day success rate, running count, failure count, build trend, recent builds, runner load and per-job success rate.
- Prometheus —
GET /metrics, deliberately outside/apiso a local agent scrapes a stable path, but still administrator-gated by the authentication middleware. The output is aggregate numbers plus job names. - Audit log — jobs, builds, runners, environment variables, settings, notifications and translations, sharing the retention setting.
Integrations
- REST API for everything the UI does.
- MCP served at
/mcpby the API itself, so an agent needs only a URL and a key — nothing installed locally.Source/Walis.Mcpis a stdio bridge for clients that cannot speak HTTP MCP. - Portability — export and import jobs and custom actions as reviewable JSON.
Localization
Traditional Chinese is the source language; English and Japanese ship as dictionaries, overlaid at load time with any overrides you set. Translations are editable in the app, importable and exportable as JSON, with an auto-translate pass for missing entries.
Deployment
deploy/release.sh publishes self-contained binaries — the target machine needs no .NET — for Windows (x64, arm64), macOS (Intel, Apple Silicon) and Linux (x64, arm64, 32-bit arm for Raspberry Pi, and musl/Alpine for both architectures). Each target gets an archive containing the API with the admin UI built in, plus the runner, and the release ships a SHA256SUMS.txt.
On macOS, deploy/install.sh registers two LaunchDaemons: the service (running as an ordinary account, not root) and a restarter watching a trigger file. On other platforms, run the binary under whatever init system you already use. The built-in self-update job pulls, builds, swaps a symlink and touches that file — restarts happen outside the build's process tree, so the build survives long enough to record its own result.
The API serves the admin UI from wwwroot, so the interface and the API share an origin and a port.