Windmill: Plataforma de Automatización de Scripts y Flujos

Windmill es una plataforma de automatización de código abierto que combina un editor de scripts, un motor de flujos de trabajo y un generador de interfaces internas, siendo una alternativa directa a Retool y Zapier que puedes alojar en tu propio servidor. Permite que equipos técnicos y no técnicos creen scripts en Python, TypeScript o Go, los encadenen en flujos complejos con aprobaciones humanas y los expongan como webhooks o tareas programadas. Esta guía cubre la instalación en Linux con Docker, la creación de scripts y flujos, la programación de tareas y la gestión de equipos.

Requisitos Previos

  • Linux (Ubuntu 20.04/22.04 o CentOS/Rocky 8/9)
  • Docker 24.x y Docker Compose v2 instalados
  • Mínimo 2 GB RAM y 2 vCPU (4 GB recomendado para uso en equipo)
  • 20 GB de espacio en disco
  • Puerto 8000 disponible (o el que configures)
# Verificar las versiones de Docker y Compose
docker version
docker compose version

# Verificar el espacio disponible
df -h /var/lib/docker

Instalación con Docker Compose

# Clonar el repositorio oficial de Windmill
git clone https://github.com/windmill-labs/windmill.git /opt/windmill
cd /opt/windmill

# Copiar el archivo de configuración de ejemplo
cp docker-compose.yml docker-compose.custom.yml

Edita el archivo docker-compose.custom.yml para personalizar la instalación:

# /opt/windmill/docker-compose.custom.yml
version: "3.7"

services:
  db:
    image: postgres:16
    restart: unless-stopped
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: TuPasswordSeguroParaPostgres
      POSTGRES_DB: windmill
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  windmill_server:
    image: ghcr.io/windmill-labs/windmill:main
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      # URL de conexión a PostgreSQL
      DATABASE_URL: postgres://postgres:TuPasswordSeguroParaPostgres@db/windmill?sslmode=disable
      # Modo de operación: servidor + worker en un solo contenedor (para instalaciones pequeñas)
      MODE: standalone
      # Dominio base para URLs de webhooks y compartición
      BASE_URL: "https://windmill.tudominio.com"
      # Clave secreta para firmar tokens JWT
      JWT_SECRET: "clave-jwt-super-secreta-aleatoria-minimo-32-caracteres"
      # Número de workers (aumentar en producción)
      NUM_WORKERS: 3
    depends_on:
      db:
        condition: service_healthy
    volumes:
      # Caché de dependencias de Python y Node.js
      - windmill_cache:/tmp/windmill

  # Worker separado para alta disponibilidad (opcional)
  windmill_worker:
    image: ghcr.io/windmill-labs/windmill:main
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://postgres:TuPasswordSeguroParaPostgres@db/windmill?sslmode=disable
      MODE: worker
      WORKER_GROUP: default
      JWT_SECRET: "clave-jwt-super-secreta-aleatoria-minimo-32-caracteres"
    depends_on:
      - windmill_server
    volumes:
      - windmill_cache:/tmp/windmill

volumes:
  db_data:
  windmill_cache:
# Iniciar todos los servicios
docker compose -f /opt/windmill/docker-compose.custom.yml up -d

# Verificar que los contenedores están corriendo
docker compose -f /opt/windmill/docker-compose.custom.yml ps

# Ver los logs de arranque
docker compose -f /opt/windmill/docker-compose.custom.yml logs windmill_server --tail=30

Configurar Nginx como proxy inverso

# Instalar Nginx
sudo apt-get install -y nginx

# Crear la configuración del virtual host
sudo tee /etc/nginx/sites-available/windmill << 'EOF'
server {
    listen 80;
    server_name windmill.tudominio.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name windmill.tudominio.com;

    ssl_certificate /etc/letsencrypt/live/windmill.tudominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/windmill.tudominio.com/privkey.pem;

    # Windmill necesita WebSocket para el editor en tiempo real
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        # Aumentar el timeout para scripts de larga ejecución
        proxy_read_timeout 3600s;
        client_max_body_size 100m;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/windmill /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Configuración Inicial

Accede a http://tu-servidor:8000 (o al dominio configurado). En el primer acceso:

  1. Crea el usuario administrador con email y contraseña
  2. Windmill crea automáticamente el workspace admins
  3. Crea un workspace adicional para tu equipo
# Verificar la versión de Windmill instalada
curl -s http://localhost:8000/api/version

# Crear un workspace adicional via API (tras autenticarte)
curl -X POST http://localhost:8000/api/workspaces \
  -H "Authorization: Bearer TU_TOKEN_API" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "mi-equipo",
    "name": "Mi Equipo de DevOps",
    "username": "admin"
  }'

Creación de Scripts

Windmill soporta Python, TypeScript, Go, Bash y SQL. Los scripts se crean desde la UI en Scripts → New script.

Script Python con recursos externos

# Script Python: Consultar base de datos y enviar resumen por email
# En Windmill, las dependencias se declaran al principio del archivo

import wmill  # Cliente oficial de Windmill (disponible en el entorno)
import psycopg2
import smtplib
from email.mime.text import MIMEText
from datetime import datetime, timedelta

def main(
    # Los parámetros se convierten automáticamente en un formulario en la UI
    dias: int = 7,
    email_destino: str = "[email protected]",
    # Los recursos de Windmill se referencian con su path
    db_resource: dict = wmill.get_resource("u/admin/base_datos_produccion")
):
    """
    Genera un resumen semanal de métricas y lo envía por email.
    Parámetros:
    - dias: número de días a analizar (por defecto: 7)
    - email_destino: dirección de correo para el resumen
    """
    # Conectar a la base de datos usando el recurso de Windmill
    conn = psycopg2.connect(
        host=db_resource["host"],
        database=db_resource["database"],
        user=db_resource["user"],
        password=db_resource["password"]
    )
    
    fecha_inicio = datetime.now() - timedelta(days=dias)
    
    with conn.cursor() as cur:
        # Consultar las métricas del período
        cur.execute("""
            SELECT 
                COUNT(*) as total_pedidos,
                SUM(importe) as ingresos_totales,
                AVG(importe) as ticket_medio
            FROM pedidos
            WHERE fecha_creacion >= %s
        """, (fecha_inicio,))
        
        resultado = cur.fetchone()
    
    conn.close()
    
    # Formatear el resumen
    resumen = f"""
    Resumen de los últimos {dias} días:
    - Pedidos totales: {resultado[0]}
    - Ingresos: {resultado[1]:.2f}€
    - Ticket medio: {resultado[2]:.2f}€
    """
    
    # Windmill puede devolver datos estructurados que se muestran en la UI
    return {
        "total_pedidos": resultado[0],
        "ingresos": float(resultado[1] or 0),
        "ticket_medio": float(resultado[2] or 0),
        "resumen": resumen
    }

Script TypeScript/Deno

// Script TypeScript: Llamar a una API externa y procesar la respuesta
import * as wmill from "npm:windmill-client@1";

export async function main(
  url: string,
  headers: Record<string, string> = {},
  timeout_segundos: number = 30
): Promise<object> {
  // Obtener token de la API desde los recursos de Windmill
  const apiConfig = await wmill.getResource("u/admin/api-externa");
  
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout_segundos * 1000);
  
  try {
    const respuesta = await fetch(url, {
      headers: {
        "Authorization": `Bearer ${apiConfig.token}`,
        "Content-Type": "application/json",
        ...headers
      },
      signal: controller.signal
    });
    
    if (!respuesta.ok) {
      throw new Error(`Error HTTP ${respuesta.status}: ${await respuesta.text()}`);
    }
    
    const datos = await respuesta.json();
    return { exito: true, datos, codigo: respuesta.status };
    
  } catch (error) {
    return { exito: false, error: error.message };
  } finally {
    clearTimeout(timeoutId);
  }
}

Script Bash para operaciones de sistema

#!/bin/bash
# Script Bash en Windmill: Limpiar logs antiguos en servidores remotos

# Windmill pasa los parámetros como variables de entorno
SERVIDOR="${servidor:-}"
DIAS_RETENCION="${dias_retencion:-30}"
DIRECTORIO_LOGS="${directorio_logs:-/var/log}"

# Validar que el parámetro servidor no está vacío
if [ -z "$SERVIDOR" ]; then
    echo "Error: Se requiere especificar un servidor" >&2
    exit 1
fi

echo "=== Limpiando logs en $SERVIDOR (retención: $DIAS_RETENCION días) ==="

# Encontrar y eliminar logs más antiguos que el período de retención
ssh -o StrictHostKeyChecking=no "deploy@$SERVIDOR" bash << EOF
    # Calcular el espacio antes de la limpieza
    ANTES=\$(du -sh $DIRECTORIO_LOGS 2>/dev/null | cut -f1)
    
    # Eliminar archivos de log más antiguos que el período configurado
    find $DIRECTORIO_LOGS -name "*.log" -mtime +$DIAS_RETENCION -delete
    find $DIRECTORIO_LOGS -name "*.log.gz" -mtime +$DIAS_RETENCION -delete
    
    # Calcular el espacio después de la limpieza
    DESPUES=\$(du -sh $DIRECTORIO_LOGS 2>/dev/null | cut -f1)
    
    echo "Espacio liberado: antes \$ANTES, después \$DESPUES"
EOF

Diseño de Flujos de Trabajo

Los flujos (flows) en Windmill encadenan scripts y añaden lógica de control:

{
  "summary": "Pipeline de procesamiento de datos diario",
  "description": "Extrae datos, los transforma y genera un informe",
  "value": {
    "modules": [
      {
        "id": "paso-1-extraer",
        "summary": "Extraer datos de la API",
        "value": {
          "type": "script",
          "path": "u/admin/extraer-datos-api",
          "input_transforms": {
            "url": {
              "type": "static",
              "value": "https://api.tudominio.com/datos"
            },
            "fecha": {
              "type": "javascript",
              "expr": "new Date().toISOString().split('T')[0]"
            }
          }
        }
      },
      {
        "id": "paso-2-validar",
        "summary": "Validar datos recibidos",
        "value": {
          "type": "script",
          "path": "u/admin/validar-datos",
          "input_transforms": {
            "datos": {
              "type": "javascript",
              "expr": "results['paso-1-extraer'].datos"
            }
          }
        }
      },
      {
        "id": "paso-3-bifurcacion",
        "summary": "Decidir según calidad de datos",
        "value": {
          "type": "branchone",
          "branches": [
            {
              "summary": "Datos válidos: procesar",
              "expr": "results['paso-2-validar'].valido === true",
              "modules": [
                {
                  "id": "transformar",
                  "value": {
                    "type": "script",
                    "path": "u/admin/transformar-datos"
                  }
                }
              ]
            },
            {
              "summary": "Datos inválidos: notificar error",
              "modules": [
                {
                  "id": "notificar-error",
                  "value": {
                    "type": "script",
                    "path": "u/admin/enviar-alerta-email"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
}

Programación de Tareas

Los schedules de Windmill ejecutan scripts y flujos automáticamente:

# Crear un schedule via API de Windmill
curl -X POST "http://localhost:8000/api/w/mi-equipo/schedules" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "u/admin/limpieza-diaria",
    "schedule": "0 2 * * *",
    "timezone": "Europe/Madrid",
    "script_path": "u/admin/limpiar-logs",
    "is_flow": false,
    "args": {
      "dias_retencion": 30,
      "directorio_logs": "/var/log/aplicacion"
    },
    "enabled": true
  }'

# Listar los schedules activos
curl "http://localhost:8000/api/w/mi-equipo/schedules/list" \
  -H "Authorization: Bearer TU_TOKEN"

Ejemplos de expresiones cron para tareas comunes:

# Cada día a las 2:00 AM (zona horaria Europe/Madrid)
"0 2 * * *"

# Cada lunes a las 9:00 AM
"0 9 * * 1"

# Cada hora durante horario laboral (9 AM - 6 PM, lunes a viernes)
"0 9-18 * * 1-5"

# Cada 15 minutos
"*/15 * * * *"

# El primer día de cada mes a medianoche
"0 0 1 * *"

Triggers por Webhook

Windmill genera automáticamente un endpoint webhook para cada script o flujo:

# Obtener la URL del webhook de un script
# Formato: https://windmill.tudominio.com/api/w/{workspace}/jobs/run/p/{path-del-script}

# Ejecutar un script via webhook con curl
curl -X POST "https://windmill.tudominio.com/api/w/mi-equipo/jobs/run/p/u/admin/mi-script" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "parametro1": "valor1",
    "parametro2": 42
  }'

# Obtener el resultado de la ejecución
JOB_ID=$(curl -s -X POST "https://windmill.tudominio.com/api/w/mi-equipo/jobs/run/p/u/admin/mi-script" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"parametro": "valor"}' | jq -r '.id')

# Esperar y obtener el resultado
curl "https://windmill.tudominio.com/api/w/mi-equipo/jobs/completed/get/$JOB_ID" \
  -H "Authorization: Bearer TU_TOKEN"

Integración con GitHub Actions mediante webhook:

# .github/workflows/deploy.yml
- name: Notificar despliegue a Windmill
  run: |
    curl -X POST "${{ secrets.WINDMILL_URL }}/api/w/mi-equipo/jobs/run/p/u/admin/post-deploy" \
      -H "Authorization: Bearer ${{ secrets.WINDMILL_TOKEN }}" \
      -H "Content-Type: application/json" \
      -d '{
        "version": "${{ github.sha }}",
        "entorno": "produccion",
        "autor": "${{ github.actor }}"
      }'

Flujos de Aprobación

Windmill soporta pasos de aprobación humana dentro de los flujos, útiles para despliegues o cambios críticos:

{
  "modules": [
    {
      "id": "preparar-despliegue",
      "value": {
        "type": "script",
        "path": "u/admin/preparar-release"
      }
    },
    {
      "id": "solicitar-aprobacion",
      "summary": "Aprobación requerida antes del despliegue",
      "value": {
        "type": "approval",
        "approvers": [
          {"type": "group", "value": "tech-leads"},
          {"type": "email", "value": "[email protected]"}
        ],
        "timeout": 3600,
        "description": "¿Aprobar el despliegue de la versión {{ results['preparar-despliegue'].version }} a producción?"
      }
    },
    {
      "id": "ejecutar-despliegue",
      "summary": "Desplegar en producción",
      "value": {
        "type": "script",
        "path": "u/admin/desplegar-produccion",
        "input_transforms": {
          "version": {
            "type": "javascript",
            "expr": "results['preparar-despliegue'].version"
          }
        }
      }
    }
  ]
}

Espacios de Trabajo en Equipo

# Crear un usuario en el workspace via API
curl -X POST "http://localhost:8000/api/w/mi-equipo/workspaces/add_user" \
  -H "Authorization: Bearer TU_TOKEN_ADMIN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "username": "nuevousuario",
    "is_admin": false,
    "operator": false
  }'

# Crear un grupo para organizar permisos
curl -X POST "http://localhost:8000/api/w/mi-equipo/groups/create" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "desarrolladores",
    "summary": "Equipo de desarrollo"
  }'

# Añadir usuario al grupo
curl -X POST "http://localhost:8000/api/w/mi-equipo/groups/add_user/desarrolladores" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username": "nuevousuario"}'

Solución de Problemas

Windmill no arranca (error de conexión a PostgreSQL):

# Verificar que la base de datos está accesible
docker compose -f /opt/windmill/docker-compose.custom.yml logs db --tail=20

# Comprobar la cadena de conexión
docker compose -f /opt/windmill/docker-compose.custom.yml exec windmill_server \
  env | grep DATABASE_URL

# Probar la conexión directamente
docker compose -f /opt/windmill/docker-compose.custom.yml exec db \
  psql -U postgres -c "\l"

Los scripts tardan mucho en ejecutarse (cold start):

# El primer script Python/TypeScript tarda mientras se instalan dependencias
# Para reducir el cold start, pre-cachear dependencias comunes

# Ver el uso de caché
docker compose -f /opt/windmill/docker-compose.custom.yml exec windmill_server \
  du -sh /tmp/windmill

# Aumentar el número de workers en docker-compose.custom.yml
# NUM_WORKERS=5 (para más ejecuciones paralelas)

Los webhooks devuelven 401 Unauthorized:

# Generar un nuevo token de API desde la UI: Settings → Tokens → New Token
# O via API:
curl -X POST "http://localhost:8000/api/auth/tokens/create" \
  -H "Authorization: Bearer TOKEN_ADMIN" \
  -H "Content-Type: application/json" \
  -d '{"label": "Webhook CI/CD", "expiration": null}'

El flujo se queda bloqueado en un paso de aprobación:

# Ver el estado del job
curl "http://localhost:8000/api/w/mi-equipo/jobs/get/JOB_ID" \
  -H "Authorization: Bearer TU_TOKEN"

# Cancelar un job bloqueado
curl -X POST "http://localhost:8000/api/w/mi-equipo/jobs/cancel/JOB_ID" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Timeout de aprobación superado"}'

Conclusión

Windmill elimina la brecha entre los scripts que escribe el equipo técnico y los procesos que necesita automatizar el equipo de negocio, ofreciendo una plataforma unificada con editor de código, flujos visuales, programación y webhooks en una sola herramienta autohospedada. Su soporte para Python, TypeScript y Bash, combinado con los flujos de aprobación y los espacios de trabajo por equipo, lo convierte en una alternativa sólida y soberana a plataformas SaaS como Zapier o Retool. Al ejecutarse completamente en tu infraestructura, mantienes el control total sobre el código, los secretos y los datos procesados.