Módulo 3: Contratos entre workflows
5. Validar las entradas en la frontera
Descripción
Al terminar esta lección vas a poder hacer que un sub-workflow rechace, en su propia puerta y antes de hacer nada, cualquier entrada que no cumpla su contrato: campos obligatorios que faltan, tipos equivocados, reglas de negocio violadas. Vas a saber validar tanto con nodos nativos como con un nodo Code que revisa los campos del item, devolver un error claro con la forma que el contrato promete, y enrutar el flujo para que un dato malo nunca llegue al efecto. Y vas a entender por qué "fallar temprano y con un mensaje útil" es una de las decisiones que más protege a un sistema.
Esto importa porque en la lección anterior pusiste al portero en la puerta, pero el portero todavía no revisa nada a fondo. Declarar los campos en el trigger le dijo a n8n los nombres y los tipos esperados, y eso ayuda a los llamadores —pero no impide que un amount negativo, un customer_id vacío o un número que llegó como texto se cuelen y lleguen hasta el corazón del sub-workflow—. Si ese sub-workflow solo consulta, un dato malo produce un resultado sin sentido. Si el sub-workflow mueve dinero, como issue-refund, un dato malo produce un efecto irreversible sobre datos equivocados. La validación en la frontera es la diferencia entre las dos cosas.
Conexión con el módulo: la lección 4 construyó la frontera y declaró el esquema en el trigger. Esta lección le da trabajo real al portero: verificar que la entrada cumple el contrato que diseñaste en la lección 3, y rechazar con la forma de error que definiste. Aquí es donde el contrato de dos partes de la lección 2 se hace valer —el que responde ejerce su derecho de rechazar una entrada que no cumple—. Y aquí es donde este módulo se da la mano con la idempotencia del Módulo 2: validar antes del efecto es lo que impide que un dato malformado dispare un reembolso equivocado. La lección 8 integra esta validación en el sub-workflow completo.
Fallar temprano: el control en la puerta, no adentro del avión
Piensa en cómo funciona la seguridad de un aeropuerto. La revisión ocurre en el filtro, antes de que subas al avión. No en el pasillo, no en tu asiento, no a mitad del vuelo. La razón es obvia cuando la dices en voz alta: cuanto más adentro dejas pasar un problema, más caro y más peligroso es sacarlo. Detectar un objeto prohibido en el filtro es un trámite de treinta segundos; detectarlo cuando el avión ya despegó es una emergencia. El mismo objeto, el mismo problema —lo que cambia es cuándo lo encontraste—.
Un dato inválido en un sub-workflow es ese objeto prohibido. Si lo detectas en la puerta —en el primer nodo, apenas cruzó la frontera—, rechazarlo cuesta nada: devuelves un error claro y no pasó nada malo. Si lo dejas pasar y lo detectas tres nodos adentro, cuando ya se usó en un cálculo, el daño está a medio hacer y desenredarlo es difícil. Y si lo dejas llegar hasta el efecto —el nodo que emite el reembolso, que llama a la API, que escribe en el CRM—, entonces el problema ya no es un dato malo: es un reembolso emitido sobre el pedido equivocado, una llamada hecha con datos basura, un registro corrupto. Un dato malo en la puerta es una molestia; el mismo dato malo pasado el efecto es un incidente.
Este es el principio de la lección, y cabe en una frase: valida en la puerta, no adentro del avión. Rechaza lo que no cumple el contrato lo más cerca posible de la entrada, antes de que toque nada. En la analogía de la lección 4, es el portero haciendo por fin su trabajo: no solo parado en la recepción, sino revisando de verdad quién trae lo que el contrato exige antes de dejarlo pasar al edificio.
Por qué el esquema del trigger no alcanza
Quizás pienses: "pero en la lección 4 ya declaré los campos en el trigger, ¿eso no valida?". Vale la pena ser preciso sobre qué hace y qué no hace la declaración del esquema, porque confundirlo deja huecos peligrosos.
Declarar los campos en el Execute Sub-workflow Trigger hace dos cosas útiles: le dice a n8n qué nombres y tipos espera el sub-workflow, y le muestra esos campos como guía a los llamadores. Es documentación ejecutable, y ayuda. Pero no es una aduana estricta. No garantiza que el amount que llegó sea de verdad un número —un llamador con un bug puede mandar texto—, no comprueba que amount sea positivo, no verifica que customer_id corresponda a un cliente que existe, ni que la currency sea una de las que Cumbre maneja. El trigger describe la forma esperada; no rechaza lo que no la cumple.
Piénsalo así: declarar el esquema es como poner un letrero en la puerta que dice "se requiere identificación con foto". El letrero informa, orienta, y la mayoría de la gente llega con su identificación por haberlo leído. Pero el letrero no revisa a nadie. Para revisar de verdad —"a ver, muéstrame esa identificación; no, esta está vencida; no, esta foto no eres tú"— hace falta un portero que mire cada caso y decida. La validación de esta lección es ese portero. El letrero (el esquema del trigger) y el portero (la validación) trabajan juntos: el letrero reduce los errores honestos, y el portero atrapa los que pasan igual.
Qué se valida: las tres capas
Una validación completa en la frontera revisa tres cosas, de la más básica a la más específica. Conviene tenerlas separadas porque son promesas distintas del contrato.
Capa 1: presencia de los obligatorios. ¿Llegaron todos los campos que el contrato marca como obligatorios? Para check-credit: ¿está customer_id? ¿está order_id? ¿está amount? Un obligatorio que falta es la violación más común y la más fácil de detectar. Aquí también aplicas los valores por defecto de los opcionales: si currency no llegó, no es un error —el contrato lo permite—, pero hay que rellenarlo con "MXN" para que el resto del sub-workflow no trabaje con un hueco.
Capa 2: tipos correctos. ¿Cada campo es del tipo que el contrato promete? ¿amount es de verdad un number, o llegó como el texto "1842.50"? Esta capa atrapa el error más silencioso del módulo: el número que se ve como número pero es texto, y que hace que las comparaciones den resultados absurdos. Un campo presente pero del tipo equivocado es tan inválido como uno ausente.
Capa 3: reglas de negocio. ¿El valor tiene sentido para el negocio, más allá de su tipo? Un amount puede ser un número perfectamente formado y aun así ser inválido si es -500 —no existe un pedido de monto negativo—. currency puede ser un texto válido y aun así ser inválido si es "XYZ", una moneda que Cumbre no maneja. Esta capa es la que el esquema del trigger nunca podría cubrir, porque depende del conocimiento del negocio, no solo de la forma del dato.
Las tres capas juntas responden la pregunta completa: "¿esta entrada cumple de verdad el contrato, en forma y en fondo?". Una validación que solo hace la capa 1 deja pasar el texto disfrazado de número; una que hace 1 y 2 pero no 3 deja pasar el monto negativo. Un portero serio revisa las tres.
Con qué se valida: nodos nativos o nodo Code
Tienes dos herramientas para construir la validación, y conviene saber cuándo usar cada una.
Nodos nativos (If, Filter, Switch). Puedes revisar condiciones con nodos visuales. Un nodo If que pregunta "¿customer_id está vacío?" enruta los que fallan hacia la salida de error. Es legible y no requiere código. Su límite: los nodos nativos son cómodos para comprobar presencia y comparar valores ("¿amount es mayor que cero?"), pero se vuelven torpes para verificar tipos con precisión ("¿amount es un número o un texto que parece número?") y para juntar varios errores en un solo mensaje. Para validaciones simples, alcanzan y son claros.
Un nodo Code que revisa los campos del item. Cuando la validación involucra tipos, varias reglas, o quieres devolver un mensaje que liste todos los problemas de una vez, un nodo Code es más limpio. Y aquí conviene ser explícito sobre lo que un nodo Code sí puede hacer en n8n 2.0, porque es justo lo que necesitamos: leer y revisar los campos del item que entró. Eso está permitido sin ninguna restricción —el código solo inspecciona datos que ya tiene—.
Lo que un nodo Code no puede hacer en n8n 2.0 no nos estorba aquí, pero vale la pena recordarlo para no diseñar una validación imposible: desde un nodo Code no puedes hacer peticiones HTTP (nada de fetch ni axios), no puedes acceder al sistema de archivos, no puedes usar require salvo para crypto y moment, y en n8n Cloud esos dos son los únicos módulos disponibles; tampoco puedes leer variables de entorno con $env ni usar helpers como this.helpers o this.getCredentials. Todo eso quedó bloqueado por el aislamiento en procesos separados que trajo la versión 2.0. Pero fíjate en lo que esto significa para nosotros: validar la entrada nunca necesita nada de eso. La validación solo mira los campos que ya llegaron y decide si cumplen —una operación puramente local—. Por eso la restricción de n8n 2.0 no es un obstáculo para esta lección: la validación de un contrato es exactamente el tipo de trabajo que un nodo Code sí puede hacer.
Hay una excepción a tener presente: la capa 3, las reglas de negocio, a veces necesita comparar contra datos externos —"¿customer_id existe en la base de clientes?"—. Esa comprobación específica no cabe en el nodo Code, porque exigiría consultar una base de datos, y para eso están los nodos nativos (un nodo de base de datos que busca el cliente). La validación de forma y de reglas locales va en el Code; la validación que requiere consultar un sistema externo va en nodos nativos antes o después. En esta lección nos concentramos en las capas 1, 2 y las reglas 3 que son locales; la validación contra sistemas externos se apoya en los nodos de integración correspondientes.
Ejemplo trabajado: validar la entrada de check-credit
Vamos a construir la validación de check-credit con un nodo Code, cubriendo las tres capas, y a enrutar el resultado para que un dato malo nunca pase. El nodo va justo después del Execute Sub-workflow Trigger —lo primero que ocurre cuando algo cruza la frontera—.
// Nodo: Code — "Validate input"
// Modo: Run Once for Each Item
// Va justo después del Execute Sub-workflow Trigger de check-credit.
// Solo LEE los campos del item para revisar el contrato: no hace HTTP ni toca
// archivos, así que respeta las restricciones del nodo Code en n8n 2.0.
const input = $input.item.json; // el item que entró por la frontera
const errors = []; // aquí voy juntando todos los problemas que encuentre
// --- Capa 1: los obligatorios están presentes ---
if (input.customer_id === undefined || input.customer_id === null || input.customer_id === '') {
errors.push('customer_id es obligatorio');
}
if (input.order_id === undefined || input.order_id === null || input.order_id === '') {
errors.push('order_id es obligatorio');
}
if (input.amount === undefined || input.amount === null) {
errors.push('amount es obligatorio');
}
// --- Capa 2: los tipos son los que el contrato promete ---
// typeof me dice de qué está hecho un valor: 'string', 'number', 'boolean'...
if (input.customer_id !== undefined && typeof input.customer_id !== 'string') {
errors.push('customer_id debe ser texto');
}
if (input.amount !== undefined && typeof input.amount !== 'number') {
errors.push('amount debe ser un número'); // "1842.50" como texto cae aquí: no es number
}
// --- Capa 3: reglas de negocio (las que se pueden revisar sin salir del nodo) ---
if (typeof input.amount === 'number' && input.amount <= 0) {
errors.push('amount debe ser mayor que cero'); // no existe un pedido de monto negativo
}
// --- Opcional con default: si currency no llego, el contrato dice asumir MXN ---
const currency = input.currency ?? 'MXN'; // ?? usa 'MXN' solo si currency es null/undefined
if (errors.length > 0) {
// No cumple el contrato. Devuelvo la forma de FALLO del contrato y NO dejo pasar
// el dato malo hacia el efecto. join('; ') junta todos los errores en un mensaje.
return {
json: {
ok: false,
error: {
code: 'INVALID_INPUT',
message: errors.join('; '),
},
},
};
}
// Cumple el contrato. Paso el item hacia adelante, ya con el default aplicado y
// con ok: true, para que el nodo If de después sepa que puede seguir al efecto.
return {
json: {
ok: true,
customer_id: input.customer_id,
order_id: input.order_id,
amount: input.amount,
currency: currency,
},
};
Después de este nodo Code, pones un nodo If que pregunta por {{ $json.ok }}:
- Si
okes true, el flujo sigue hacia la lógica de crédito (consultar el saldo, comparar, decidir) y termina produciendo la respuesta de éxito. - Si
okes false, el item ya tiene la forma de fallo del contrato ({ ok: false, error: { code, message } }), así que esa rama va directo a la salida del sub-workflow. Recuerda la lección 4: lo que el llamador recibe es lo que produce el último nodo del camino; en la rama de fallo, ese último nodo lleva el error del contrato.
[Execute Sub-workflow Trigger]
│
[Code: Validate input]
│
[If: ok?]
├─ true → [lógica de crédito] → [respuesta de éxito] → salida
└─ false → (el error ya tiene la forma del contrato) → salida
Qué esperar. Con una entrada buena —{ customer_id: "CUST-118", order_id: "ORD-2041", amount: 1842.50 }—, el nodo Code produce { ok: true, customer_id: "CUST-118", order_id: "ORD-2041", amount: 1842.50, currency: "MXN" } (fíjate en el currency: "MXN" que se rellenó solo), el If toma la rama true, y el sub-workflow procede a consultar el crédito. Con una entrada mala —digamos { customer_id: "CUST-118", order_id: "ORD-2041", amount: "-50" }, con el monto como texto y negativo—, el nodo Code junta dos problemas: amount no es un número (es texto) y, aunque lo fuera, sería menor que cero. Produce { ok: false, error: { code: "INVALID_INPUT", message: "amount debe ser un número; amount debe ser mayor que cero" } }, el If toma la rama false, y el sub-workflow devuelve ese error sin haber tocado la lógica de crédito. El dato malo nunca pasó del filtro. Y del lado de order-triage, la respuesta que vuelve es la forma de fallo que su contrato esperaba, lista para que su rama de error la maneje.
Nota que el mensaje lista todos los problemas de una vez —"es texto Y es negativo"—, no solo el primero. Esto es cortesía con quien depura: un mensaje que dice los tres errores de la entrada ahorra tres viajes de ida y vuelta para descubrirlos uno por uno.
El mensaje de error es parte del contrato
Es tentador tratar el mensaje de error como un detalle menor —"total, si falla, falla"—. Pero el mensaje es lo que convierte un rechazo en información útil, y merece el mismo cuidado que el resto del contrato.
Compara dos formas de rechazar la misma entrada. La primera: el sub-workflow simplemente se cae con un error interno de n8n, algo como "Cannot read property of undefined". Quien lo recibe no sabe qué campo faltó, ni por qué, ni qué corregir; le toca abrir el sub-workflow y hacer ingeniería inversa del fallo. La segunda: el sub-workflow devuelve { ok: false, error: { code: "INVALID_INPUT", message: "amount es obligatorio y debe ser un número" } }. Quien lo recibe sabe exactamente qué pasó y qué arreglar, sin abrir nada. El mismo rechazo; una diferencia enorme en cuánto cuesta entenderlo.
Dos piezas hacen útil un mensaje de error. El code es una etiqueta estable y corta, de una lista conocida (INVALID_INPUT, CUSTOMER_NOT_FOUND, ALREADY_REFUNDED), pensada para que una máquina la lea y decida: order-triage puede tener una rama distinta según el code. El message es la explicación legible, pensada para que un humano que lee el log entienda de un vistazo. Los dos importan: el code para que el sistema reaccione, el message para que la persona diagnostique. Un error con message pero sin code obliga a los llamadores a leer texto para decidir —frágil—; uno con code pero sin message deja al humano a ciegas. El contrato de fallo que diseñaste en la lección 3 tiene los dos por esta razón.
Dónde va la validación: un solo cuello de botella
Una última decisión de diseño, y es la que amarra todo con la lección 4. La validación va en un solo lugar: justo después del Execute Sub-workflow Trigger, antes de cualquier otra cosa. No repartida en comprobaciones sueltas por todo el sub-workflow, no a mitad de camino. Un solo cuello de botella por donde pasa toda entrada.
La razón es la misma de la puerta única de la lección 4. Si la validación está concentrada en un punto, hay un solo lugar donde revisar qué se valida, un solo lugar que actualizar cuando el contrato cambie, y una garantía clara: si algo pasó de este nodo, cumple el contrato. Si en cambio repartes las comprobaciones —un poco aquí, otro poco tres nodos adelante—, pierdes esa garantía: nunca sabes con certeza si un dato ya fue validado por completo o solo a medias, y actualizar la validación se vuelve una cacería por todo el lienzo. El portero está en la puerta, no distribuido por los pasillos. Una entrada se valida una vez, entera, al entrar; de ahí en adelante, el resto del sub-workflow puede confiar en que trabaja con datos que cumplen el contrato.
Esto conecta directo con el efecto. Como la validación está antes de todo, y como un dato malo toma la rama de fallo y sale sin tocar la lógica, el efecto nunca ve un dato inválido. En check-credit, que solo lee, esto evita un resultado sin sentido. En issue-refund, que mueve dinero, esto evita algo mucho peor: un reembolso emitido sobre datos basura. Aquí es donde la validación de este módulo y la idempotencia del Módulo 2 se abrazan —la validación garantiza que el efecto reciba datos correctos; la idempotencia garantiza que el efecto no se aplique dos veces—. Las dos protegen el mismo punto sensible desde ángulos distintos.
Errores comunes
Validar después del efecto en vez de antes (conceptual). Qué pasa: alguien pone la comprobación de "¿amount es válido?" después del nodo que ya consultó el crédito o —peor— después del que ya emitió el reembolso; para cuando el error se detecta, el efecto ya ocurrió. Por qué pasa: al construir el sub-workflow de arriba hacia abajo, es fácil poner la lógica principal primero y "agregar las validaciones después", y "después" termina significando después en el flujo, no solo después en el tiempo de construcción. Cómo detectarlo: mira dónde está tu nodo de validación respecto de tu efecto; si el efecto puede ejecutarse antes de que la validación haya rechazado una entrada mala, está en el lugar equivocado. Cómo corregirlo: la validación va inmediatamente después del trigger, antes de cualquier efecto, sin excepción. El principio del aeropuerto: se revisa en el filtro, no en el asiento.
Confiar en el esquema del trigger como si validara (práctico). Qué pasa: alguien declara los campos en el Execute Sub-workflow Trigger, ve que aparecen como guía en el llamador, y concluye que "ya está validado"; en producción, un llamador con un bug manda amount como texto y el sub-workflow lo procesa como si nada. Por qué pasa: la declaración del esquema se siente como una barrera porque muestra tipos y ayuda a los llamadores, pero es un letrero informativo, no un portero. Cómo detectarlo: pregúntate "si un llamador ignora el esquema y manda basura, ¿algo la rechaza?". Si la única respuesta es "el trigger la declaró", no hay rechazo real. Cómo corregirlo: agrega una validación explícita después del trigger. El esquema orienta y reduce errores honestos; la validación es la que de verdad rechaza. Se necesitan las dos.
Un mensaje de error inútil (práctico). Qué pasa: la validación rechaza correctamente, pero devuelve algo como { ok: false, error: "invalid" } o simplemente deja que el sub-workflow se caiga con un error interno de n8n; quien lo recibe no sabe qué campo falló ni por qué. Por qué pasa: al escribir la validación, uno tiene el contexto fresco en la cabeza y no siente la falta de detalle; el problema aparece semanas después, cuando alguien más recibe el error sin ese contexto. Cómo detectarlo: lee tu mensaje de error como si no supieras nada del sub-workflow; si no te dice qué campo corregir, es inútil. Cómo corregirlo: devuelve siempre un code estable (para que la máquina decida) y un message que nombre el campo y el problema concreto ("amount debe ser un número"), idealmente listando todos los problemas de una vez. Un buen mensaje de error es la diferencia entre un arreglo de treinta segundos y una tarde de depuración.
Ejercicios
Ejercicio 1 — Clasifica cada validación por capa. Para issue-refund (recibe order_id, amount, reason), clasifica cada una de estas comprobaciones en su capa: presencia de obligatorios (1), tipo correcto (2) o regla de negocio local (3). Marca también cuál de ellas no cabría en un nodo Code y por qué.
(a) order_id no está vacío.
(b) amount es un number, no un texto.
(c) amount es mayor que cero.
(d) order_id corresponde a un pedido que existe en la base de datos.
Ver solución
(a) Capa 1 (presencia). Solo comprueba que el obligatorio llegó. Cabe en un nodo Code sin problema.
(b) Capa 2 (tipo). Comprueba que amount sea del tipo que el contrato promete. Cabe en un nodo Code con typeof.
(c) Capa 3 (regla de negocio local). Un monto de reembolso negativo o cero no tiene sentido. Es una regla de negocio, pero es local —se decide solo mirando el valor—, así que cabe en un nodo Code.
(d) Capa 3 (regla de negocio), y NO cabe en un nodo Code. Comprobar que el pedido existe requiere consultar la base de datos, y desde un nodo Code en n8n 2.0 no puedes hacer peticiones ni consultar sistemas externos (nada de fetch, axios ni acceso a credenciales). Esta comprobación va con un nodo nativo de base de datos, antes o después del Code, no dentro de él.
Por qué funciona: el ejercicio separa las reglas de negocio locales (que solo miran el valor que llegó, como "amount > 0") de las que necesitan consultar el mundo (como "este pedido existe"). Las primeras caben en el nodo Code; las segundas exigen un nodo nativo por la restricción de n8n 2.0. Saber dónde cae cada regla es lo que te evita diseñar una validación imposible —intentar consultar la base de datos desde el Code y chocar con el aislamiento—.
Ejercicio 2 — Encuentra el hueco. Este nodo Code intenta validar la entrada de check-credit, pero deja pasar un caso malo. Encuéntralo y arréglalo.
// Modo: Run Once for Each Item
const input = $input.item.json;
const errors = [];
if (!input.customer_id) errors.push('customer_id es obligatorio');
if (!input.order_id) errors.push('order_id es obligatorio');
if (!input.amount) errors.push('amount es obligatorio');
if (errors.length > 0) {
return { json: { ok: false, error: { code: 'INVALID_INPUT', message: errors.join('; ') } } };
}
return { json: { ok: true, ...input } };
Ver solución
El hueco está en la comprobación de tipos: no hay ninguna. Este código revisa la capa 1 (presencia) pero se salta la capa 2 (tipo) y la capa 3 (reglas). Un amount que llega como el texto "1842.50" pasa la comprobación —no está vacío—, y sigue hacia la lógica de crédito disfrazado de número. Ese es exactamente el error silencioso que la validación debía atrapar.
Hay además un bug sutil en if (!input.amount): en JavaScript, !0 es true, así que un amount de 0 —que también es inválido, pero por regla de negocio— sería reportado como "obligatorio faltante", un mensaje engañoso. Peor aún, si algún día un campo pudiera ser 0 legítimamente, !input.amount lo rechazaría por error.
Arreglo: agregar la comprobación de tipo y de regla, y comprobar la presencia de amount de forma que no confunda 0 con "ausente":
if (input.amount === undefined || input.amount === null) {
errors.push('amount es obligatorio');
}
if (input.amount !== undefined && typeof input.amount !== 'number') {
errors.push('amount debe ser un número'); // atrapa "1842.50" como texto
}
if (typeof input.amount === 'number' && input.amount <= 0) {
errors.push('amount debe ser mayor que cero'); // atrapa 0 y negativos, por regla
}
Por qué funciona: la validación de solo-presencia es la trampa más común, porque parece completa —revisa que los campos estén— y deja pasar justo el error que más duele: el tipo equivocado. Y el detalle de !input.amount muestra que incluso la capa 1 tiene sutilezas: comprobar presencia con una negación simple confunde "ausente" con "cero" o "cadena vacía". Validar en serio es revisar las tres capas, con cuidado en cada una.
Ejercicio 3 — Diseña el mensaje de error. Un llamador manda a check-credit esta entrada: { order_id: "ORD-2041", amount: "cero" } —falta customer_id, y amount es el texto "cero"—. Escribe el objeto de respuesta completo que debería producir la validación, con code y message, siguiendo el patrón de la lección.
Ver solución
{
"ok": false,
"error": {
"code": "INVALID_INPUT",
"message": "customer_id es obligatorio; amount debe ser un número"
}
}
La entrada tiene dos problemas y el mensaje los lista los dos: falta customer_id (capa 1) y amount es texto, no número (capa 2). El code es el estable INVALID_INPUT, para que order-triage sepa que fue un problema de entrada sin leer el texto. Nota que no reportamos "amount debe ser mayor que cero", porque "cero" ni siquiera es un número —falla antes, en la capa de tipo—; reportar la regla de negocio sobre un valor que no es número sería confuso.
Por qué funciona: un buen mensaje de error hace dos cosas bien —lista todos los problemas de una pasada, para que quien depura no descubra los errores de a uno; y no reporta reglas que no aplican, como el "mayor que cero" sobre un texto—. El orden importa: primero se revisa presencia y tipo, y solo si el valor ya es un número con sentido se le aplican las reglas de negocio. Esa disciplina hace que el mensaje sea preciso en vez de ruidoso.
Resumen y siguiente paso
En esta lección le diste trabajo real al portero. Adoptaste el principio del aeropuerto —validar en la puerta, no adentro del avión—: rechazar lo que no cumple el contrato lo más cerca posible de la entrada, antes de que toque nada, porque un dato malo en la puerta es una molestia y el mismo dato pasado el efecto es un incidente. Viste por qué el esquema del trigger no alcanza —es un letrero que informa, no un portero que revisa— y por qué se necesitan los dos. Separaste las tres capas de la validación: presencia de obligatorios, tipos correctos (donde se atrapa el número disfrazado de texto), y reglas de negocio locales (como amount > 0). Elegiste la herramienta según el caso —nodos nativos para lo simple, un nodo Code para tipos y varias reglas— y confirmaste que validar es justo el trabajo que un nodo Code sí puede hacer en n8n 2.0, porque solo lee los campos del item, sin necesitar HTTP ni archivos ni credenciales. Construiste la validación completa de check-credit, con las tres capas y un If que garantiza que un dato malo tome la rama de fallo sin tocar la lógica. Cuidaste el mensaje de error como parte del contrato —un code estable para la máquina y un message legible para el humano, listando todos los problemas de una vez—. Y pusiste la validación en un solo cuello de botella, justo después del trigger, para que el efecto nunca vea una entrada inválida.
Antes de avanzar a la lección 6 deberías poder: escribir una validación de tres capas para el contrato de un sub-workflow; decidir qué reglas caben en un nodo Code y cuáles necesitan un nodo nativo; y diseñar un mensaje de error con code y message útiles.
Hasta aquí tienes un contrato diseñado, declarado en la frontera y validado. Pero los contratos no son eternos: llega el día en que necesitas cambiar uno —agregar un campo, cambiar un tipo, ajustar la salida— y ese día, si no tienes cuidado, repites la rotura silenciosa de la lección 1 a mayor escala. La lección 6 es sobre cómo cambiar un contrato sin romper a los que ya lo llaman: distinguir un cambio compatible de uno rompiente, hacer convivir dos versiones, y migrar a los llamadores sin caídas. Es lo que le falta al contrato para estar completo: no solo definido y validado, sino capaz de evolucionar.
Recursos
- Code node — n8n Docs — la ficha del nodo Code que usaste para validar, con sus dos modos de ejecución y lo que puedes leer del item.
- Using the Code node — n8n Docs — qué se puede y qué no se puede hacer dentro del nodo Code en n8n 2.0, incluidas las restricciones de HTTP, archivos y módulos.
- If node — n8n Docs — el nodo que enruta el flujo según el resultado de la validación (la rama
ok: truefrente a laok: false). - Filter node — n8n Docs — alternativa nativa para descartar items que no cumplen una condición simple de presencia o valor.
- Data structure — n8n Docs — cómo se representan los tipos en un item, base para entender la comprobación de tipos de la capa 2.