Windmill: Self-Hosted Scripts, Workflows, and Internal Tools in One Platform
Most automation tools fall into one of two camps. Visual workflow builders like n8n and Zapier are great for connecting APIs, but they're frustrating when you need real logic -- loops, conditionals, data transformations. Code-first tools like Airflow give you full control, but require so much boilerplate that a simple "fetch data, transform it, send an alert" takes hours.
Photo by CLARA METIVIER BEUKES on Unsplash
Windmill is an open-source platform that combines both approaches. Write scripts in TypeScript, Python, Go, Bash, or SQL. Chain them into workflows with a visual DAG editor. Build internal UIs that run those scripts with a drag-and-drop app builder. Schedule everything with cron, trigger it via webhooks, or run it on demand.
Think of Windmill as Retool + Airflow + a script runner, self-hosted in a single Docker stack.
What Windmill Does
- Scripts -- Write and run scripts in TypeScript, Python, Go, Bash, SQL, or PowerShell with automatic input forms
- Flows -- Chain scripts into DAG workflows with branching, loops, error handling, and retries
- Apps -- Build internal tools and dashboards with a drag-and-drop UI builder
- Scheduling -- Cron-based scheduling for any script or flow
- Webhooks -- Trigger scripts and flows via HTTP
- Variables and secrets -- Centralized, encrypted credential storage
- Permissions -- Granular access control per script, flow, and resource
- Versioning -- Full Git-like version history for every script
Docker Deployment
# docker-compose.yml
services:
windmill:
image: ghcr.io/windmill-labs/windmill:main
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://windmill:windmillpass@db:5432/windmill
- BASE_URL=https://windmill.yourdomain.com
depends_on:
- db
restart: unless-stopped
windmill-worker:
image: ghcr.io/windmill-labs/windmill:main
command: windmill worker
environment:
- DATABASE_URL=postgresql://windmill:windmillpass@db:5432/windmill
- BASE_URL=https://windmill.yourdomain.com
- WORKER_GROUP=default
depends_on:
- db
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
db:
image: postgres:16
volumes:
- windmill_db:/var/lib/postgresql/data
environment:
- POSTGRES_DB=windmill
- POSTGRES_USER=windmill
- POSTGRES_PASSWORD=windmillpass
restart: unless-stopped
volumes:
windmill_db:
docker compose up -d
Access Windmill at http://your-server:8000. The default admin credentials are [email protected] / changeme -- change these immediately.
Architecture notes:
- The
windmillservice runs the API server and web UI - The
windmill-workerservice executes scripts and flows. Add more workers for parallel execution - PostgreSQL stores all state: scripts, flows, schedules, run history, and the job queue
- Mounting the Docker socket on the worker allows scripts to spawn Docker containers (useful for isolated execution)
BASE_URLmust match your public URL for webhooks and sharing links
Scaling Workers
For heavier workloads, run multiple workers:
windmill-worker-1:
image: ghcr.io/windmill-labs/windmill:main
command: windmill worker
environment:
- DATABASE_URL=postgresql://windmill:windmillpass@db:5432/windmill
- WORKER_GROUP=default
- WORKER_TAGS=python,typescript
restart: unless-stopped
windmill-worker-2:
image: ghcr.io/windmill-labs/windmill:main
command: windmill worker
environment:
- DATABASE_URL=postgresql://windmill:windmillpass@db:5432/windmill
- WORKER_GROUP=default
- WORKER_TAGS=bash,go
restart: unless-stopped
Worker tags let you route specific script languages to specific workers.
Writing Scripts
Windmill's core building block is the script. Write it in the web editor or sync from Git.
TypeScript Example
// Fetch data from an API and return a summary
export async function main(
api_url: string,
api_key: string,
) {
const response = await fetch(api_url, {
headers: { Authorization: `Bearer ${api_key}` },
});
const data = await response.json();
return {
total_items: data.length,
latest: data[0],
fetched_at: new Date().toISOString(),
};
}
Python Example
import requests
def main(webhook_url: str, message: str, severity: str = "info"):
"""Send a notification to a webhook endpoint."""
payload = {
"text": message,
"severity": severity,
"source": "windmill",
}
response = requests.post(webhook_url, json=payload)
response.raise_for_status()
return {"status": "sent", "status_code": response.status_code}
How Scripts Work
When you save a script, Windmill:
- Parses the function signature -- Each parameter becomes a form field in the UI
- Auto-generates an input form -- Users can run the script by filling in the form, no CLI needed
- Creates a webhook endpoint -- Every script is callable via HTTP
- Versions the code -- Full history with diff view
Type hints matter. Windmill uses them to generate appropriate input widgets (text fields, dropdowns, file uploads, etc.).
Like what you're reading? Subscribe to Self-Hosted Weekly — free weekly guides in your inbox.
Building Flows
Flows chain scripts into multi-step workflows with a visual DAG editor.
Example: Daily Database Backup and Notification
- Create a new flow
- Add steps:
Step 1: Run backup (Bash)
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME | gzip > /tmp/backup-$(date +%Y%m%d).sql.gz
echo "/tmp/backup-$(date +%Y%m%d).sql.gz"
Step 2: Upload to S3 (TypeScript)
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFile } from "fs/promises";
export async function main(backup_path: string, bucket: string) {
const client = new S3Client({ region: "us-east-1" });
const file = await readFile(backup_path);
await client.send(new PutObjectCommand({
Bucket: bucket,
Key: `backups/${new Date().toISOString().split("T")[0]}.sql.gz`,
Body: file,
}));
return { uploaded: true, path: backup_path };
}
Step 3: Send notification (Python)
import requests
def main(upload_result: dict, discord_webhook: str):
requests.post(discord_webhook, json={
"content": f"Backup completed: {upload_result['path']}"
})
return {"notified": True}
- Connect the steps (output of step 1 feeds into step 2, etc.)
- Add a schedule:
0 3 * * *(daily at 3 AM)
Flow Features
- Branching -- Route to different steps based on conditions
- For loops -- Iterate over arrays, running a step for each item
- Error handling -- Catch errors and route to recovery steps
- Retries -- Automatic retry with configurable backoff
- Approval steps -- Pause the flow and wait for human approval
- Parallel execution -- Run independent steps concurrently
Building Internal Apps
Windmill's app builder creates internal tools -- dashboards, admin panels, data viewers -- without a separate frontend framework.
Example: Server Health Dashboard
- Create a new app
- Add components:
- Table -- Displays server data from a script that queries your monitoring API
- Button -- "Restart Service" triggers a script that runs
docker restart <container> - Chart -- Shows CPU usage over time from a data-fetching script
- Text input -- Filter servers by name
- Connect components to scripts (the table's data source is a script, the button triggers a script)
- Deploy the app -- it's immediately accessible at a URL
The app builder supports:
- Tables, charts, forms, buttons, text, images, tabs, modals
- Script-backed data sources (any Windmill script)
- Component interactions (button click triggers script, table row selection filters another table)
- Custom CSS for styling
This is where Windmill competes with Retool. Instead of building a React app for an internal tool, drag components onto a canvas and wire them to your scripts.
Scheduling
Any script or flow can be scheduled with cron syntax:
# Every 5 minutes
*/5 * * * *
# Daily at midnight
0 0 * * *
# Every Monday at 9 AM
0 9 * * 1
# First day of each month
0 0 1 * *
Schedules are managed in the UI with a visual cron builder, or via the API/CLI.
Windmill vs n8n vs Retool
| Feature | Windmill | n8n | Retool |
|---|---|---|---|
| Pricing | Free (self-hosted) | Free (self-hosted) | $10+/user/month |
| Script languages | TS, Python, Go, Bash, SQL | JS (limited Python) | JS (limited) |
| Visual workflows | Yes (DAG editor) | Yes (node editor) | No |
| App builder | Yes (drag-and-drop) | No | Yes (excellent) |
| Pre-built connectors | Limited | 400+ | 100+ |
| Code-first | Yes | No (visual-first) | Partially |
| Scheduling | Cron | Cron | Cron |
| Webhooks | Yes | Yes | Yes |
| Git sync | Yes | Limited | Yes |
| Worker scaling | Horizontal | Single process | Cloud-managed |
| Learning curve | Medium | Low | Low-Medium |
| Best for | Developer teams | Non-technical users | Internal tools |
Choose Windmill When
- Your team writes code and wants a platform to run, schedule, and chain scripts
- You need both workflow automation AND internal tool building
- You want multi-language support (not just JavaScript)
- You need horizontal scaling with multiple workers
- You want Git-based version control for your automation code
Choose n8n When
- You prefer visual workflow building over writing code
- You need 400+ pre-built connectors to SaaS services
- Most of your automations are "connect API A to API B"
- Your team is less code-oriented
Choose Retool When
- Your primary need is building internal tools and admin panels
- You want the most polished drag-and-drop UI builder
- You don't want to self-host
- Your team needs to build complex data-driven interfaces
Production Tips
Variables and Secrets
Store credentials centrally:
- Go to Resources in the Windmill UI
- Add a resource type (PostgreSQL, S3, Slack, HTTP, custom)
- Enter the credentials
- Reference resources in scripts by name -- they're injected at runtime
Secrets are encrypted at rest in PostgreSQL.
Git Sync
Windmill supports syncing scripts and flows to a Git repository:
# Install the CLI
npm install -g windmill-cli
# Pull scripts from Windmill to local
wmill pull
# Push local changes to Windmill
wmill push
This enables code review workflows: write scripts locally, push to Git, review in a PR, then sync to Windmill.
Backups
Back up the PostgreSQL database -- it contains everything:
docker exec windmill-db-1 pg_dump -U windmill windmill > windmill-backup.sql
Resource Usage
Windmill's server is lightweight (~200 MB RAM). Workers use more, depending on what scripts are executing. Python scripts with heavy dependencies (pandas, numpy) will spike worker memory during execution.
For a homelab with a few dozen scheduled scripts, one worker with 512 MB RAM is sufficient.
The Bottom Line
Windmill occupies a unique space: it's a script runner, workflow engine, and internal tool builder in one platform. If your automation needs go beyond "connect these two APIs" and into "run this Python script, process the output in TypeScript, and display the results in a dashboard," Windmill handles the entire chain. The code-first approach means you're writing real scripts in real languages, with Windmill handling the execution, scheduling, error handling, and UI. Deploy it alongside your other self-hosted services and consolidate your scattered cron jobs, one-off scripts, and admin tools into a single platform.
