Windmill Script and Flow Automation Platform
Windmill is an open-source developer platform for building internal tools, workflows, and automation scripts using Python, TypeScript, Go, Bash, or SQL. Self-hosted on Linux, it provides an IDE-like editor, flow builder, scheduled jobs, webhook triggers, and team workspaces — making it a powerful alternative to Retool, Zapier, and custom automation scripts.
Prerequisites
- Docker and Docker Compose installed
- At least 2 GB RAM (4 GB recommended for multiple workers)
- A domain name with SSL (for webhooks and team access)
- PostgreSQL (included in Docker Compose)
Docker Installation
# Create project directory and download official compose file
mkdir -p /opt/windmill && cd /opt/windmill
curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/.env -o .env
Edit .env with your configuration:
# .env
WM_BASE_URL=https://windmill.yourdomain.com
DATABASE_URL=postgresql://windmill:windmill@db/windmill
SECRET=your-random-32-char-secret
# SMTP for email notifications
EMAIL_HOST=smtp.sendgrid.net
EMAIL_PORT=587
EMAIL_HOST_USER=apikey
EMAIL_HOST_PASSWORD=your-sendgrid-key
EMAIL_USE_TLS=true
The included docker-compose.yml:
version: '3'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: windmill
POSTGRES_USER: windmill
POSTGRES_PASSWORD: windmill
volumes:
- db_data:/var/lib/postgresql/data
windmill_server:
image: ghcr.io/windmill-labs/windmill:main
depends_on:
- db
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://windmill:windmill@db/windmill
BASE_URL: https://windmill.yourdomain.com
MODE: server
volumes:
- worker_logs:/tmp/windmill/logs
windmill_worker:
image: ghcr.io/windmill-labs/windmill:main
depends_on:
- windmill_server
environment:
DATABASE_URL: postgresql://windmill:windmill@db/windmill
BASE_URL: https://windmill.yourdomain.com
MODE: worker
WORKER_GROUP: default
NUM_WORKERS: 3
volumes:
- worker_logs:/tmp/windmill/logs
- /var/run/docker.sock:/var/run/docker.sock # For Docker jobs
volumes:
db_data:
worker_logs:
# Start Windmill
docker compose up -d
# View logs
docker compose logs windmill_server --tail 50 -f
# Access at http://your-server:8000
# Create your admin account on first visit
Script Creation
Windmill scripts are versioned, runnable functions with typed inputs:
Python Script
# In Windmill web editor: Scripts → New Script → Python3
import requests
import wmill # Windmill Python client
# Input types are defined as function parameters
# They appear as a form in the Windmill UI
def main(
target_url: str,
expected_status: int = 200,
timeout_seconds: int = 10,
notify_on_failure: bool = True,
) -> dict:
"""Check if a URL returns the expected HTTP status code."""
try:
response = requests.get(target_url, timeout=timeout_seconds)
success = response.status_code == expected_status
result = {
"url": target_url,
"status_code": response.status_code,
"success": success,
"response_time_ms": int(response.elapsed.total_seconds() * 1000),
}
if not success and notify_on_failure:
# Access Windmill resources (credentials stored in platform)
slack_token = wmill.get_variable("f/monitoring/slack_token")
requests.post(
"https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {slack_token}"},
json={
"channel": "#alerts",
"text": f"Health check failed: {target_url} returned {response.status_code}"
}
)
return result
except Exception as e:
return {"url": target_url, "success": False, "error": str(e)}
TypeScript Script
// TypeScript scripts in Windmill (Deno runtime)
import * as wmill from "npm:windmill-client@1"
// Define resource types for typed connections
type PgDatabase = {
host: string
port: number
database: string
user: string
password: string
}
export async function main(
db: PgDatabase,
table: string,
limit: number = 100,
): Promise<object[]> {
// Use Windmill's resource system for database connections
const client = await wmill.getResource<PgDatabase>(`postgresql/${table}`)
// ... or use the passed parameter directly
return [{ table, limit, connected: true }]
}
Bash Script
#!/bin/bash
# Windmill Bash script - parameters come from environment variables
# Variables are injected from the form input
set -euo pipefail
# Windmill injects parameters as env vars
HOSTNAME="${HOSTNAME:-localhost}"
PORT="${PORT:-80}"
EXPECTED_STATUS="${EXPECTED_STATUS:-200}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOSTNAME:$PORT" || echo "000")
if [ "$STATUS" = "$EXPECTED_STATUS" ]; then
echo "✓ $HOSTNAME:$PORT returned $STATUS"
exit 0
else
echo "✗ $HOSTNAME:$PORT returned $STATUS (expected $EXPECTED_STATUS)"
exit 1
fi
Flow Design
Flows connect scripts and modules into multi-step workflows:
Creating a Flow
- Go to Flows → New Flow
- Add steps by clicking +
- Each step can be a script, inline code, loop, or branch
Flow step types:
- Script: Run a saved Windmill script
- Inline Script: Write code directly in the flow
- For Loop: Iterate over an array
- Branch: If/else conditional routing
- Input Transform: Transform data between steps
Flow with Multiple Steps
// Step 1: Fetch data (TypeScript inline)
// Input: { "limit": 50 }
export async function main(limit: number) {
const response = await fetch(`https://api.example.com/orders?limit=${limit}`)
return await response.json()
}
// Step 2: Filter orders (Python inline)
// Input: { "orders": "${previous_step.result}" }
def main(orders: list) -> list:
return [o for o in orders if o["status"] == "pending"]
// Step 3: For Each - process each pending order
// Loops over the result of Step 2
// Step 3a: Process individual order (calls existing script)
// Script: scripts/process-order
// Input: { "order_id": "${for_loop_value.id}" }
Scheduling and Webhook Triggers
Scheduled Jobs
# In Windmill UI: Scripts/Flows → Schedule tab
# Or via API:
curl -X POST "https://windmill.yourdomain.com/api/w/myworkspace/schedules" \
-H "Authorization: Bearer $WINDMILL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"path": "f/monitoring/check-all-sites",
"schedule": "*/5 * * * *",
"timezone": "UTC",
"is_flow": false,
"args": {
"notify": true
},
"enabled": true
}'
# List schedules
curl "https://windmill.yourdomain.com/api/w/myworkspace/schedules/list" \
-H "Authorization: Bearer $WINDMILL_TOKEN"
Webhook Triggers
Every script and flow gets a webhook URL automatically:
# Get webhook URL for a script
curl "https://windmill.yourdomain.com/api/w/myworkspace/scripts/by_path/f/myapp/process-webhook" \
-H "Authorization: Bearer $WINDMILL_TOKEN"
# Trigger script via webhook (async - returns job ID)
curl -X POST \
"https://windmill.yourdomain.com/api/w/myworkspace/jobs/run/p/f/myapp/process-webhook" \
-H "Authorization: Bearer $WINDMILL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event_type": "user.signup", "user_id": 42}'
# Trigger and wait for result (sync)
curl -X POST \
"https://windmill.yourdomain.com/api/w/myworkspace/jobs/run_wait_result/p/f/myapp/process-data" \
-H "Authorization: Bearer $WINDMILL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"input_data": [1, 2, 3]}'
Approval Flows
Approval steps pause a flow until a human approves or rejects:
# In a flow step, add an Approval type step
# Or in a Python script, use the wmill client:
import wmill
def main(order_id: str, amount: float):
if amount > 1000:
# Suspend and wait for approval
# Sends an email with approve/deny links
approvers = ["[email protected]"]
approval_id = wmill.get_resume_urls(approvers=approvers)
# This suspends execution - next steps run only after approval
return {
"status": "pending_approval",
"order_id": order_id,
"approval_id": approval_id,
}
# Small orders proceed automatically
return {"status": "auto_approved", "order_id": order_id}
Configure approval notifications in the Windmill UI:
- Flow → Approval Step → set approvers (email or Windmill usernames)
- Set expiration time (auto-reject if not approved within X hours)
Team Workspaces and Permissions
# Windmill organizes resources in workspaces
# Each workspace has its own scripts, flows, and credentials
# Create a new workspace via API
curl -X POST "https://windmill.yourdomain.com/api/workspaces/create" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "myteam", "name": "My Team", "username": "admin"}'
# Invite a user to a workspace
curl -X POST "https://windmill.yourdomain.com/api/w/myteam/workspaces/add_user" \
-H "Authorization: Bearer $WINDMILL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "is_admin": false, "operator": false}'
# Path-based permissions: f/ = folder prefix
# f/public/ - accessible by all workspace members
# f/private/ - accessible only by owner
# f/teamname/ - accessible by team members (groups)
Storing Credentials (Resources)
# Store a database credential as a Windmill resource
curl -X POST "https://windmill.yourdomain.com/api/w/myworkspace/resources/create" \
-H "Authorization: Bearer $WINDMILL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"path": "f/databases/production_pg",
"resource_type": "postgresql",
"value": {
"host": "db.yourdomain.com",
"port": 5432,
"database": "myapp",
"user": "readonly",
"password": "secret"
}
}'
Worker Configuration and Scaling
# Scale workers for higher throughput
docker compose up -d --scale windmill_worker=5
# Create specialized worker groups for resource-intensive jobs
# docker-compose.yml additions:
#
# windmill_worker_heavy:
# extends: windmill_worker
# environment:
# WORKER_GROUP: heavy
# NUM_WORKERS: 1
# deploy:
# resources:
# limits:
# memory: 4G
# Tag scripts to run on specific worker groups:
# Script settings → Worker Group Tag: "heavy"
Troubleshooting
Script fails with module import error:
# Python packages are installed per-script using requirements block
# Add at top of script:
# import: requests, pandas, boto3
# Or use Windmill's requirements.txt approach:
# Check script settings → Extra Libraries
# View worker logs
docker compose logs windmill_worker --tail 50
Webhook not triggering:
# Verify Windmill is accessible from the internet
curl -I https://windmill.yourdomain.com
# Check the webhook URL format
# https://windmill.yourdomain.com/api/w/{workspace}/jobs/run/p/{script_path}
# Test webhook manually with verbose output
curl -v -X POST "https://windmill.yourdomain.com/api/w/myworkspace/jobs/run/p/f/myapp/script" \
-H "Authorization: Bearer $WINDMILL_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Flow stuck on approval step:
# Check job status
curl "https://windmill.yourdomain.com/api/w/myworkspace/jobs/JOBID" \
-H "Authorization: Bearer $WINDMILL_TOKEN"
# Manually approve/cancel suspended job in UI:
# Runs → select job → Resume or Cancel
Conclusion
Windmill combines the power of a full script execution environment with a visual flow builder and team collaboration features, making it a versatile platform for internal tooling and workflow automation. Its support for multiple languages, type-safe inputs that auto-generate UI forms, and built-in approval flows accelerate building production-grade automation without maintaining custom infrastructure for each use case.


