Skip to content
application · automation · workflow

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.
AGPLv3License
8000Default port
TS · Py · Go · BashLanguages
ZeroIdle cost

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

TermMeaning
ScriptA single function in TS/Python/Go/Bash, wrapped as a runnable API
FlowA multi-step DAG that chains scripts together with branching and loops
AppAn internal UI built on top of scripts and flows
WorkspaceAn isolated tenant holding its own scripts, flows, secrets, and permissions
TriggerWhat starts a run — HTTP, schedule (cron), webhook, or a Discord command
WorkerA 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-stopped

Function to API

First Script

  1. Write a function Every script exports a main function. 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}`);
    }
  2. 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.

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

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

CommandWhat it does
/wmWith no script name, lists every available command — see the index below
/wm poemFetches a random poem from PoetryDB — /wm poem Emily Dickinson filters by author
/wm npmLists the maintained @kbve npm packages live from the registry — /wm npm laser filters to one
/wm udDefines 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.

  1. Author the script Add npm.ts plus a npm.script.yaml manifest under apps/windmill/f/discordsh/, written for the Bun runtime with zero dependencies. Every folder in the tree also needs a folder.meta.yaml — a missing one fails the whole sync.

  2. Scope the sync apps/windmill/wmill.yaml sets includes: f/**, so a push only manages the f/ tree and never clobbers user-space scripts. The push also runs with --skip-variables so it can’t touch stored secrets — it stays prune-safe.

  3. CI on merge to main .github/workflows/windmill-sync.yml triggers on a push to main that touches apps/windmill/**. It runs windmill-cli pinned to the deployed server version (the CLI and server protocol are version-matched — a mismatch breaks the push).

  4. 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 401 on create).

  5. Go live Merge lands → sync pushes f/discordsh/npm into the workspace → the bot’s /wm allowlist already covers f/discordsh/*/wm npm renders. 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:

TableRole in a run’s lifecycle
v2_jobThe 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_queueThe 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_completedThe 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:

  1. Trigger — the Discord command calls the Windmill API. A row appears in v2_job (the args, the script path, who asked) and one in v2_job_queue.

  2. Execute — a worker claims the queued row, runs the script, and streams logs. For a lightweight /wm script this is a few hundred milliseconds.

  3. Complete — the worker deletes the v2_job_queue row and writes v2_job_completed with status = success and 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.