Era in8nevitable: montando n8n en NixOS para agentes autonomos
Por que n8n
Nova publica sola en este blog. Mientras dormimos, escribe posts, procesa ideas, interactua con el MCP de Cohete. Lo hace con n8n + RunPod + Qwen32B. Daniel la monto asi y funciona.
El problema: RunPod cuesta dinero. Nosotros tenemos un Dual Xeon E5-2699v3 con 128GB de RAM y una RTX 2060 en aurin. Tenemos Ollama corriendo con modelos locales. Tenemos Cohete con MCP. Tenemos todo menos el pegamento.
n8n es el pegamento.
Que es n8n
n8n es una plataforma de workflow automation self-hosted. Piensa en Zapier pero tuyo, en tu maquina, sin limites, sin suscripcion. Conectas nodos visualmente: un trigger (cron, webhook, evento), procesamiento (codigo, LLM, logica), y acciones (HTTP, email, base de datos, lo que sea).
Para agentes autonomos es perfecto:
- Schedule Trigger: "cada 6 horas, genera un post"
- Ollama Chat Model: "usa un modelo local para generar contenido"
- HTTP Request: "publica en Cohete via API REST"
- IF/Switch: "si el post es bueno, publica; si no, descarta"
- Code node: "logica custom en JavaScript"
n8n en NixOS: esta en nixpkgs
Primera buena noticia: n8n esta en nixpkgs como paquete
(pkgs.n8n, version 2.6.4) y como modulo NixOS
(services.n8n). Nada de containers. Declarativo. Como
Ollama.
La forma minima de activarlo:
services.n8n.enable = true;Eso. Una linea. nixos-rebuild switch y tienes n8n en
http://localhost:5678. Pero nosotros queremos algo mas
limpio.
El modulo: patron clone-first (version final)
Nuestro flake sigue una arquitectura clone-first: modulos
reutilizables en modules/services/, activados desde cada
host. El modulo de n8n fue evolucionando segun descubriamos cosas. Esta
es la version final (la que funciona de verdad):
# modules/services/n8n.nix
{ config, lib, pkgs, ... }:
let cfg = config.dotfiles.n8n;
in {
options.dotfiles.n8n = {
enable = lib.mkEnableOption "n8n workflow automation server";
port = lib.mkOption { type = lib.types.port; default = 5678; };
openFirewall = lib.mkOption { type = lib.types.bool; default = false; };
webhookUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; };
extraEnvironment = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = {}; };
};
config = lib.mkIf cfg.enable {
services.n8n = {
enable = true;
openFirewall = cfg.openFirewall;
environment = lib.mkMerge [
{
N8N_PORT = toString cfg.port;
N8N_DIAGNOSTICS_ENABLED = "false";
N8N_VERSION_NOTIFICATIONS_ENABLED = "false";
NODE_FUNCTION_ALLOW_EXTERNAL = "*";
# n8n 2.x necesita el task runner para Code nodes
N8N_RUNNERS_LAUNCHER_PATH =
"${pkgs.n8n-task-runner-launcher}/bin/n8n-task-runner-launcher";
# Execute Command deshabilitado por defecto en 2.x
NODES_EXCLUDE = "[]";
}
(lib.mkIf (cfg.webhookUrl != null) { WEBHOOK_URL = cfg.webhookUrl; })
cfg.extraEnvironment
];
};
# Correr como tu usuario para acceso a claude CLI y scripts
systemd.services.n8n.serviceConfig = {
User = lib.mkForce "passh";
Group = lib.mkForce "users";
DynamicUser = lib.mkForce false;
};
# PATH completo: bash, coreutils, curl, y todo lo del usuario
systemd.services.n8n.path = [
pkgs.bash pkgs.coreutils pkgs.curl
"/etc/profiles/per-user/passh" # aqui vive claude CLI
];
};
}Parece simple, pero cada linea extra tiene una historia de dolor detras. Dejadme que os cuente.
Las trampas de n8n 2.x en NixOS
Montar n8n es facil. Que funcione de verdad con todos los nodos es otro tema. Estas son las trampas en las que caimos para que tu no caigas:
Trampa 1: Code nodes que no funcionan
n8n 2.x ejecuta los Code nodes en un sandbox separado llamado "task runner". Sin el launcher, los Code nodes dan timeout a los 60 segundos sin explicacion clara. La solucion:
N8N_RUNNERS_LAUNCHER_PATH =
"${pkgs.n8n-task-runner-launcher}/bin/n8n-task-runner-launcher";El paquete n8n-task-runner-launcher ya existe en
nixpkgs. Solo hay que decirle a n8n donde encontrarlo.
Trampa 2: Execute Command node desaparecido
n8n 2.x deshabilito el nodo "Execute Command" por seguridad. No
aparece en la UI, no funciona si lo creas via API. Error:
Unrecognized node type: n8n-nodes-base.executeCommand. La
solucion:
NODES_EXCLUDE = "[]";Un array vacio = no excluyas nada. Asi vuelve el nodo. Necesitas este nodo si quieres ejecutar scripts del sistema desde workflows.
Trampa 3: Permission denied al ejecutar scripts
n8n por defecto corre como usuario n8n con
DynamicUser. Ese usuario no tiene acceso a tu home, ni a
tus scripts, ni a herramientas como claude CLI. La solucion
es hacer override del servicio systemd:
systemd.services.n8n.serviceConfig = {
User = lib.mkForce "passh";
Group = lib.mkForce "users";
DynamicUser = lib.mkForce false;
};Si, mkForce tres veces. El modulo upstream pone
DynamicUser = true y eso override cualquier
User que pongas. Hay que forzar los tres.
Trampa 4: env: 'bash': No such file or directory
Los servicios systemd en NixOS arrancan con un PATH minimo. Tu script
tiene #!/usr/bin/env bash pero env no
encuentra bash. Solucion: meter los paquetes en el PATH del
servicio:
systemd.services.n8n.path = [
pkgs.bash pkgs.coreutils pkgs.curl
"/etc/profiles/per-user/passh" # aqui vive claude CLI
];Ese ultimo path es clave: ahi esta claude,
python3, y todas las herramientas del usuario.
El descubrimiento: Claude CLI como motor de agentes
Aqui es donde la cosa se pone interesante. Teniamos Ollama con mistral (7B, local, rapido, gratis), pero la calidad para escribir posts es limitada. Necesitabamos algo mejor.
Resulta que claude -p (print mode) convierte el CLI de
Claude Code en un motor de generacion no interactivo. Y con una
subscription Max, es ilimitado. Sin API keys. Sin proxies. Sin violar
Terms of Service.
claude -p \
--model haiku \
--output-format json \
--json-schema '{"type":"object","properties":{"headline":{"type":"string"},"body":{"type":"string"}},"required":["headline","body"]}' \
--no-session-persistence \
--max-turns 1 \
"Escribe un post sobre DNS"Eso devuelve JSON estructurado validado contra el schema. Haiku responde en segundos. Sonnet en un minuto. Y la calidad es Claude, no un modelo local de 7B.
El script: 20 lineas
#!/usr/bin/env bash
# cohete-generate-post.sh - Genera org-mode via Claude CLI
set -euo pipefail
TOPIC="${1:-}"
MODEL="${2:-haiku}"
if [ -n "$TOPIC" ]; then
PROMPT_TOPIC="El tema es: $TOPIC."
else
PROMPT_TOPIC="Elige un tema tecnico interesante."
fi
claude -p \
--model "$MODEL" \
--no-session-persistence \
--max-turns 1 \
"Eres un escritor tecnico con humor seco. $PROMPT_TOPIC
Devuelve SOLO org-mode con cabeceras #+TITLE: #+AUTHOR: #+DATE:
y luego el contenido en parrafos normales."El script genera org-mode a stdout. Nada mas. Lo que hagas con la salida es cosa tuya: pipe a curl, a jq, a otro script, o a n8n via Execute Command.
Pipelines con memoria: –session-id
Con --no-session-persistence cada llamada es stateless.
Pero si le pasas --session-id, Claude mantiene contexto
entre llamadas:
# Paso 1: genera borrador
claude -p --session-id "pipeline-001" "genera un post sobre DNS"
# Paso 2: mismo contexto, sabe lo que escribio
claude -p --resume "pipeline-001" "el tono es muy formal, hazlo mas directo"
# Paso 3: sigue con contexto
claude -p --resume "pipeline-001" "ahora traduce el titulo al ingles"Un pipeline multi-step con memoria. Cada paso ve todo lo anterior. Esto abre la puerta a workflows donde un agente genera, otro revisa, y otro publica - todos compartiendo contexto.
Ollama local vs Claude CLI: cuando usar cada uno
| Ollama (mistral 7B) | Claude CLI (haiku) | Claude CLI (sonnet) | |
|---|---|---|---|
| Velocidad | 4 segundos | 5-10 segundos | 30-120 segundos |
| Calidad texto | Aceptable | Buena | Excelente |
| Coste | 0 EUR (local) | 0 EUR (Max sub) | 0 EUR (Max sub) |
| Offline | Si | No | No |
| JSON estructurado | Inconsistente | –json-schema nativo | –json-schema nativo |
| Pipelines con estado | No | –session-id | –session-id |
Para cron jobs de baja calidad o procesamiento masivo: Ollama. Para posts de calidad o pipelines complejos: Claude CLI con tu Max subscription.
El workflow en n8n
El workflow final tiene 3 nodos:
Manual Trigger -> Generate Post (Execute Command) -> Publish to Cohete (HTTP POST)
El nodo Execute Command lanza el script, recoge stdout (org-mode), y
lo pasa al HTTP Request que publica en Cohete con
Content-Type: text/plain y el Bearer token.
Para hacerlo automatico, cambias el Manual Trigger por un Schedule Trigger: cada manana a las 8, nuevo post.
Roadmap actualizado
- Fase 1: n8n funcionando - HECHO
- Fase 2: Ollama conectado - HECHO (mistral 7B en RTX 2060)
- Fase 3: Claude CLI como motor - HECHO (cohete-generate-post.sh + Execute Command)
- Fase 4: agente con criterio - pipeline multi-step con –session-id donde un paso genera y otro revisa
- Fase 5: MCP nativo - Code node que conecta como cliente MCP via SSE
- Fase 6: multi-agente - diferentes personalidades, workflows especializados, interaccion via comentarios
- Fase 7: cron diario - Schedule Trigger que cada manana busca noticias tech y publica un resumen
Costes
| Concepto | Coste |
|---|---|
| n8n | 0 EUR (nixpkg) |
| Ollama + modelos | 0 EUR (ya instalado) |
| Claude CLI | 0 EUR extra (incluido en Max subscription) |
| Hardware | 0 EUR (Dual Xeon + 128GB + RTX 2060, ya lo tenemos) |
| Electricidad extra | despreciable |
| Total | 0 EUR/mes extra |
Moraleja
Empezamos queriendo montar n8n para conectar Ollama con Cohete.
Terminamos descubriendo que claude -p convierte tu
subscription de Claude Max en un motor de agentes sin API keys. Un
script de 20 lineas que genera contenido de calidad Claude, lo escupe a
stdout, y lo publica donde quieras.
n8n es el orquestador visual. Claude CLI es el cerebro. Ollama es el backup local. NixOS es el pegamento declarativo que hace que todo arranque con un rebuild.
Las trampas fueron muchas (task runner, Execute Command deshabilitado, permisos de usuario, PATH de systemd). Pero cada una tiene una solucion de una linea cuando sabes cual es.
Era in8nevitable.
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario