Módulo 3: Contratos entre workflows
6. Versionar un contrato sin romper a quienes lo llaman
Descripción
Al terminar esta lección vas a poder distinguir, ante cualquier cambio que quieras hacerle a un contrato, si es compatible —no rompe a quien ya lo llama— o rompiente —lo rompe—; vas a saber hacer un cambio compatible con seguridad y un cambio rompiente sin caídas, conviviendo dos versiones y migrando a los llamadores de forma gradual; y vas a saber encontrar quién llama a un sub-workflow antes de tocarlo. Es la pieza que le faltaba al contrato para estar completo: no solo definido, declarado y validado, sino capaz de evolucionar sin repetir la rotura silenciosa de la lección 1.
Esto importa porque los contratos no son eternos. El negocio cambia: Cumbre agrega una moneda nueva, check-credit necesita devolver un dato más, un campo resulta que debería ser de otro tipo. El día que necesites cambiar un contrato es el día de mayor peligro de todo el módulo, porque un cambio hecho sin cuidado sobre un contrato con varios llamadores no rompe uno, rompe todos —y en silencio, como viste desde la primera lección—. Versionar es la disciplina que convierte ese día peligroso en un cambio controlado.
Conexión con el módulo: la lección 1 te mostró la rotura silenciosa —alguien renombra approved y order-triage se rompe sin un solo error—. Todo el módulo, hasta aquí, fue construir el contrato para poder protegerlo. Esta lección cierra el ciclo: cómo cambiarlo sin causar esa rotura. Se apoya en la distinción cocina/ventanilla de la lección 2 (solo los cambios a la ventanilla rompen), en el esquema de la lección 3 (contra el cual comparas para saber si un cambio rompe), y en la validación de la lección 5 (que hay que actualizar junto con el contrato). La lección 8 te hace construir, como parte del proyecto, una segunda versión compatible de check-credit aplicando exactamente lo de aquí.
El menú que evoluciona sin romper a los comensales
Volvamos al restaurante de la lección 1, porque su menú es la imagen exacta de cómo un contrato cambia bien o cambia mal.
Un restaurante que lleva años abierto cambia su menú todo el tiempo, y sin embargo sus clientes habituales nunca se quedan sin poder pedir. ¿Cómo? Porque hay dos tipos de cambio de menú, y el restaurante sabe cuál es cuál. Cuando agrega un plato nuevo —un postre que antes no estaba—, ningún cliente se ve afectado: los que pedían lo de siempre lo siguen pidiendo igual, y los que quieran el postre nuevo ahora pueden. Es un cambio que solo suma. Pero cuando renombra un plato —el "café americano" pasa a llamarse "café de la casa"— o cuando quita uno del menú, ahí sí rompe a alguien: el cliente que entra y pide "un americano" se queda sin nada, porque el nombre con el que sabía pedirlo ya no existe. El plato quizás sigue en la cocina, igualito; pero desde el punto de vista del cliente, la promesa con la que contaba desapareció.
Esta es la distinción entera de la lección, y cabe en dos palabras: agregar es seguro, quitar y renombrar rompe. Un cambio que solo suma algo opcional —un plato nuevo, un campo nuevo que nadie está obligado a usar— no afecta a quien ya venía pidiendo lo de siempre. Un cambio que quita, renombra o cambia lo que ya existía le mueve el piso a todo el que dependía de ello. Los primeros se llaman cambios compatibles; los segundos, cambios rompientes. Y toda la disciplina de versionar consiste en saber a cuál de los dos pertenece lo que estás por hacer, y tratar cada uno como merece.
La taxonomía: qué rompe y qué no
Pongamos la distinción en una tabla concreta, porque la intuición "agregar es seguro" tiene matices que conviene ver de frente. Para cada cambio sobre el contrato de check-credit, esto es lo que pasa con un llamador que ya existía y no se enteró del cambio:
| Cambio en el contrato | ¿Rompe a un llamador viejo? | Por qué |
|---|---|---|
| Agregar un campo de entrada opcional con default | No — compatible | El llamador viejo no lo manda, y el default se encarga. Todo sigue igual. |
| Agregar un campo de salida nuevo | No — compatible | El llamador viejo simplemente ignora el campo nuevo; lee los que ya leía. |
| Renombrar un campo (entrada o salida) | Sí — rompiente | El llamador viejo escribe/lee el nombre viejo, que ya no existe. La rotura silenciosa. |
| Quitar un campo | Sí — rompiente | El llamador que dependía de él se queda sin nada. |
| Cambiar el tipo de un campo | Sí — rompiente | El llamador manda/espera el tipo viejo; el nuevo no encaja. |
| Volver obligatorio un campo que era opcional | Sí — rompiente | El llamador viejo, que no lo mandaba, ahora falla la validación. |
| Agregar un campo de entrada obligatorio | Sí — rompiente | El llamador viejo no lo manda, y ahora es requerido: falla. |
Cambiar la forma del sobre de salida (de ok a status) | Sí — rompiente | El llamador lee el discriminador viejo, que ya no está. |
Fíjate en el patrón que atraviesa toda la tabla. Lo compatible tiene una firma común: suma algo que el llamador viejo puede ignorar. Un campo de entrada opcional (lo ignora y el default lo cubre), un campo de salida nuevo (lo ignora y lee los suyos). Lo rompiente tiene la firma opuesta: cambia o quita algo con lo que el llamador viejo contaba. El renombre, el borrado, el cambio de tipo, el apretar una regla —todos le mueven el piso a una expectativa existente—.
De aquí sale una prueba mental rápida que puedes aplicar a cualquier cambio, sin memorizar la tabla: "¿un llamador que no se entera de este cambio sigue funcionando igual?" Si la respuesta es sí, es compatible. Si es no, es rompiente. El llamador que "no se entera" es la clave: un cambio compatible es invisible para quien no lo necesita; uno rompiente se le impone aunque no quiera.
Un matiz importante que la tabla esconde: agregar un campo de entrada obligatorio es rompiente, aunque "agregar" suene a compatible. La palabra "agregar" no basta; lo que decide es si el llamador viejo puede seguir sin cambiar. Un campo de entrada opcional lo puede ignorar; uno obligatorio lo obliga a cambiar. Por eso la forma segura de agregar una entrada nueva es casi siempre agregarla opcional con default, y solo volverla obligatoria más adelante, con una migración —lo cual, como dice la tabla, ya es un cambio rompiente en sí mismo—.
El cambio compatible: cómo agregar sin romper
Empecemos por el caso fácil, porque tiene su propia técnica y conviene hacerlo bien. Digamos que Cumbre quiere que check-credit pueda, opcionalmente, devolver también el historial de crédito del cliente —pero solo cuando el llamador lo pida, porque calcularlo es costoso y la mayoría de los llamadores no lo necesita—.
La forma compatible de hacerlo tiene dos partes:
En la entrada, un campo opcional con default. Agregas include_history como campo de entrada opcional, de tipo boolean, con default false. Un llamador viejo que no manda include_history obtiene el default false —el comportamiento de siempre, sin historial—. Un llamador nuevo que quiere el historial manda include_history: true. Nadie se rompe: el viejo ni se entera, el nuevo tiene lo que necesita.
En la salida, un campo nuevo que solo aparece cuando corresponde. Cuando include_history es true, la respuesta de éxito incluye un campo extra credit_history. Cuando es false, no aparece. Un llamador viejo lee approved y available_credit como siempre, e ignora por completo credit_history —ni sabe que existe—. Un campo de salida nuevo nunca rompe a nadie, porque nadie está obligado a leerlo.
El contrato evolucionado se ve así, con lo nuevo marcado:
ESQUEMA — check-credit (evolucionado, cambio COMPATIBLE)
ENTRADA
customer_id : string obligatorio
order_id : string obligatorio
amount : number obligatorio
currency : string opcional (default: "MXN")
include_history : boolean opcional (default: false) ← NUEVO, opcional
SALIDA EN ÉXITO
{ ok: true, customer_id, approved, available_credit,
credit_history?: [...] } ← NUEVO, solo si include_history=true
Qué esperar. Después de este cambio, order-triage —que llama a check-credit sin mandar include_history y solo lee approved— sigue funcionando exactamente igual, sin que nadie lo toque. No falla, no cambia de comportamiento, ni se entera de que el contrato creció. Ese es el sello de un cambio compatible: los llamadores viejos siguen su vida sin enterarse, y los nuevos tienen una capacidad más. Pudiste evolucionar el contrato sin coordinar con nadie, sin migración, sin riesgo. Así se agrega bien.
Una nota que conecta con la lección 5: cuando agregas include_history, también actualizas la validación para que lo contemple —que si llega, sea booleano; que si no llega, tome el default—. El contrato y su validación cambian juntos, siempre. Un contrato evolucionado con una validación que quedó en la versión anterior es una grieta esperando a abrirse.
El cambio rompiente: cómo hacerlo sin caídas
Ahora el caso difícil, el que de verdad exige disciplina. Digamos que Cumbre descubre que amount debería haber sido siempre un objeto con monto y moneda juntos —{ value: 1842.50, currency: "MXN" }— en vez de un número suelto con la moneda en un campo aparte. Cambiar amount de number a un objeto es, según la tabla, rompiente: todo llamador que manda amount como número dejaría de encajar.
La tentación es hacer el cambio "de una" —modificar check-credit, avisar por chat "ojo, cambié amount", y correr a actualizar a los llamadores antes de que alguien note—. Es exactamente la receta de la rotura silenciosa a mayor escala: entre que cambias el sub-workflow y terminas de actualizar a todos los llamadores, hay una ventana en la que los que todavía no migraste están rotos. Y en un sistema real esa ventana puede durar días.
La forma correcta no toca el contrato viejo. Se llama versiones paralelas, y el ciclo tiene cuatro pasos:
Paso 1 — Crea una versión nueva al lado, sin tocar la vieja. Duplicas check-credit en un sub-workflow nuevo, check-credit-v2, con el contrato nuevo (amount como objeto). El check-credit original —llamémoslo v1 de aquí en adelante— queda intacto, funcionando, con su contrato viejo. En este momento existen las dos versiones: los llamadores viejos siguen llamando a v1 y no se rompió nada; v2 está lista para los que quieran el contrato nuevo.
Paso 2 — Migra a los llamadores, uno por uno, cuando puedas. Vas cambiando cada llamador de v1 a v2 a tu ritmo, probando cada uno de punta a punta al migrarlo. No hay prisa ni ventana de rotura: mientras un llamador no esté listo, sigue usando v1 tranquilo. Migras order-triage hoy, otro llamador la semana que viene. Cada migración es un cambio pequeño y verificado, no un salto colectivo al vacío.
Paso 3 — Marca v1 como obsoleta (deprecada). Cuando ya no quieres que aparezcan llamadores nuevos de v1, la marcas como deprecada —un aviso en su Sticky Note: "DEPRECADA, usar check-credit-v2, se retira el 30 de septiembre"—. Deprecar no es borrar; es avisar que esto va a desaparecer y dar tiempo. v1 sigue funcionando para los que aún no migraron.
Paso 4 — Retira v1 cuando ya nadie la llama. Solo cuando confirmaste que ningún llamador usa v1 —el paso siguiente te dice cómo confirmarlo— la borras. Ahora el sistema quedó limpio, con una sola versión, y en ningún momento hubo una ventana de rotura.
Línea de tiempo del cambio rompiente:
v1 ──────────────────────────────────● (retirada, cuando ya nadie la llama)
/
v2 ●────────────────────────── (creada al lado; los llamadores migran de a uno)
│ │ │
creada order-triage otros
migrado migrados
Qué esperar. Durante toda la migración, el sistema nunca deja de funcionar. En cada momento, cada llamador está apuntando a una versión que le sirve —v1 si aún no migró, v2 si ya—, y ninguno queda apuntando a un contrato que se le cambió debajo de los pies. El cambio rompiente ocurrió, pero se repartió en pasos pequeños y verificados en vez de un salto colectivo. Comparado con el "cámbialo y corre a arreglar", la diferencia no es de estilo: es la diferencia entre un cambio sin caídas y una ventana de horas o días en que parte del sistema está roto en silencio.
Encontrar a los llamadores antes de tocar nada
Todo lo anterior depende de una pregunta que hay que poder responder antes de cambiar cualquier contrato: ¿quién llama a este sub-workflow? Si no sabes quiénes son los llamadores, no puedes saber si un cambio los rompe, ni migrarlos, ni confirmar que ya nadie usa la versión vieja antes de borrarla.
n8n ayuda con esto. Cuando abres un sub-workflow, la interfaz puede mostrarte qué otros workflows lo llaman —la relación entre un Execute Sub-workflow y el sub-workflow que invoca es visible—. Conviene verificar en tu versión cómo se muestra exactamente esa información, porque la interfaz cambia; el punto es que la dependencia no es invisible: n8n sabe quién llama a quién, porque el nodo Execute Sub-workflow nombra explícitamente a su sub-workflow.
Pero no te fíes solo de la herramienta. La disciplina que de verdad sostiene esto es documentar los llamadores en el propio contrato. En el Sticky Note de check-credit, junto al esquema, conviene una lista de "Quién me llama": order-triage, y cualquier otro. Así, quien vaya a cambiar el contrato ve de inmediato a quién tiene que considerar, sin depender de recordar ni de rastrear a mano. Es la misma filosofía de todo el módulo: la información que necesitas para no romper algo vive pegada a ese algo, no en la memoria de quien lo escribió.
La regla operativa: antes de cualquier cambio rompiente, lista a todos los llamadores. Si el cambio es compatible, la lista te tranquiliza (nadie se rompe). Si es rompiente, la lista es tu plan de migración: son exactamente los workflows que tienes que mover de v1 a v2. Y antes de borrar v1, la lista tiene que estar vacía —cero llamadores— o estás a punto de romper a alguien.
Dos recordatorios que ahorran versiones
Antes de cerrar, dos observaciones que evitan versionar de más —porque versionar tiene su costo, y no todo cambio lo necesita—.
El primero: la mayoría de los cambios no toca el contrato. Vuelve a la distinción cocina/ventanilla de la lección 2. Todo lo que este módulo llama "cambio rompiente" es un cambio a la ventanilla —a lo que cruza la frontera—. Pero la mayor parte del trabajo de mantener un sub-workflow es cambiar la cocina: mejorar cómo calcula el crédito, cambiar la fuente de datos de una hoja a una base de datos, optimizar los nodos internos. Nada de eso toca el contrato, así que nada de eso necesita versionarse. Antes de arrancar el ciclo de versiones paralelas, hazte la pregunta de la lección 2: "¿lo que voy a cambiar cruza la frontera?". Si la respuesta es no —si solo cambia cómo trabaja el sub-workflow por dentro—, cámbialo con libertad, sin versión, sin migración, sin avisar a nadie. Versionar es para la ventanilla; la cocina se cambia gratis. Confundir un cambio de cocina con uno de ventanilla te hace montar una migración cara para nada.
El segundo: a veces una sola versión puede aceptar las dos formas. Las versiones paralelas (v1 y v2) son la vía más limpia para un cambio rompiente, pero no la única. Para algunos cambios existe una técnica más ligera: hacer que el sub-workflow, durante la transición, acepte tanto la forma vieja como la nueva. Volvamos al caso de amount que pasa de número a objeto { value, currency }. En lugar de crear check-credit-v2, puedes ajustar la validación de check-credit para que acepte las dos formas: si amount llega como número, lo trata como antes; si llega como objeto, usa la forma nueva. Así, los llamadores viejos siguen mandando un número y no se rompen, y los nuevos ya pueden mandar el objeto —sin crear un segundo workflow—.
// Fragmento de la validación, durante la transición: acepta las dos formas de amount.
let amountValue;
let amountCurrency;
if (typeof input.amount === 'number') {
// Forma vieja: amount es un número, currency viene aparte (o su default).
amountValue = input.amount;
amountCurrency = input.currency ?? 'MXN';
} else if (input.amount && typeof input.amount === 'object') {
// Forma nueva: amount es un objeto { value, currency }.
amountValue = input.amount.value;
amountCurrency = input.amount.currency ?? 'MXN';
} else {
errors.push('amount debe ser un número o un objeto { value, currency }');
}
Esta técnica —tolerar la entrada vieja y la nueva a la vez— convierte un cambio rompiente en uno temporalmente compatible, y te ahorra el segundo workflow. Su costo es que el sub-workflow queda más complejo por dentro mientras dura la transición, con lógica para dos formas; por eso se usa como un puente, no como estado final. Cuando confirmes que ningún llamador manda ya la forma vieja, quitas la rama vieja y el contrato queda limpio en una sola forma. Es la misma meta que las versiones paralelas —migrar sin ventana de rotura— por otro camino: en vez de dos workflows, un workflow tolerante durante la transición. Elige según el caso: versiones paralelas cuando el cambio es grande o la lógica de las dos formas sería enredada; workflow tolerante cuando el cambio es acotado y aceptar las dos formas es sencillo.
Errores comunes
Llamar "pequeño" a un cambio rompiente porque el código cambió poco (conceptual). Qué pasa: alguien renombra un campo de salida de available_credit a remaining_credit —"es un solo campo, un cambio chiquito"— y lo hace directo sobre el contrato en uso; los llamadores que leían available_credit se rompen en silencio. Por qué pasa: el tamaño del cambio se mide por instinto en cuánto código se tocó, y renombrar un campo toca casi nada. Pero el tamaño que importa no es cuánto cambió el sub-workflow, es cuántos llamadores dependían de lo que cambió. Cómo detectarlo: aplica la prueba mental —"¿un llamador que no se entera sigue funcionando?"—; si la respuesta es no, el cambio es rompiente sin importar lo chico que se vea. Cómo corregirlo: trata todo cambio rompiente, por pequeño que parezca en código, con el ciclo de versiones paralelas; el renombre de un solo campo merece el mismo cuidado que rediseñar la salida entera, porque rompe igual.
Cambiar el contrato y actualizar a los llamadores "en caliente" (práctico). Qué pasa: alguien modifica check-credit directamente, y empieza a correr a actualizar order-triage y los demás llamadores uno por uno mientras el sub-workflow ya cambió; durante ese rato, los llamadores no migrados están rotos. Por qué pasa: se siente más rápido hacer un solo cambio y "arreglar lo que salga" que crear una versión paralela; el costo —la ventana de rotura— no se ve hasta que un pedido cae en ella. Cómo detectarlo: si tu plan de cambio incluye una frase como "y luego arreglo rápido a los llamadores", tienes una ventana de rotura. Cómo corregirlo: nunca cambies un contrato en uso en el lugar. Crea la versión nueva al lado (check-credit-v2), migra a los llamadores a tu ritmo, y retira la vieja solo cuando esté sin uso. La versión paralela cuesta un poco más de trabajo y elimina la ventana por completo.
Borrar la versión vieja sin confirmar que nadie la llama (práctico). Qué pasa: alguien migró "a todos" los llamadores a v2, marcó v1 como deprecada, y a la semana la borra por limpieza; resultaba que un workflow olvidado seguía llamando a v1, y ahora falla. Por qué pasa: "creo que ya migré a todos" no es lo mismo que "confirmé que nadie llama a v1"; la memoria falla y siempre hay un llamador olvidado. Cómo detectarlo: antes de borrar, revisa la lista real de llamadores de v1 —en la interfaz de n8n y en el Sticky Note de "quién me llama"—; si no está vacía, o si no puedes confirmarla, no borres. Cómo corregirlo: retira una versión solo cuando su lista de llamadores esté verificablemente vacía. Deprecar da el tiempo para llegar a ese cero; borrar antes de confirmarlo convierte una limpieza en una caída.
Ejercicios
Ejercicio 1 — Compatible o rompiente. Para cada cambio sobre el contrato de check-credit, decide si es compatible o rompiente, aplicando la prueba mental "¿un llamador que no se entera sigue funcionando?":
(a) Agregar un campo de salida checked_at con la fecha de la consulta.
(b) Renombrar el campo de entrada amount a order_amount.
(c) Agregar un campo de entrada opcional notify_on_reject con default false.
(d) Volver obligatorio el campo currency, que era opcional.
(e) Cambiar el discriminador de salida de ok: true/false a status: "success"/"error".
Ver solución
(a) Compatible. Un campo de salida nuevo; el llamador viejo lo ignora y lee los que ya leía. No se entera, sigue igual.
(b) Rompiente. Renombrar un campo de entrada: el llamador viejo sigue mandando amount, que ya no existe con ese nombre; el nuevo order_amount le llega vacío. Falla.
(c) Compatible. Entrada opcional con default; el llamador viejo no la manda, el default la cubre. No se entera.
(d) Rompiente. Volver obligatorio un opcional: el llamador viejo, que no mandaba currency, ahora falla la validación por no mandarlo. Se le impuso una obligación nueva.
(e) Rompiente. Cambiar la forma del sobre: el llamador lee ok para saber si fue éxito, y ok ya no existe; ahora hay status, que no está leyendo. Falla al interpretar toda respuesta.
Por qué funciona: la prueba mental los separa a todos sin memorizar la tabla. (a) y (c) suman algo ignorable —el llamador que no se entera sigue igual—; (b), (d) y (e) cambian algo con lo que el llamador contaba —lo obligan a cambiar aunque no quiera—. Nota que (d) tiene la trampa de "no agregué ni quité, solo cambié una marca": apretar una regla (opcional → obligatorio) es rompiente aunque no toque el nombre ni el tipo.
Ejercicio 2 — Convierte un cambio rompiente en compatible. Cumbre quiere que check-credit reciba, además, el channel por el que llegó el pedido (web, whatsapp, rep_csv), para aplicar reglas de crédito distintas por canal. La primera idea es agregar channel como campo de entrada obligatorio. Eso es rompiente. ¿Cómo lo agregas de forma compatible, y qué costo tiene esa opción?
Ver solución
Lo agregas como campo de entrada opcional con un default sensato, no obligatorio. Por ejemplo, channel : string opcional (default: "web"). Así, un llamador viejo que no manda channel obtiene el default "web" y sigue funcionando; un llamador nuevo puede mandar el canal real. El cambio pasa de rompiente a compatible.
El costo: el default "web" es una suposición. Si un llamador viejo en realidad procesaba pedidos de whatsapp pero no manda el campo, check-credit lo va a tratar como web y aplicarle las reglas equivocadas —silenciosamente correcto en forma, incorrecto en negocio—. Es el precio de la compatibilidad: para no romper a nadie, asumes un valor por los que no lo mandan, y ese valor puede no ser el suyo.
Por eso, cuando el campo nuevo de verdad tiene que ser correcto para cada llamador —cuando ningún default es seguro—, la vía compatible no alcanza y toca hacer un cambio rompiente bien hecho: versión paralela (check-credit-v2 con channel obligatorio) y migrar a cada llamador mandándole su canal real. La regla: agrega opcional con default cuando exista un default seguro; ve a versión paralela cuando no lo haya.
Por qué funciona: el ejercicio muestra que "hazlo compatible" no es gratis ni siempre correcto. Un campo obligatorio se puede volver compatible haciéndolo opcional con default, pero el default introduce una suposición que puede ser falsa para algún llamador. Saber cuándo esa suposición es aceptable (hay un default seguro) y cuándo no (cada llamador necesita su valor real) es la decisión de diseño de fondo detrás de compatible-vs-rompiente.
Ejercicio 3 — Ordena la migración. Tienes que cambiar amount de number a un objeto { value, currency } en check-credit —un cambio rompiente—. check-credit (v1) es llamado hoy por tres workflows: order-triage, bulk-order-import y credit-report. Ordena los pasos de la migración con versiones paralelas, y di en qué momento es seguro borrar v1.
Ver solución
- Crear
check-credit-v2con el contrato nuevo (amountcomo objeto), sin tocarcheck-creditv1. Ahora existen las dos; los tres llamadores siguen en v1, nada se rompió. - Listar y confirmar los llamadores de v1:
order-triage,bulk-order-import,credit-report. Esos tres son el plan de migración. - Migrar los llamadores uno por uno, a ritmo propio, probando cada uno de punta a punta al cambiarlo: primero
order-triage(mandarleamountcomo objeto y verificar que todo el flujo sigue bien), luegobulk-order-import, luegocredit-report. Mientras un workflow no esté migrado, sigue usando v1 sin problema. - Marcar v1 como deprecada cuando los tres estén migrados, con una fecha de retiro, para atrapar cualquier llamador olvidado o nuevo.
- Confirmar que la lista de llamadores de v1 está vacía —cero workflows apuntando a v1—.
- Borrar v1 solo entonces.
Es seguro borrar v1 únicamente en el paso 6: cuando confirmaste (no solo "creo") que ninguno de los tres —ni ningún otro— la sigue llamando. Si borras en el paso 4, confiando en que "ya migré a los tres", te arriesgas a que un llamador olvidado —o uno creado mientras migrabas— caiga.
Por qué funciona: la secuencia garantiza que en ningún momento un llamador quede apuntando a un contrato que se le cambió debajo. La clave es que la creación de v2 (paso 1) y el borrado de v1 (paso 6) están en los extremos, y toda la migración ocurre en medio con las dos versiones vivas. El error clásico —borrar v1 apenas crees que terminaste de migrar— se evita con el paso 5: confirmar el cero, no suponerlo.
Resumen y siguiente paso
En esta lección cerraste el ciclo del contrato: aprendiste a cambiarlo sin repetir la rotura silenciosa de la lección 1. Viste el menú del restaurante que evoluciona sin dejar sin comer a nadie, y destilaste la distinción entera en dos palabras: agregar es seguro, quitar y renombrar rompe. Separaste los cambios compatibles —que suman algo que el llamador viejo puede ignorar, como un campo de entrada opcional con default o un campo de salida nuevo— de los rompientes —que cambian o quitan algo con lo que el llamador contaba, como renombrar, borrar, cambiar un tipo o apretar una regla—, con la prueba mental "¿un llamador que no se entera sigue funcionando?" como criterio único. Hiciste un cambio compatible bien —include_history opcional con default, más un campo de salida que solo aparece cuando se pide— sin coordinar con nadie. Y aprendiste a hacer un cambio rompiente sin caídas con el ciclo de versiones paralelas: crear v2 al lado, migrar a los llamadores uno por uno, deprecar v1, y retirarla solo cuando su lista de llamadores esté verificablemente vacía. Cerraste con la pregunta que todo lo anterior necesita: quién llama a este sub-workflow, respondida con la ayuda de n8n y con la disciplina de documentar los llamadores pegados al contrato.
Antes de avanzar a la lección 7 deberías poder: clasificar cualquier cambio como compatible o rompiente con la prueba mental; hacer un cambio compatible con un campo opcional y default; y ordenar la migración de un cambio rompiente con versiones paralelas, sabiendo cuándo es seguro borrar la versión vieja.
Hasta aquí, el llamador de tus contratos fue siempre otro workflow —order-triage llamando a check-credit—. Pero hay un tipo de llamador nuevo, cada vez más común, que consume tu workflow de otra manera: un AI Agent o un cliente MCP que trata tu workflow como una herramienta. Para ese llamador, el contrato no se ve como campos en un nodo Execute Sub-workflow; se ve como una descripción en lenguaje natural más un esquema de entrada, y un contrato claro es literalmente lo que hace que el agente use la herramienta bien. La lección 7 lleva todo lo que aprendiste sobre contratos a ese terreno.
Recursos
- Sub-workflows — n8n Docs — cómo un workflow llama a otro y cómo se ve la relación entre un Execute Sub-workflow y el sub-workflow que invoca, base para encontrar a los llamadores.
- Execute Sub-workflow Trigger — n8n Docs — el nodo donde vive el esquema que vas a evolucionar; útil para ver cómo se agregan o cambian campos de entrada.
- Sticky notes — n8n Docs — dónde documentar la versión del contrato, la lista de llamadores y los avisos de deprecación pegados al workflow.
- Semantic Versioning — la convención estándar de la industria para nombrar versiones distinguiendo cambios compatibles de rompientes; el mismo criterio de esta lección, formalizado.