Turn scripts into,infrastructure.
An open-source developer platform that wraps your TypeScript, Python, Go, and Bash scripts as APIs, cron jobs, flows, and internal UIs — code first, with auto-generated forms, managed secrets, and permissions. This guide covers the core concepts, self-hosting with Docker, your first script, and how KBVE triggers one-off jobs straight from Discord.
Code first, low-code second
You write plain scripts; Windmill generates the input form from the function signature and wraps each one as an API, cron, or UI. Flows then chain scripts together visually when you need orchestration.
- Scripts — a single function, wrapped as an API.
- Flows — scripts chained into a multi-step DAG.
- Apps — internal UIs built on top of scripts.
On this page
What this guide covers
Concepts
Scripts, flows, apps, workspaces, triggers, and workers — the vocabulary of the platform.
Self-Host
A Docker Compose deployment with Postgres and a scalable worker pool.
First Script
Write a function, let Windmill generate the form, run it as an API or on a schedule.
KBVE Workflows
How we trigger one-off Windmill scripts from Discord with the /wm command.
Deployment
The monorepo-to-cluster sync pipeline, and what it took to ship /wm npm.
State & Runs
Where Windmill keeps every job — the schema, the run tables, and the lifecycle of one execution.
Start here
Overview
Windmill is an open-source developer platform that turns plain scripts into infrastructure. Instead of wiring visual nodes, you write a function in TypeScript, Python, Go, or Bash — and Windmill wraps it as an API, a cron job, a step in a flow, or an internal UI, generating the input form straight from your function signature.
Because it is self-hostable and code-first, your logic lives in version-controlled files and your data and secrets stay on your own infrastructure.
This guide covers core concepts, self-hosting, your first script, and how KBVE runs workflows.
Platform vocabulary
Concepts
| Term | Meaning |
|---|---|
| Script | A single function in TS/Python/Go/Bash, wrapped as a runnable API |
| Flow | A multi-step DAG that chains scripts together with branching and loops |
| App | An internal UI built on top of scripts and flows |
| Workspace | An isolated tenant holding its own scripts, flows, secrets, and permissions |
| Trigger | What starts a run — HTTP, schedule (cron), webhook, or a Discord command |
| Worker | A container that pulls jobs from the Postgres-backed queue and executes them |
A script is on-demand: it runs when triggered and then exits. Attach a cron schedule or a long-running flow only when you actually need something to keep ticking — otherwise an idle script costs nothing.
Docker deployment
Self-Host
The simplest deployment is Docker Compose — a server, a Postgres database, and one or more workers:
services: windmill_server: image: ghcr.io/windmill-labs/windmill:latest ports: - "8000:8000" environment: - DATABASE_URL=postgres://windmill:changeme@db/windmill - MODE=server depends_on: - db restart: unless-stopped
windmill_worker: image: ghcr.io/windmill-labs/windmill:latest environment: - DATABASE_URL=postgres://windmill:changeme@db/windmill - MODE=worker - WORKER_GROUP=default depends_on: - db restart: unless-stopped
db: image: postgres:16 environment: - POSTGRES_USER=windmill - POSTGRES_PASSWORD=changeme - POSTGRES_DB=windmill volumes: - ./windmill-db:/var/lib/postgresql/data restart: unless-stoppedFunction to API
First Script
-
Write a function Every script exports a
mainfunction. Windmill reads its parameters and generates a typed input form automatically.export async function main(name: string, count: number = 1) {return Array.from({ length: count }, () => `hello ${name}`);} -
Run it Fill the generated form and hit run, or call the script’s auto-generated HTTP endpoint with a JSON body — same code, two entry points.
-
Schedule or webhook Attach a cron schedule to run it periodically, or a webhook to trigger it from an external service. Leave both off and it stays a pure on-demand job.
-
Compose into a flow When one script isn’t enough, drop it into a flow to chain it with others, add branching, retries, and error handlers.
Our workflow automation
KBVE Workflows
KBVE self-hosts Windmill on the cluster and drives it two ways: from Discord and from the monorepo.
/wm — Discord-triggered scripts. Our Discord bot exposes a /wm slash command that runs a Windmill script and renders the result as a rich embed. Because each script is a one-off — trigger, run, exit — there is no idle CPU cost to keeping dozens of these commands available. You only pay for the few hundred milliseconds each invocation actually runs.
| Command | What it does |
|---|---|
/wm | With no script name, lists every available command — see the index below |
/wm poem | Fetches a random poem from PoetryDB — /wm poem Emily Dickinson filters by author |
/wm npm | Lists the maintained @kbve npm packages live from the registry — /wm npm laser filters to one |
/wm ud | Defines a term via Urban Dictionary — /wm ud yeet; /wm urbandictionary is a long-form alias |
Scripts live in the monorepo. Every /wm target is a plain TypeScript file checked into apps/windmill/ and written for the fast Bun runtime with zero dependencies. Each script returns a generic embed contract — a { embed: { title, description, fields, ... } } object — and a single renderer in the bot turns that into a Discord message. Adding a new command is just adding a new script.
A self-listing menu. Invoking /wm with no script name runs f/discordsh/help, which queries Windmill’s own scripts API (scripts/list, scoped to the f/discordsh/ folder) and renders each surface script’s summary as an embed field. The menu is self-updating — the moment a new command syncs into the workspace it appears in /wm, with no bot rebuild. The bot simply substitutes the help path when the slash-command argument is blank; the listing itself is a script like any other, so it uses the job-scoped WM_TOKEN and reaches the API over the cluster-internal address.
One core, many surfaces. Scripts are organised by the surface that invokes them — f/discordsh/* for the Discord bot, f/web/* for the website dashboard — with reusable logic factored into a shared parent under f/shared/*. A surface script stays a thin wrapper: f/discordsh/poem and f/web/poem both import the same fetchPoem() from f/shared/poem and only differ in how they shape the result (a Discord embed versus a plain dashboard object). The logic lives once; each surface keeps its own tightly-scoped entry point.
Merge, and it syncs. When a change to apps/windmill/ lands on main, a CI job pushes the versioned scripts into the running Windmill workspace automatically — the repo is the source of truth, and going live is a merge, not a manual deploy.
Monorepo to cluster
Deployment
Shipping a new /wm command is a merge, not a manual deploy — but a few pieces have to line up. Here is the exact path /wm npm took from a file to a live Discord command.
-
Author the script Add
npm.tsplus anpm.script.yamlmanifest underapps/windmill/f/discordsh/, written for the Bun runtime with zero dependencies. Every folder in the tree also needs afolder.meta.yaml— a missing one fails the whole sync. -
Scope the sync
apps/windmill/wmill.yamlsetsincludes: f/**, so a push only manages thef/tree and never clobbers user-space scripts. The push also runs with--skip-variablesso it can’t touch stored secrets — it stays prune-safe. -
CI on merge to main
.github/workflows/windmill-sync.ymltriggers on a push tomainthat touchesapps/windmill/**. It runswindmill-clipinned to the deployed server version (the CLI and server protocol are version-matched — a mismatch breaks the push). -
Run inside the cluster The Windmill API is cluster-internal, so the sync job runs on a self-hosted in-cluster runner — an external runner cannot reach it. That runner’s egress must explicitly allow it to reach the Windmill service, and the CI token’s workspace role must permit writing scripts (an operator-only token gets a
401on create). -
Go live Merge lands → sync pushes
f/discordsh/npminto the workspace → the bot’s/wmallowlist already coversf/discordsh/*→/wm npmrenders. No bot rebuild was needed because the path was already inside the allowed namespace and Windmill runs it as a script.
Where runs live
State & Runs
Windmill is stateless in the container and stateful in Postgres — there is no separate queue engine (no Redis, no Valkey). Every job, its arguments, its result, and its logs are rows in the database. For KBVE that database is our shared Supabase Postgres, and Windmill keeps all of its tables in a dedicated windmill schema so it never collides with app data.
A single execution is identified by one job id (a UUID) and moves through three tables:
| Table | Role in a run’s lifecycle |
|---|---|
v2_job | The canonical record — created the moment a job is triggered. Holds the id, kind (script / flow), the runnable path, the caller, and the input args. |
v2_job_queue | The live queue — a row exists here only while the job is pending or running. Workers poll this table; when the job finishes the row is deleted. |
v2_job_completed | The terminal record — written when the job exits, carrying the status (success / failure), duration, completion time, and the result payload. |
Log output streams to companion tables (job_logs, job_result_stream_v2), and per-run stats land in job_stats — all keyed off that same job id.
The lifecycle of one /wm run:
-
Trigger — the Discord command calls the Windmill API. A row appears in
v2_job(the args, the script path, who asked) and one inv2_job_queue. -
Execute — a worker claims the queued row, runs the script, and streams logs. For a lightweight
/wmscript this is a few hundred milliseconds. -
Complete — the worker deletes the
v2_job_queuerow and writesv2_job_completedwithstatus = successand the duration. The result is returned to the bot, which renders the embed.
Questions
Frequently asked
What is Windmill?
Windmill is an open-source developer platform that turns scripts written in TypeScript, Python, Go, Bash, and SQL into shareable APIs, scheduled cron jobs, multi-step flows, and internal UIs. You write plain code; Windmill auto-generates an input form from the function signature and handles execution, secrets, permissions, and logging.
Is Windmill open source?
Windmill's core is open source under the AGPLv3 license and can be self-hosted for free. There is also a paid enterprise edition and a managed cloud offering with additional features like distributed workers, audit logs, and SSO.
How is Windmill different from n8n?
n8n is node-based — you wire visual nodes on a canvas. Windmill is code-first — you write scripts in TypeScript, Python, Go, or Bash and Windmill wraps each one as an API, cron, or UI. Windmill also supports visual flows that chain scripts together, so you get both code-native ergonomics and a low-code composition layer.
How do I self-host Windmill?
The simplest route is Docker Compose. Run the ghcr.io/windmill-labs/windmill image backed by a Postgres database, add one or more worker containers, and expose the server behind a reverse proxy with TLS. Workers pull jobs from a Postgres-backed queue, so you scale throughput by adding worker replicas.
Do Windmill scripts keep running and use resources when idle?
No. A Windmill script is on-demand — a trigger starts it, it runs, it exits. Unless you attach a cron schedule or a long-polling flow, an idle script consumes no CPU. You only pay for execution time per invocation, which makes one-off command-triggered scripts extremely cheap to keep around.
