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:
- Crea el usuario administrador con email y contraseña
- Windmill crea automáticamente el workspace
admins - 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.


