← All articles
white and black windmill under white clouds and blue sky during daytime

Windmill: Self-Hosted Scripts, Workflows, and Internal Tools in One Platform

Automation 2026-02-14 · 7 min read windmill automation workflows internal-tools scripts
By Selfhosted Guides Editorial TeamSelf-hosting practitioners covering open source software, home lab infrastructure, and data sovereignty.

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.

Windmill workflow automation platform logo

What Windmill Does

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:

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:

  1. Parses the function signature -- Each parameter becomes a form field in the UI
  2. Auto-generates an input form -- Users can run the script by filling in the form, no CLI needed
  3. Creates a webhook endpoint -- Every script is callable via HTTP
  4. 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

  1. Create a new flow
  2. 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}
  1. Connect the steps (output of step 1 feeds into step 2, etc.)
  2. Add a schedule: 0 3 * * * (daily at 3 AM)

Flow Features

Building Internal Apps

Windmill's app builder creates internal tools -- dashboards, admin panels, data viewers -- without a separate frontend framework.

Example: Server Health Dashboard

  1. Create a new app
  2. 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
  3. Connect components to scripts (the table's data source is a script, the button triggers a script)
  4. Deploy the app -- it's immediately accessible at a URL

The app builder supports:

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

Choose n8n When

Choose Retool When

Production Tips

Variables and Secrets

Store credentials centrally:

  1. Go to Resources in the Windmill UI
  2. Add a resource type (PostgreSQL, S3, Slack, HTTP, custom)
  3. Enter the credentials
  4. 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.

Get free weekly tips in your inbox. Subscribe to Self-Hosted Weekly