n8n en producción: sub-workflows, dead letter queues y Code Node avanzado

Conectar dos servicios con cuatro nodos en n8n es la parte fácil. La parte difícil llega cuando ese workflow procesa pedidos reales, sincroniza bases de datos o mueve dinero, y algo falla a las tres de la mañana. Ahí es donde la diferencia entre un workflow improvisado y uno diseñado para producción se hace evidente.
Este artículo asume que ya sabes crear workflows, configurar webhooks y usar los nodos principales. Lo que cubrimos son los patrones que cualquier equipo que opera n8n en serio acaba descubriendo por las malas: sub-workflows para reutilizar lógica, dead letter queues para no perder datos, transformaciones complejas con el Code Node y estrategias de monitoreo que detectan problemas antes de que lleguen a los usuarios.
Sub-workflows: reutilización sin duplicación
Cuando tienes veinte workflows activos y quince de ellos hacen la misma transformación de datos antes de guardar en Supabase, tienes un problema de mantenimiento. Cada vez que cambia el esquema de la base de datos, actualizas quince workflows. Cada vez que hay un bug en la transformación, lo corriges quince veces.
La solución son los sub-workflows: workflows diseñados para ser llamados desde otros workflows mediante el nodo Execute Workflow. El hijo recibe datos, los procesa y devuelve el resultado al padre.
// Sub-workflow: normalizar_contacto
// Entrada: { nombre, email, telefono, empresa }
// Salida: { nombre, email, telefono_formateado, slug_empresa, procesado_en }
const item = $input.item.json;
const nombre = item.nombre
?.trim()
.split(' ')
.map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
.join(' ') || '';
const telefono = item.telefono
?.replace(/\D/g, '')
.replace(/^34/, '')
.replace(/^(\d{3})(\d{3})(\d{3})$/, '$1 $2 $3') || null;
const slugEmpresa = item.empresa
?.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || null;
return [{ json: {
nombre,
email: item.email?.toLowerCase().trim() || null,
telefono_formateado: telefono,
slug_empresa: slugEmpresa,
procesado_en: new Date().toISOString(),
} }];Este sub-workflow se llama desde cualquier workflow que necesite normalizar un contacto. Cuando el formato cambia, lo actualizas en un solo lugar.
Cuándo no usar sub-workflows
Los sub-workflows añaden latencia por cada llamada. Para transformaciones simples de un solo campo, un nodo Set o una expresión inline es más rápido y más legible. Reserva los sub-workflows para lógica que se repite en tres o más workflows y que tiene suficiente complejidad para justificar la indirección.
Dead letter queues: no pierdas datos cuando algo falla
El comportamiento por defecto de n8n cuando un nodo falla es detener la ejecución. Si no tienes configurado un workflow de error, esos datos se pierden silenciosamente. En un pipeline de pagos o sincronización de inventario, eso es inaceptable.
// Nodo Code en el workflow de error global
// Settings → Error Workflow → apunta a este workflow
const errorInfo = {
workflow_id: $workflow.id,
workflow_name: $workflow.name,
node_nombre: $execution.error?.node?.name || 'desconocido',
mensaje_error: $execution.error?.message || 'Error desconocido',
stack_trace: $execution.error?.stack || null,
datos_originales: JSON.stringify($input.item.json),
execution_id: $execution.id,
timestamp: new Date().toISOString(),
};
return [{ json: errorInfo }];
// → Conectar a Supabase → INSERT en tabla error_logs
// → Conectar a Send Email o Telegram → alerta al equipoReintentos con backoff exponencial
Para errores temporales como timeouts de API, los reintentos con espera creciente reducen los fallos permanentes:
const maxIntentos = 3;
const intento = $input.item.json._intento || 1;
if (intento > maxIntentos) {
throw new Error(`Máximo de ${maxIntentos} intentos alcanzado para item ${$input.item.json.id}`);
}
// Espera: 2s → 4s → 8s
const esperaMs = Math.pow(2, intento) * 1000;
await new Promise(r => setTimeout(r, esperaMs));
return [{ json: { ...$input.item.json, _intento: intento + 1 } }];Code Node avanzado: transformaciones que no caben en nodos visuales
Sincronización incremental
Procesar únicamente los registros que han cambiado desde la última ejecución:
const actuales = $input.first().json.productos_actuales;
const anteriores = $input.last().json.productos_anteriores;
const mapaAnterior = new Map(anteriores.map(p => [p.id, p]));
const cambios = actuales.reduce((acc, p) => {
const anterior = mapaAnterior.get(p.id);
if (!anterior) {
acc.nuevos.push(p);
} else if (anterior.precio !== p.precio) {
acc.cambios_precio.push({ ...p, precio_anterior: anterior.precio });
} else if (anterior.stock !== p.stock) {
acc.cambios_stock.push(p);
}
return acc;
}, { nuevos: [], cambios_precio: [], cambios_stock: [] });
return [{ json: {
...cambios,
total_nuevos: cambios.nuevos.length,
total_cambios_precio: cambios.cambios_precio.length,
total_cambios_stock: cambios.cambios_stock.length,
procesado_en: new Date().toISOString(),
} }];Errores comunes en workflows de producción
Workflows monolíticos de 40 nodos. Un workflow que hace todo es un workflow que nadie entiende cuando falla. Divide por responsabilidad: un workflow recolecta, otro transforma, otro persiste.
No versionar los workflows. Exporta todos los workflows como JSON y guárdalos en Git. Sin versionado no hay rollback cuando algo se rompe.
Credenciales en nodos. Nunca pongas URLs de API o tokens directamente en la configuración de un nodo. Usa el sistema de credenciales de n8n y variables de entorno con $env.NOMBRE_VARIABLE.
Sin alertas de duración. Un workflow que normalmente tarda 30 segundos y empieza a tardar 15 minutos tiene un problema. Guarda el timestamp de inicio y fin y configura alertas si la duración supera un umbral.
Loops infinitos entre webhooks. Si el workflow A llama al webhook de B y B llama al de A, tienes un bucle infinito. Añade siempre una condición de corte.
Buenas prácticas para operar n8n en equipo
Nombra todos los nodos descriptivamente desde el primer momento. "Guardar contacto en Supabase" es infinitamente más útil que "Supabase3" cuando debugeas a las dos de la mañana.
Implementa un workflow de healthcheck que se ejecuta cada cinco minutos y alerta si algún servicio crítico no responde. Es mejor recibir una alerta preventiva que enterarte del fallo por un cliente.
Fija la versión de la imagen Docker de n8n en tu docker-compose.yml. Las actualizaciones automáticas de imagen pueden romper workflows que dependían del comportamiento de una versión específica.
Conclusión
El salto de usar n8n para automatizaciones simples a construir pipelines de producción fiables requiere cambiar de mentalidad: de "¿cómo encadeno estos nodos?" a "¿qué pasa cuando falla el nodo tres a las 3am con 500 items en cola?". Los sub-workflows resuelven la reutilización. Las dead letter queues resuelven la fiabilidad. El Code Node resuelve las transformaciones que ningún nodo visual puede hacer. Con esos tres pilares, n8n escala a pipelines de producción serios sin fricciones.