Módulo 3: Contratos entre workflows
8. Proyecto: un sub-workflow validado con contrato
Descripción
Al terminar esta lección vas a haber construido check-credit de punta a punta: un sub-workflow con su contrato de entrada y salida documentado, una validación en la frontera que rechaza las entradas inválidas con un mensaje claro, un cuerpo que decide sobre el crédito y devuelve la respuesta con la forma del contrato, y una segunda versión compatible que agrega una capacidad sin romper a nadie. Es el entregable que reúne todo lo del módulo en una sola pieza que funciona y que puedes defender.
Esto importa porque hasta aquí trabajaste cada concepto por separado —el contrato en la lección 2, el esquema en la 3, la frontera en la 4, la validación en la 5, el versionado en la 6, la cara de agente en la 7—. Un sistema real no usa esos conceptos de a uno; los usa todos juntos en el mismo sub-workflow. Este proyecto es donde ves cómo encajan: el esquema que diseñaste se declara en el trigger, la validación protege el efecto, la forma de salida cumple el contrato, y el versionado te deja crecer. Construirlo entero es lo que convierte seis ideas sueltas en una habilidad.
Conexión con el módulo: este es el proyecto que cierra el Módulo 3, y su capacidad de salida es la del módulo entero: definir un contrato, validarlo en la frontera y versionarlo. Reúne el esquema de la lección 3, la frontera de la 4, la validación de la 5 y el versionado de la 6, y lo deja listo para exponerse como herramienta (lección 7) si quisieras. El entregable —el sub-workflow más su contrato escrito— es defendible en portafolio y en entrevista: muestra que sabes construir la promesa entre dos workflows, no solo conectarlos. Después de esto, el Módulo 4 se mete en dónde vive el estado del sistema, que es lo que check-credit todavía finge tener.
Qué vas a construir
El entregable es check-credit: el sub-workflow que order-triage llama para decidir si un cliente tiene crédito suficiente para un pedido. Al terminar tendrás dos cosas juntas —y las dos cuentan como el entregable—:
- El sub-workflow
check-creditfuncionando, con su frontera declarada, su validación de tres capas, su cuerpo de decisión y su salida con la forma del contrato. - El contrato escrito, en un Sticky Note pegado al workflow, con el esquema de entrada y salida, los ejemplos, y la lista de quién lo llama.
La forma final del sub-workflow, en un diagrama, es esta:
check-credit
────────────
[Execute Sub-workflow Trigger] ← la frontera: declara el esquema de entrada
│
[Code: Validate input] ← el portero: valida las tres capas
│
[If: ok?]
├─ true → [Code: Look up credit] → [Edit Fields: success envelope] → salida
└─ false → (el item ya lleva el error del contrato) → salida
[Sticky Note: el contrato escrito] ← pegado al lienzo, documentación para humanos
Vamos a construirlo por partes, cada una con lo que esperar al terminarla. Si tienes una instancia de n8n a mano, síguelo paso a paso; si no, léelo y constrúyelo después. Los nombres exactos de botones y opciones pueden variar según tu versión —verifícalos en tu panel—.
Parte 1 — El contrato escrito, primero
Antes de tocar un nodo, escribe el contrato. Es el orden que enseñó la lección 3: el esquema en papel antes que en n8n. Este contrato es el mismo que diseñaste en la lección 3, y va a ser tu referencia para todo lo demás —lo que declares en el trigger, lo que valides, lo que devuelvas—.
Crea el workflow check-credit, y pon un Sticky Note en el lienzo con esto:
CONTRATO — check-credit (v1)
ENTRADA
customer_id : string obligatorio — de quién se checa el crédito
order_id : string obligatorio — a que pedido pertenece la consulta
amount : number obligatorio — total del pedido a comparar contra el crédito
currency : string opcional — default "MXN"
SALIDA (sobre con discriminador "ok")
ÉXITO → { ok: true, customer_id, approved, available_credit }
FALLO → { ok: false, error: { code, message } }
codes: "INVALID_INPUT", "CUSTOMER_NOT_FOUND"
EFECTOS
ninguno (solo lectura)
QUIÉN ME LLAMA
order-triage
EJEMPLO DE ENTRADA
{ "customer_id": "CUST-118", "order_id": "ORD-2041", "amount": 1842.50 }
EJEMPLO DE SALIDA (éxito)
{ "ok": true, "customer_id": "CUST-118", "approved": true, "available_credit": 3157.50 }
Qué esperar. Todavía no hay nada que ejecutar, y ese es el punto: el contrato existe antes que la implementación. A partir de aquí, cada decisión de construcción se mide contra este texto. Si en algún paso te descubres devolviendo un campo que no está en el contrato, o validando algo que el contrato no pide, el Sticky Note es el árbitro. Escribir el contrato primero no es burocracia; es fijar el blanco antes de disparar.
Parte 2 — La frontera: declarar el esquema en el trigger
El primer nodo de check-credit es el Execute Sub-workflow Trigger (búscalo como "Execute Sub-workflow Trigger" o, en el lienzo, "When Executed by Another Workflow"). Es la recepción del edificio: el único punto de entrada.
Ábrelo, pon el "Input data mode" en "Define using fields below", y declara los cuatro campos de tu contrato:
customer_id : string
order_id : string
amount : number
currency : string
Qué esperar. El trigger queda mostrando esos cuatro campos como la entrada esperada del sub-workflow. Cuando más adelante conectes order-triage, su nodo Execute Sub-workflow va a mostrar estos mismos campos listos para llenar —el contrato ayudando al llamador—. Recuerda lo de la lección 5: declarar estos campos orienta y ayuda, pero no valida a fondo. El portero de verdad viene en la parte siguiente.
Parte 3 — El portero: la validación de tres capas
Conecta, justo después del trigger, un nodo Code llamado "Validate input" en modo Run Once for Each Item. Es la validación de la lección 5, con las tres capas —presencia, tipo y reglas de negocio locales—.
// Nodo: Code — "Validate input"
// Modo: Run Once for Each Item
// Valida el item de entrada contra el contrato de check-credit.
// Solo LEE los campos del item: sin HTTP, sin archivos, respeta n8n 2.0.
const input = $input.item.json;
const errors = [];
// --- Capa 1: obligatorios 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: tipos correctos ---
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í
}
// --- Capa 3: reglas de negocio locales ---
if (typeof input.amount === 'number' && input.amount <= 0) {
errors.push('amount debe ser mayor que cero');
}
// --- Opcional con default ---
const currency = input.currency ?? 'MXN';
if (errors.length > 0) {
// No cumple: forma de FALLO del contrato, y NO pasa el dato malo al efecto.
return {
json: {
ok: false,
error: { code: 'INVALID_INPUT', message: errors.join('; ') },
},
};
}
// Cumple: pasa el item hacia adelante, con el default aplicado y ok: true.
return {
json: {
ok: true,
customer_id: input.customer_id,
order_id: input.order_id,
amount: input.amount,
currency: currency,
},
};
Después del Code, pon un nodo If que pregunte por {{ $json.ok }}: la rama true sigue al cuerpo de decisión (parte 4), y la rama false va directo a la salida —el item ya lleva la forma de fallo del contrato—.
Qué esperar. Con este nodo, cualquier entrada que no cumpla el contrato toma la rama false y sale como error sin tocar la lógica de crédito. Si le pasas { customer_id: "CUST-118", order_id: "ORD-2041", amount: "-50" }, el Code produce { ok: false, error: { code: "INVALID_INPUT", message: "amount debe ser un número; amount debe ser mayor que cero" } } y el If lo manda por la rama de fallo. El portero está haciendo su trabajo.
Parte 4 — El cuerpo: consultar el crédito y decidir
La rama true del If lleva al cuerpo del sub-workflow: la parte que de verdad decide si hay crédito. En un sistema real, este paso consultaría el saldo del cliente en una base de datos con un nodo nativo de base de datos —y ese estado real es justo lo que construye el Módulo 4—. Para este proyecto, y para que sea autocontenido, usamos un nodo Code con una tabla de crédito fija que hace de sustituto de esa consulta. Está marcado como sustituto a propósito: es honesto sobre lo que finge.
// Nodo: Code — "Look up credit and decide"
// Modo: Run Once for Each Item
// SUSTITUTO de una consulta real a la base de clientes. En un sistema real,
// el límite de crédito vendría de un nodo nativo de base de datos (Módulo 4).
// Aqui usamos una tabla fija solo para que el proyecto funcione autocontenido.
const input = $input.item.json; // ya validado: customer_id, order_id, amount, currency
// Tabla de crédito de mentira (stand-in de la base de clientes de Cumbre).
const creditLimits = {
'CUST-118': 5000,
'CUST-204': 12000,
'CUST-330': 800,
};
const limit = creditLimits[input.customer_id];
// Si el cliente no esta en la tabla, es un fallo con su propio code del contrato.
if (limit === undefined) {
return {
json: {
ok: false,
error: {
code: 'CUSTOMER_NOT_FOUND',
message: `El cliente ${input.customer_id} no existe en la base de crédito.`,
},
},
};
}
// La decisión: cuánto crédito queda tras este pedido, y si alcanza.
const availableCredit = limit - input.amount;
const approved = availableCredit >= 0;
return {
json: {
ok: true,
customer_id: input.customer_id,
approved: approved, // true si el crédito alcanza
available_credit: availableCredit, // lo que queda después del pedido
},
};
Fíjate en dos cosas de este nodo. Primero, que devuelve la forma exacta de la salida de éxito del contrato —{ ok: true, customer_id, approved, available_credit }— porque, como enseñó la lección 4, lo que el llamador recibe es lo que produce el último nodo. Segundo, que maneja un fallo propio —CUSTOMER_NOT_FOUND— con la misma forma de error del contrato: no todo fallo es de validación de entrada; un cliente que no existe es un fallo de negocio, y también merece la forma de error prometida.
Conecta este Code a un nodo final que sea la salida del sub-workflow. Como el Code ya produce la forma del contrato, puedes dejarlo como último nodo, o —si prefieres separar el cálculo de la respuesta— pasar por un nodo Edit Fields que arme el sobre final. Lo importante es que el último nodo de esta rama produzca exactamente la forma de éxito del contrato.
Qué esperar. Con una entrada válida de un cliente que existe —{ customer_id: "CUST-118", order_id: "ORD-2041", amount: 1842.50 }—, el sub-workflow devuelve { ok: true, customer_id: "CUST-118", approved: true, available_credit: 3157.50 } (5000 de límite menos 1842.50 del pedido). Con un amount de 6000 para el mismo cliente, devuelve approved: false y available_credit: -1000. Con un customer_id que no está en la tabla, devuelve { ok: false, error: { code: "CUSTOMER_NOT_FOUND", ... } }. Las tres respuestas tienen la forma del contrato.
Parte 5 — Conectar el llamador y probar de punta a punta
Ahora prueba check-credit desde un llamador, como haría order-triage. Crea un workflow de prueba (o usa order-triage si ya lo tienes) con un Manual Trigger y un Edit Fields que arme un pedido de Cumbre, seguido de un nodo Execute Sub-workflow que llame a check-credit en modo "Run once for each item" con "Wait for Sub-Workflow Completion" activo.
Prueba estos cuatro casos, uno por uno:
| Caso | Entrada | Salida esperada |
|---|---|---|
| Válido, con crédito | { customer_id: "CUST-118", order_id: "ORD-2041", amount: 1842.50 } | ok: true, approved: true, available_credit: 3157.50 |
| Válido, sin crédito | { customer_id: "CUST-330", order_id: "ORD-2042", amount: 1000 } | ok: true, approved: false, available_credit: -200 |
| Inválido (amount texto) | { customer_id: "CUST-118", order_id: "ORD-2043", amount: "1000" } | ok: false, code: "INVALID_INPUT" |
| Cliente inexistente | { customer_id: "CUST-999", order_id: "ORD-2044", amount: 500 } | ok: false, code: "CUSTOMER_NOT_FOUND" |
Qué esperar. Los cuatro casos producen la forma del contrato, y cada uno toma su camino: los dos válidos pasan por el cuerpo de decisión y devuelven ok: true con el approved que corresponda; el del amount como texto es rechazado por la validación con INVALID_INPUT sin tocar la lógica; el del cliente inexistente pasa la validación de entrada (los campos están bien formados) pero falla en el cuerpo con CUSTOMER_NOT_FOUND. Fíjate en la diferencia entre los dos fallos: uno lo atrapa el portero de la entrada (capa de validación), el otro lo atrapa el cuerpo (regla de negocio que necesita "consultar el mundo"). Los dos devuelven la forma de error del contrato, pero por razones distintas. Del lado del llamador, las cuatro respuestas se leen igual: primero ok, y según eso, approved o error.
Si algún caso no da lo esperado, las causas típicas: el nodo anterior no se ejecutó (revisa que el Edit Fields corrió y hay datos), el modo del Execute Sub-workflow no es "for each item", o algún campo quedó sin llenar en los inputs. La frontera es exigente a propósito.
Parte 6 — La segunda versión, compatible
Cierra el proyecto haciendo evolucionar el contrato con un cambio compatible, aplicando la lección 6. Cumbre quiere que check-credit pueda, opcionalmente, devolver también el límite de crédito total del cliente —pero solo cuando el llamador lo pida—.
Como es un cambio compatible, no necesitas una versión paralela ni migrar a nadie: lo haces sobre el mismo check-credit.
En la entrada, agrega un campo opcional al trigger y a la validación: include_limit : boolean, opcional, default false. En el Code de validación, añade la lectura del default:
// Nuevo campo opcional, con su default (cambio COMPATIBLE)
const includeLimit = input.include_limit ?? false;
// ...y pasarlo hacia adelante en el return de exito, junto a los demas:
// include_limit: includeLimit,
En la salida, cuando include_limit sea true, agrega un campo credit_limit a la respuesta de éxito. En el Code "Look up credit and decide":
const result = {
ok: true,
customer_id: input.customer_id,
approved: approved,
available_credit: availableCredit,
};
if (input.include_limit === true) {
result.credit_limit = limit; // solo aparece si el llamador lo pidio
}
return { json: result };
Actualiza también el Sticky Note del contrato: marca la versión como v1.1, agrega include_limit a la entrada y credit_limit a la salida (indicando que es opcional), y anota que es un cambio compatible.
Qué esperar. order-triage, que no manda include_limit y solo lee approved, sigue funcionando exactamente igual, sin que lo toques —no falla, no cambia, ni se entera del campo nuevo—. Un llamador que sí quiera el límite manda include_limit: true y recibe el credit_limit extra. Acabas de evolucionar el contrato sin coordinar con nadie ni arriesgar una caída: el sello de un cambio compatible bien hecho. Con esto, tu entregable demuestra las dos mitades del versionado —el criterio para saber que el cambio es compatible, y la ejecución que lo confirma con el llamador viejo intacto—.
El entregable: lista de verificación
Tu proyecto está completo cuando puedes marcar todo esto:
- El contrato está escrito en un Sticky Note pegado a
check-credit: entrada, salida (éxito y fallo), efectos, quién lo llama, y ejemplos. - El Execute Sub-workflow Trigger declara el esquema con "Define using fields below" y los campos del contrato.
- Hay una validación en la frontera (nodo Code, tres capas) inmediatamente después del trigger, antes de cualquier efecto.
- Una entrada inválida es rechazada con la forma de error del contrato (
ok: false,code,message), sin llegar al cuerpo. - El cuerpo produce la salida de éxito con la forma exacta del contrato, y maneja su propio fallo de negocio (
CUSTOMER_NOT_FOUND) con la misma forma de error. - El sub-workflow se prueba de punta a punta desde un llamador, con los cuatro casos (válido con crédito, válido sin crédito, inválido, cliente inexistente).
- Existe una segunda versión compatible (
include_limitopcional) que no rompe al llamador viejo.
Si puedes marcar los siete, construiste la capacidad de salida del módulo entero: un contrato definido, validado en la frontera y versionado. Eso es lo que separa a quien conecta workflows de quien es dueño de la promesa entre ellos.
Cómo defender este entregable
Este proyecto es de los que sirven en un portafolio y en una entrevista técnica, y vale la pena saber cómo mostrarlo, porque lo que lo hace valioso no salta a la vista con solo abrir el lienzo. Un check-credit que corre no impresiona a nadie; lo que impresiona es poder explicar las decisiones detrás de él.
Si tuvieras que presentarlo, estas son las tres cosas que conviene que sepas responder, porque son las que muestran que entiendes el problema y no solo la herramienta:
"¿Qué pasa si el que llama manda un dato malo?" Aquí abres el nodo de validación y muestras las tres capas, y explicas por qué está antes del cuerpo: un dato malo se rechaza en la puerta con un mensaje claro, nunca llega al efecto. Si además puedes decir "y si esto fuera issue-refund en vez de check-credit, esa validación es lo que evita un reembolso sobre datos basura", conectaste el contrato con el efecto, que es el corazón de la guía.
"¿Qué pasa cuando necesitas cambiar el contrato?" Aquí muestras la versión compatible que hiciste en la parte 6, y explicas la distinción compatible-vs-rompiente: por qué agregar include_limit opcional no rompió a order-triage, y qué habrías hecho distinto —versiones paralelas— si el cambio fuera renombrar un campo. Muestra que sabes que un contrato vive para cambiar, y que cambiarlo mal es la rotura silenciosa que este módulo existe para evitar.
"¿Dónde está la parte que finge?" Esta es la que más confianza da, porque muestra honestidad técnica. Señalas el nodo Code del cuerpo y dices: "esta tabla de crédito es un sustituto; en un sistema real este dato vive en una base de datos y esta consulta sería un nodo nativo. Lo dejé marcado como sustituto a propósito, y sé exactamente qué habría que cambiar para hacerlo real". Saber qué finge tu propio sistema, y decirlo sin que te pregunten, es lo que distingue a quien entiende lo que construyó de quien solo lo copió.
La regla de fondo: el entregable no es el lienzo, es el razonamiento que puedes defender sobre él. Un check-credit con un contrato escrito, una validación que sabes explicar y una versión compatible que sabes justificar dice más de ti que diez workflows que corren pero que no puedes sustentar.
Errores comunes
Poner el cuerpo antes de la validación (práctico). Qué pasa: al construir de arriba hacia abajo, alguien conecta el trigger directo al Code de "Look up credit" y agrega la validación "después", terminando con el orden invertido —el cuerpo corre antes de que nada rechace una entrada mala—. Por qué pasa: es natural construir primero la lógica principal, que es la interesante, y dejar la validación para el final; pero "al final de la construcción" no debe significar "al final del flujo". Cómo detectarlo: mira el orden de los nodos; si el Code de decisión puede ejecutarse con una entrada que la validación no revisó antes, están al revés. Cómo corregirlo: el nodo de validación va inmediatamente después del trigger, y el cuerpo va después del If, en la rama true. El portero antes de la puerta interior, siempre.
Que el último nodo no produzca la forma del contrato (práctico). Qué pasa: el cuerpo calcula bien el crédito pero deja campos internos en la salida —o le falta el ok, o devuelve available en vez de available_credit—, y el llamador recibe algo que no puede leer como esperaba. Por qué pasa: al concentrarse en el cálculo, es fácil olvidar que la forma de lo que devuelve el último nodo es lo que el contrato promete, no solo el valor correcto. Cómo detectarlo: compara la salida real del sub-workflow, campo por campo, contra la salida de éxito del contrato del Sticky Note; cualquier campo de más, de menos o con otro nombre es el problema. Cómo corregirlo: asegúrate de que el último nodo de cada rama produzca exactamente la forma del contrato —ni un campo interno de más, ni el ok de menos—. Es lo que la lección 4 marcó: el llamador recibe lo que produce el último nodo, tenga o no la forma prometida.
Hacer el cambio de la parte 6 como rompiente sin darse cuenta (conceptual). Qué pasa: al agregar include_limit, alguien lo declara obligatorio en vez de opcional, o hace que credit_limit aparezca siempre y con un nombre que pisa otro; el llamador viejo, que no manda include_limit, empieza a fallar la validación, o el cambio de salida lo confunde. Por qué pasa: "agregar un campo" suena compatible por definición, y se olvida que agregar una entrada obligatoria es rompiente (lección 6). Cómo detectarlo: aplica la prueba mental —"¿order-triage, que no se entera del cambio, sigue funcionando igual?"—; si no, lo que creíste compatible es rompiente. Cómo corregirlo: el campo de entrada nuevo va opcional con default, y el de salida nuevo solo aparece cuando se pide; así el llamador viejo ni se entera. Si necesitaras un cambio que sí es rompiente, toca el ciclo de versiones paralelas, no un cambio en caliente.
Ejercicios
Ejercicio 1 — Agrega un code de negocio nuevo. El contrato de check-credit tiene los codes INVALID_INPUT y CUSTOMER_NOT_FOUND. Cumbre quiere que, si un cliente tiene el crédito congelado (por mora), check-credit no lo apruebe y devuelva un fallo distinto de "sin crédito". Diseña el cambio: ¿qué code nuevo agregas, dónde va la comprobación (validación de entrada o cuerpo), y es un cambio compatible o rompiente?
Ver solución
El code nuevo es algo como CREDIT_FROZEN. La comprobación va en el cuerpo, no en la validación de entrada: que el crédito esté congelado es una regla de negocio que depende del estado del cliente ("consultar el mundo"), no de la forma de la entrada —el customer_id llegó perfectamente bien formado—. En el Code "Look up credit and decide", después de encontrar al cliente, se revisa si está congelado (en el sustituto, otra tabla o campo de la tabla de crédito) y, si lo está, se devuelve { ok: false, error: { code: "CREDIT_FROZEN", message: "..." } }.
Es un cambio compatible: agregar un code de fallo nuevo no rompe a order-triage, porque order-triage ya sabía que un fallo llega con la forma { ok: false, error: { code, message } }. Un llamador que maneja el sobre de error genérico —"si ok es false, es un fallo"— maneja el code nuevo sin cambios. Solo un llamador que tuviera lógica específica por cada code necesitaría enterarse, y aun así, no se rompe: simplemente no tiene una rama especial para CREDIT_FROZEN hasta que se la agregues.
Por qué funciona: el ejercicio distingue dos cosas. Primero, dónde va cada comprobación: forma de la entrada → validación; estado del negocio → cuerpo. Segundo, que agregar un valor nuevo a un conjunto que el llamador ya trataba de forma genérica (los codes de error) es compatible, mientras que cambiar la forma del sobre de error sería rompiente. Diseñar el sobre de error genérico desde el principio (lección 3) es lo que hace que agregar codes nuevos sea barato.
Ejercicio 2 — Prueba adversarial de la validación. Escribe tres entradas "malas" que intenten colarse por la validación de check-credit, y di qué debería pasar con cada una. Piensa como alguien que quiere romper el portero, no como quien lo respeta.
Ver solución
Tres ejemplos (hay más):
-
{ customer_id: "CUST-118", order_id: "ORD-1", amount: 0 }—amountde cero. Debería ser rechazada por la capa 3 (amount debe ser mayor que cero). Es la trampa de la lección 5: un!input.amountingenuo la dejaría pasar o la confundiría con "ausente", por eso validamos con<= 0explícito. -
{ customer_id: "CUST-118", order_id: "ORD-2", amount: 1000, currency: 500 }—currencycon un número en vez de texto. Con la validación tal como está en el proyecto, esto pasa, porque no comprobamos el tipo decurrency. Es un hueco real: sicurrencyimportara para el cálculo, habría que agregarif (input.currency !== undefined && typeof input.currency !== 'string'). Descubrir este hueco es el punto del ejercicio. -
{ customer_id: " ", order_id: "ORD-3", amount: 1000 }—customer_idcon solo espacios. Con la validación actual," "no es cadena vacía, así que pasa la capa 1 y llega al cuerpo, dondecreditLimits[" "]esundefinedy produceCUSTOMER_NOT_FOUND. Funciona, pero por accidente: si quisieras rechazarlo en la entrada con un mensaje más claro, agregarías un.trim()en la comprobación de presencia.
Por qué funciona: pensar adversarialmente encuentra los huecos que las pruebas "amables" nunca tocan. Una validación no es "buena" porque acepte las entradas buenas; es buena porque rechaza las malas, incluidas las que no imaginaste al escribirla. Los casos 2 y 3 muestran que la validación del proyecto es correcta pero no exhaustiva —y saber dónde están sus límites es parte de ser dueño del contrato, no solo de escribirlo—.
Ejercicio 3 — Documenta un segundo llamador. Otro workflow de Cumbre, bulk-order-import, empieza a llamar a check-credit. Actualiza la sección "QUIÉN ME LLAMA" del contrato y explica por qué, a partir de ahora, cualquier cambio rompiente a check-credit es más caro que antes.
Ver solución
La sección queda:
QUIÉN ME LLAMA
order-triage
bulk-order-import
Cualquier cambio rompiente es más caro ahora porque hay dos llamadores que migrar en vez de uno. Con un solo llamador, un cambio rompiente mal hecho rompe un workflow; con dos, rompe dos. Y el ciclo de versiones paralelas de la lección 6 —crear check-credit-v2, migrar a cada llamador, confirmar que la lista de v1 está vacía antes de borrarla— ahora tiene dos workflows en la lista de migración, dos pruebas de punta a punta, y dos oportunidades de olvidar a alguien. El costo de un cambio rompiente crece con el número de llamadores; por eso el contrato estable (lección 2) y el cambio compatible cuando se puede (lección 6) valen cada vez más a medida que un sub-workflow gana consumidores.
Por qué funciona: documentar el segundo llamador no es un trámite; es actualizar el mapa de a quién puedes romper. La lista "quién me llama" es la que convierte un cambio rompiente de una apuesta a ciegas en un plan de migración concreto. Cada llamador que agregas a esa lista sube el valor de mantener el contrato estable —y baja las ganas de renombrar un campo "porque suena mejor"—.
Resumen y siguiente paso
En esta lección construiste check-credit de punta a punta y reuniste todo el módulo en un solo entregable. Escribiste el contrato primero, en un Sticky Note, fijando el blanco antes de disparar. Declaraste el esquema en el Execute Sub-workflow Trigger con "Define using fields below". Pusiste el portero —la validación de tres capas— inmediatamente después del trigger, garantizando que ninguna entrada inválida llegue al cuerpo. Construiste el cuerpo que consulta el crédito (con un sustituto honesto de la base de datos real que llega en el Módulo 4) y devuelve la forma exacta del contrato, manejando tanto el éxito como el fallo de negocio CUSTOMER_NOT_FOUND. Probaste el sub-workflow de punta a punta desde un llamador, con los cuatro casos, viendo cómo cada uno toma su camino y todos devuelven la forma del contrato. Y cerraste con una segunda versión compatible —include_limit opcional— que evolucionó el contrato sin tocar al llamador viejo. El entregable, el sub-workflow más su contrato escrito, es la capacidad de salida del módulo entero, y es defendible en portafolio.
Antes de cerrar el módulo deberías poder: construir un sub-workflow con contrato documentado, validación en la frontera y salida con la forma prometida; probarlo con casos válidos e inválidos; y hacerle un cambio compatible sin romper a los llamadores.
Hay una pieza que este proyecto fingió tener, y la nombramos con honestidad: check-credit "consultó" el crédito de una tabla fija dentro de un nodo Code, un sustituto de la base de clientes real. En un sistema de verdad, ese crédito vive en algún lado —en una base de datos, en un registro que sobrevive entre ejecuciones—, y saber dónde vive la verdad del sistema, cómo diseñar ese estado y cómo deduplicar contra él es el tema del Módulo 4. Pasaste de "un solo workflow que no se duplica" (Módulo 2) a "dos workflows que se entienden por contrato" (este módulo); el Módulo 4 te da el tercer pilar: dónde vive el estado que los dos comparten.
Recursos
- Execute Sub-workflow Trigger — n8n Docs — el nodo con el que declaraste la frontera de
check-credity su esquema de entrada. - Execute Sub-workflow — n8n Docs — el nodo con el que el llamador invoca a
check-credit, con el modo "Run once for each item" que usaste al probar. - Code node — n8n Docs — la ficha del nodo Code que usaste para la validación y el cuerpo, con sus dos modos de ejecución.
- Sticky notes — n8n Docs — dónde vive el contrato escrito, pegado al workflow, con el esquema, los ejemplos y la lista de llamadores.
- Sub-workflows — n8n Docs — el panorama de cómo un workflow llama a otro, el marco de todo lo que construiste en este proyecto.