Módulo 2: Idempotencia: que repetir no duplique

8. Proyecto: volver idempotente un paso que crea registros

Descripción

Al terminar esta lección vas a tener un entregable concreto: un flujo de Cumbre que antes duplicaba —insertaba un registro y llamaba a una API en cada reintento— convertido en idempotente, y probado re-ejecutándolo dos veces para demostrar que deja un solo registro y un solo efecto. No es una lección de conceptos nuevos: es donde juntas los seis anteriores y produces algo que puedes mostrar. Vas a asignar la clave de idempotencia, convertir la inserción en un upsert, blindar la llamada a la API con su cabecera, evitar la trampa de verificar-y-actuar, y —lo más importante— vas a construir la prueba que separa "creo que es idempotente" de "demostré que es idempotente".

Esto importa porque la diferencia entre alguien que "sabe de idempotencia" y alguien que un equipo contrata es exactamente esta: la segunda persona no promete que su workflow no duplica, lo demuestra. Un entregable que muestra el flujo, la clave elegida, y una prueba de dos ejecuciones con un solo efecto, es defendible en una entrevista y en un portafolio. Es el "workflow builder vs automation system owner" del que habla toda la guía, convertido en algo que se ve y se toca.

Conexión con el módulo: esta lección cierra el módulo 2 juntando todo. La clave viene de la lección 3, el upsert de la 4, la cabecera de la 5, el cuidado con verificar-y-actuar de la 6, y la idempotencia de herramientas de agente de la 7. No hay concepto nuevo; hay integración y prueba. Al terminar, tendrás la "operación segura" que la Fase 1 de la guía prometía, lista para que el módulo 3 le agregue contratos y el módulo 4 le agregue el estado persistente.

El criterio de terminado: la prueba de las dos ejecuciones

Antes de tocar nada, definamos qué significa "terminado", porque un proyecto sin criterio de aceptación es un proyecto que nunca se sabe si funciona.

Piénsalo como la inspección de seguridad de un ascensor. No basta con que el instalador diga "quedó bien". Un inspector aprieta el botón cinco veces y verifica que el ascensor no se vuelve loco; abre las puertas con el ascensor en movimiento y verifica que se detiene; sobrecarga la cabina y verifica que no arranca. La certificación no es la palabra del instalador; es el resultado de pruebas concretas. Tu proyecto necesita su propia certificación.

El criterio de terminado de este proyecto es una sola frase, y es la prueba de idempotencia que venimos anticipando desde la lección 1:

Ejecuto el flujo dos veces con el mismo pedido ORD-2041, y al final hay exactamente un registro en la base de datos y exactamente un efecto en la API. Las dos ejecuciones terminan sin error.

Fíjate en las tres partes, porque cada una descarta un fallo distinto:

"Exactamente un registro" descarta el INSERT que duplica (lección 4). Si hay dos filas, el upsert falló o falta la restricción de unicidad.

"Exactamente un efecto" descarta el POST que duplica (lección 5). Si hay dos cobros, la cabecera de idempotencia falla o la clave no es estable.

"Sin error" descarta que hayas "resuelto" el duplicado rompiendo el flujo. Un workflow que falla en la segunda ejecución no es idempotente; es un workflow roto que casualmente no duplica. La segunda ejecución debe correr completa y limpia, absorbiendo la repetición en silencio.

Grábate este criterio, porque todo lo que sigue existe para cumplirlo, y al final vas a verificarlo paso a paso.

Fase 0: el flujo frágil que vas a arreglar

Este es el punto de partida —el order-triage frágil que abrió el módulo, en su versión más simple y sin agente para empezar; el agente lo agregamos como extensión al final—:

Webhook          Postgres (INSERT)          HTTP Request (POST)
recibe      ──►  INSERT INTO orders    ──►  POST /charges
ORD-2041         (a ciegas)                 (crea un cobro nuevo)

Dos efectos que acumulan, los dos ya diagnosticados en la lección 2: el INSERT a ciegas mete una fila nueva por cada reintento, y el POST /charges crea un cobro nuevo por cada reintento. Si disparas este flujo dos veces con ORD-2041, terminas con dos filas y dos cobros. Falla el criterio de terminado en sus tres partes.

Antes de arreglarlo, hazle la prueba de las dos ejecuciones en su estado frágil, para tener una línea base con la que comparar. Qué esperar: dos filas en orders con order_id = 'ORD-2041', y dos cobros en la pasarela. Ese es el problema, medido. Ahora vamos a hacerlo desaparecer, fase por fase.

Fase 1: asignar la clave de idempotencia

Lo primero que necesita cualquier reparación es un nombre estable para el evento. Insertas un nodo Code justo después del Webhook, antes de cualquier efecto.

Webhook  ──►  Code               ──►  Postgres (INSERT)  ──►  HTTP Request (POST)
              (calcula                 (todavía a ciegas)      (todavía duplica)
               idempotency_key)

Como los pedidos web de Cumbre traen un order_id estable que la tienda conserva en los reintentos, usamos la clave natural: el propio order_id. Aun así, guardamos el valor en un campo idempotency_key explícito, para que los nodos siguientes lo lean de un solo lugar, sin importar el canal:

// Nodo: Code — "Assign idempotency_key"
// Modo: Run Once for Each Item
// Objetivo: dejar la clave estable lista en un campo, temprano, para todos los nodos siguientes.

const order = $input.item.json;

// Los pedidos web traen order_id estable: es la clave natural, la mejor opción.
// (Para canales sin order_id, aquí calcularías un hash con crypto — lección 3.)
const idempotencyKey = order.order_id;   // 'ORD-2041'

return {
  json: {
    ...order,                        // conservo todo el pedido
    idempotency_key: idempotencyKey, // y agrego la clave, lista para los efectos
  },
};

Qué esperar: el panel OUTPUT muestra el pedido con un campo nuevo, idempotency_key, con el valor "ORD-2041". Si ejecutas el nodo dos veces con el mismo pedido, la clave es idéntica las dos veces —es la propiedad que todo el resto va a explotar—.

Un recordatorio de la lección 3, porque aquí es donde se arruina todo si te descuidas: no metas nada temporal ni aleatorio en esta clave. Nada de new Date(), nada de randomUUID(). La clave tiene que ser la misma cuando el webhook llegue por segunda vez. Con order_id lo es gratis; si hubieras tenido que calcular un hash, sería de los campos estables del pedido, sin la hora de llegada.

Fase 2: convertir el INSERT en upsert

Ahora arreglamos el primer efecto: la escritura al CRM. Recuerda la lección 4 —el upsert necesita, primero, una restricción de unicidad sobre la columna clave—.

Paso 2a — La restricción de unicidad (una sola vez). En tu base de datos Postgres, garantiza que orders no permita dos filas con el mismo idempotency_key:

-- Una sola vez, al preparar la tabla.
ALTER TABLE orders ADD CONSTRAINT orders_idem_unique UNIQUE (idempotency_key);

Qué esperar: si la tabla no tenía la restricción, se crea sin ruido. Si al correrla te avisa que ya hay valores duplicados, es porque tu prueba de línea base de la Fase 0 dejó dos ORD-2041; borra uno y vuelve a intentar. (Ese aviso es, en sí, una confirmación de que el problema era real.)

Paso 2b — El upsert. Cambias el nodo Postgres de un INSERT a ciegas a un upsert con ON CONFLICT. Puedes usar la operación de upsert del nodo o, para control total, la operación de ejecutar consulta:

-- Nodo Postgres, operación "Execute Query".
-- Los valores vienen del item, pasados como parámetros.
INSERT INTO orders (idempotency_key, order_id, customer_id, amount, status)
VALUES ($1, $2, $3, $4, 'pending')
ON CONFLICT (idempotency_key) DO NOTHING;

Los parámetros $1..$4 se llenan con idempotency_key, order_id, customer_id y amount del item.

Qué esperar al probarlo ahora: dispara el flujo una vez → una fila en orders. Dispara otra vez el mismo ORD-2041 → el nodo corre sin error y sigue habiendo una sola fila. La segunda inserción chocó con la restricción de unicidad y el DO NOTHING la absorbió. El primer efecto ya es idempotente. (El POST /charges, en cambio, todavía duplica; eso es la Fase 3.)

Fase 3: hacer idempotente la llamada a la API

Ahora el segundo efecto: el cobro. Asumimos, como en la lección 5, que la pasarela de Cumbre soporta la cabecera Idempotency-Key —y que lo confirmaste en su documentación, junto con el nombre exacto y la ventana de retención—.

En el nodo HTTP Request que hace el POST /charges, activas el envío de cabeceras y agregas:

  • Nombre: Idempotency-Key
  • Valor: {{ $json.idempotency_key }}

Esa expresión toma la clave que la Fase 1 dejó en el item —ORD-2041— y la manda en la cabecera. La pasarela, la primera vez, crea el cobro y guarda "la clave ORD-2041 → este cobro"; la segunda vez, con la misma clave, devuelve el mismo cobro sin crear otro.

Qué esperar al probarlo ahora: dispara el flujo una vez → un cobro en la pasarela. Dispara otra vez el mismo ORD-2041 → la llamada devuelve éxito (no error) y sigue habiendo un solo cobro, el mismo de la primera vez. El segundo efecto ya es idempotente.

Recuerda el diagnóstico de fallos de la lección 5, por si ves dos cobros: casi siempre es que la cabecera no se está enviando (revisa el interruptor y el nombre) o que la clave no es estable (imposible aquí, porque es order_id, pero revísalo si usaste un hash). Y verifica en la documentación de la pasarela el nombre exacto de la cabecera; no todas la llaman igual.

Fase 4: verificar que no quedó ningún "verificar y luego actuar"

Antes de la prueba final, una revisión de la lección 6. Recorre tu flujo y confirma que no hay ningún patrón de "primero un nodo que consulta si existe, luego un If, luego un nodo que crea". Si en algún momento, para "asegurarte", agregaste un SELECT que verifica antes del upsert, quítalo: el upsert ya hace la verificación de forma atómica, y anteponerle un SELECT no agrega seguridad, agrega una ventana de carrera y ruido.

El flujo terminado no tiene verificaciones manuales de existencia. Tiene un upsert atómico y una cabecera de idempotencia, que delegan la unicidad a algo que sabe ser atómico —la base de datos y la pasarela—. Así se ve:

Webhook  ──►  Code               ──►  Postgres (UPSERT)        ──►  HTTP Request (POST)
              (idempotency_key)        INSERT ... ON CONFLICT        con cabecera
                                       DO NOTHING                    Idempotency-Key

Compáralo con el flujo frágil de la Fase 0. Se agregó un nodo (Code) y se cambiaron dos configuraciones (el INSERT a upsert, la cabecera al HTTP Request). Ningún nodo de verificación, ningún If de existencia. Menos complejidad de la que tendría la solución ingenua, y correcta bajo concurrencia.

Fase 5: la prueba que lo demuestra (el entregable)

Esta es la fase que convierte tu trabajo en un entregable defendible. No basta con que funcione; tienes que demostrar que funciona, y guardar la evidencia.

El procedimiento de prueba, paso a paso:

Paso 1 — Estado inicial limpio. Asegúrate de que no haya rastros de ORD-2041 de pruebas anteriores. Consulta y, si hace falta, limpia:

SELECT * FROM orders WHERE order_id = 'ORD-2041';   -- debe devolver 0 filas antes de empezar

Y revisa que no haya cobros previos de ORD-2041 en la pasarela.

Paso 2 — Primera ejecución. Dispara el flujo con ORD-2041. Qué esperar: una fila en orders, un cobro en la pasarela, la ejecución termina sin error.

Paso 3 — Segunda ejecución (la que importa). Dispara el flujo otra vez con el mismísimo ORD-2041, simulando el reintento del webhook. Qué esperar: la ejecución termina sin error, y —el momento de la verdad— al volver a consultar:

SELECT COUNT(*) FROM orders WHERE order_id = 'ORD-2041';   -- debe devolver 1

una sola fila, y en la pasarela un solo cobro. La segunda ejecución corrió completa, absorbió la repetición, y no dejó ningún efecto nuevo.

Paso 4 — Guardar la evidencia. Este paso es el que distingue un proyecto de un entregable. Captura:

  • El diagrama del flujo antes (frágil) y después (idempotente).
  • El nodo Code con la clave elegida y la justificación de por qué es estable.
  • El SELECT COUNT(*) devolviendo 1 después de dos ejecuciones.
  • La captura de la pasarela mostrando un solo cobro.

Con eso tienes la demostración, no la promesa. Si además tu instancia es n8n 2.0, puedes usar el motor de replay para volver a ejecutar una de las dos corridas y mostrar que ni así se duplica —pero eso es material del módulo 6; por ahora, la prueba de las dos ejecuciones basta y sobra—.

Definición de terminado, verificada: dos ejecuciones, una fila, un cobro, cero errores. Si tu prueba muestra eso, el proyecto está completo. Si muestra dos filas o dos cobros, vuelve a la fase correspondiente —dos filas es Fase 2 (upsert o restricción), dos cobros es Fase 3 (cabecera o clave)—.

Qué NO cubre este proyecto (y por qué está bien)

Parte de entregar con honestidad es conocer los límites de lo que construiste. Tu flujo es idempotente para el caso que este módulo promete —repetir el mismo evento no duplica el efecto— pero hay cosas que deliberadamente no resuelve, y confundirlas sería sobrevender tu trabajo.

No deduplica entre ejecuciones separadas por mucho tiempo si dependes solo de la cabecera. Recuerda de la lección 5 que la pasarela olvida la clave pasadas unas horas. Tu prueba de dos ejecuciones seguidas está cubierta de sobra; pero si el "mismo" evento se reprocesara días después —un replay manual de una ejecución vieja—, la cabecera ya no lo recordaría. La defensa duradera contra eso es un registro tuyo que no expira, y ese registro —el ledger de deduplicación— es el módulo 4. El upsert sobre tu base de datos sí es duradero (la fila no expira), así que el registro está protegido para siempre; el cobro, solo dentro de la ventana de la API. Saber esa asimetría es parte de dominar el tema.

No coordina varios efectos que dependen entre sí. Tu flujo hace dos efectos independientes (registro y cobro). Si tuvieras tres efectos donde el segundo depende del éxito del primero, y quisieras que un fallo a mitad no dejara el sistema a medias, eso es coordinación —el patrón outbox del módulo 5—. Aquí cada efecto es idempotente por su cuenta, que es la base necesaria, pero no es coordinación.

No reintenta ni alerta cuando algo falla de verdad. Tu criterio incluye "sin error", pero no construiste qué pasa cuando la pasarela está caída, o cuando el error no es un duplicado esperado sino un fallo real. Reintentos seguros, acciones compensatorias y alertas son el módulo 6.

Ninguna de estas ausencias es un defecto de tu proyecto; son los siguientes módulos. Tu entregable cumple exactamente lo que promete —una operación segura de repetir— y esa es la pieza fundamental sobre la que todo lo demás se construye. Un buen ingeniero entrega la pieza completa y nombra con precisión dónde termina; eso es más valioso que fingir que resolvió todo.

Cómo presentarlo en una entrevista o portafolio

Un entregable no defendido es la mitad de su valor. Así se cuenta esta historia en dos minutos, que es el tiempo que tienes en una entrevista:

Empieza por el problema, no por la solución. "Este workflow de una distribuidora procesa pedidos por webhook. El webhook a veces se dispara dos veces —reintento del proveedor, doble clic, reintento del motor— y la versión original creaba un segundo cobro y un segundo registro cada vez. Un cliente recibía el cargo duplicado." El problema, contado así, cualquiera entiende por qué importa.

Nombra la propiedad, no solo el arreglo. "Lo volví idempotente: repetir la operación deja el mismo resultado que hacerla una vez." La palabra "idempotente" en una entrevista técnica te ubica de inmediato del lado de quien entiende sistemas.

Muestra las dos decisiones clave. Primero, la clave: "usé el order_id como clave de idempotencia porque el proveedor lo conserva en los reintentos; si no lo conservara, habría hasheado el contenido estable". Segundo, dónde vive la unicidad: "la hace cumplir la base de datos con una restricción de unicidad y un upsert atómico, y la pasarela con su cabecera Idempotency-Key —no un nodo que verifica y luego crea, que tendría una condición de carrera bajo concurrencia—". Esa última frase, sobre no caer en verificar-y-actuar, es la que demuestra profundidad.

Cierra con la prueba. "Y no lo afirmo, lo demuestro: aquí está el flujo ejecutado dos veces con el mismo pedido, y el COUNT da uno." La evidencia es lo que te separa de quien solo leyó sobre idempotencia.

Esa es la conversación de un automation system owner, no la de un workflow builder. Es, literalmente, el objetivo de la guía convertido en dos minutos que puedes ensayar.

Un consejo sobre cómo guardar el entregable para que siga siendo útil dentro de seis meses: acompaña las capturas con una nota corta que responda tres preguntas —qué clave elegiste y por qué es estable, dónde vive la garantía de unicidad (la restricción de la base de datos y la cabecera de la API), y cómo se prueba (las dos ejecuciones)—. Esa nota es tu propio "manual del sistema", y es lo que te va a permitir retomar el proyecto, explicárselo a un compañero, o defenderlo en una entrevista sin tener que reconstruir el razonamiento desde cero. Un entregable con su nota de diseño vale mucho más que uno sin ella, y escribirla toma cinco minutos que se pagan solos.

Extensión con agente: el flujo completo de order-triage

Si quieres el reto completo, agrega el AI Agent que el caso de estudio tiene de verdad, aplicando la lección 7:

Webhook  ──►  Code               ──►  AI Agent  ──►  (herramientas idempotentes)
              (idempotency_key)        clasifica       charge_customer (cabecera)
                                       y decide        upsert_order (ON CONFLICT)

La regla de la lección 7 manda: la idempotency_key la calcula el nodo Code antes del agente, derivada del pedido, y las herramientas (charge_customer, upsert_order) la usan sin recalcularla ni derivarla de la clasificación del agente. Así, aunque el agente invoque charge_customer dos veces o clasifique distinto entre corridas, el cobro es uno. La prueba de terminado es la misma —dos ejecuciones, una fila, un cobro— más una comprobación extra: que una doble invocación del agente dentro de una sola ejecución tampoco duplique.

Errores comunes

Declarar "terminado" sin la prueba de las dos ejecuciones (conceptual). Qué pasa: se configura el upsert y la cabecera, se dispara el flujo una vez, funciona, y se da por resuelto. Nunca se disparó dos veces, así que nunca se probó la idempotencia —solo se probó que corre—. Por qué pasa: la idempotencia es invisible en una sola ejecución; solo se manifiesta en la segunda. Cómo detectarlo: si tu evidencia es "lo ejecuté y funcionó", no probaste idempotencia. La prueba es la segunda ejecución. Cómo corregirlo: haz siempre la prueba de las dos ejecuciones y verifica el COUNT. "Corrió sin error" no es el criterio; "corrí dos veces y hay exactamente un efecto" sí lo es. Sin esa prueba, no tienes un entregable, tienes una esperanza.

Olvidar la restricción de unicidad y creer que el upsert falló (práctico). Qué pasa: se configura el upsert, se prueba, y aparecen dos filas; se concluye que "el upsert no sirve" y se busca otra solución. Por qué pasa: falta la restricción de unicidad sobre idempotency_key, sin la cual el upsert no tiene contra qué chocar y se degrada a un insert. Es la causa número uno de "mi upsert no protege", de la lección 4. Cómo detectarlo: consulta las restricciones de la tabla; si no hay un UNIQUE sobre la columna clave, ese es el problema, no el upsert. Cómo corregirlo: crea la restricción (ALTER TABLE ... ADD CONSTRAINT ... UNIQUE (idempotency_key)); si falla por duplicados existentes, límpialos primero. El paso cero del upsert no se salta.

Probar solo secuencial y asumir que cubre la concurrencia (conceptual). Qué pasa: la prueba de las dos ejecuciones se hace disparando una, esperando a que termine, y disparando la otra. Pasa. Pero si el flujo tuviera un verificar-y-actuar escondido, esa prueba secuencial no lo detectaría —el fallo solo aparece con ejecuciones solapadas—. Por qué pasa: la prueba secuencial es la fácil de hacer a mano, y cubre el reintento típico, pero no la concurrencia. Cómo detectarlo: revisa el diseño (Fase 4) además de la prueba; si no hay ningún SELECT+If+INSERT, la prueba secuencial es suficiente porque el upsert y la cabecera son atómicos por diseño. Si sí lo hay, ninguna prueba secuencial te salva. Cómo corregirlo: elimina cualquier verificar-y-actuar (Fase 4) para que la correctitud no dependa de reproducir la concurrencia en una prueba —el upsert atómico te da la garantía por construcción, no por suerte en el timing—.

Ejercicios

Ejercicio 1 — Escribe tu criterio de aceptación. Para el flujo que construiste, redacta el criterio de terminado en una frase, y luego lista los tres SELECT o comprobaciones concretas que ejecutarías para verificarlo. No mires el criterio de esta lección hasta terminar; después compara.

Ver solución

Un criterio bien escrito, en una frase: "Tras ejecutar el flujo dos veces con ORD-2041, hay exactamente una fila en orders y exactamente un cobro en la pasarela, y ambas ejecuciones terminan sin error."

Las tres comprobaciones concretas:

  1. SELECT COUNT(*) FROM orders WHERE order_id = 'ORD-2041'; → debe dar 1.
  2. Consultar la pasarela (su panel o su API) por cobros de CUST-118/ORD-2041 → debe haber exactamente uno.
  3. Revisar el historial de ejecuciones de n8n → las dos ejecuciones deben estar marcadas como exitosas, ninguna con error.

Por qué funciona: un criterio de aceptación convierte "creo que está bien" en "puedo verificar que está bien". Cada comprobación mapea a un efecto distinto (fila, cobro) y a la salud del flujo (sin error). Si redactaste algo equivalente, ya piensas como quien entrega sistemas, no solo workflows.

Ejercicio 2 — Diagnostica por síntoma. Después de tu prueba de dos ejecuciones, obtienes cada uno de estos resultados. Para cada uno, di qué fase revisarías y cuál es la causa más probable:

(a) Dos filas en orders, un solo cobro. (b) Una fila en orders, dos cobros. (c) Una fila, un cobro, pero la segunda ejecución terminó con error. (d) Cero filas, cero cobros, las dos ejecuciones "exitosas".

Ver solución

(a) Revisa la Fase 2. El registro duplicó pero el cobro no, así que la cabecera funciona y el upsert no. Causa más probable: falta la restricción de unicidad sobre idempotency_key, o el nodo quedó como INSERT a ciegas en vez de ON CONFLICT.

(b) Revisa la Fase 3. El registro no duplicó (upsert bien) pero el cobro sí. Causa más probable: la cabecera Idempotency-Key no se está enviando (interruptor apagado, nombre mal escrito), o la API no la soporta con ese nombre. Menos probable aquí, pero posible: la clave no es estable (revísalo si usaste hash en vez de order_id).

(c) Revisa la Fase 4 y el manejo de errores. No duplicó, pero la segunda ejecución falló, y eso viola el criterio "sin error". Causa probable: hay un nodo que trata el "ya existe" como un fallo —por ejemplo, un INSERT sin ON CONFLICT que lanza el error de unicidad y nadie lo maneja—. Un flujo idempotente absorbe la repetición en silencio; si truena en la segunda, algo interpreta el duplicado esperado como error.

(d) Revisa la Fase 1 y el disparo. Cero efectos significa que el flujo no está haciendo nada —quizás el Code falla y detiene todo, o el pedido no está llegando, o un If mal puesto bloquea el camino—. No es un problema de idempotencia; es un problema de que el flujo no corre. Revisa que idempotency_key se esté calculando y que el pedido llegue completo.

Por qué funciona: cada síntoma apunta a una fase distinta, porque cada fase protege un efecto distinto. Diagnosticar por síntoma —"¿qué duplicó y qué no?"— te lleva directo a la causa, sin revisar todo el flujo a ciegas. Es exactamente cómo se depura un sistema en producción.

Ejercicio 3 — Adáptalo a un canal sin order_id. Tu flujo usa order_id como clave natural, perfecto para los pedidos web. Ahora te llega un pedido por WhatsApp sin order_id. Describe qué cambiarías —y qué no cambiarías— en las cinco fases para que el flujo siga siendo idempotente.

Ver solución

Lo único que cambia es la Fase 1. En vez de const idempotencyKey = order.order_id, calculas una clave sintética con un hash de los campos estables del pedido de WhatsApp, tal como en la lección 3:

const crypto = require('crypto');
const order = $input.item.json;
const fingerprint = [
  order.session_id,          // si existe, estable por envío: da especificidad
  order.customer_id,
  order.amount,
  order.line_items.map((l) => `${l.sku}x${l.quantity}`).join(','),
].join('|');
const idempotencyKey = crypto.createHash('sha256').update(fingerprint).digest('hex');

Lo que NO cambia:

  • Fase 2 (upsert): sigue siendo ON CONFLICT (idempotency_key) DO NOTHING, con la restricción de unicidad sobre idempotency_key. Como la clave siempre vive en esa columna —sea natural o sintética—, el upsert no distingue el canal. Ese es justo el beneficio de haber unificado todo bajo idempotency_key en la lección 4.
  • Fase 3 (cabecera): sigue mandando {{ $json.idempotency_key }} en la cabecera. La pasarela recibe el hash en vez de ORD-2041, y le da igual: deduplica por el valor que sea, mientras sea estable.
  • Fase 4 (sin verificar-y-actuar): idéntica.
  • Fase 5 (prueba): idéntica en estructura; solo cambias el pedido de entrada por el de WhatsApp y verificas el mismo criterio —dos ejecuciones, una fila, un cobro—.

Por qué funciona: diseñaste el flujo para que la identidad del evento viva siempre en un solo campo, idempotency_key, y ese campo lo llena la Fase 1 con lo que corresponda al canal. Cambiar de canal cambia cómo se calcula la clave, no cómo se usa. Es la señal de un buen diseño: la variabilidad queda aislada en un solo lugar, y el resto del flujo ni se entera.

Resumen y siguiente paso

En esta lección juntaste todo el módulo en un entregable. Tomaste el order-triage frágil que abría con un INSERT a ciegas y un POST que duplicaba, y lo volviste idempotente en fases: asignaste la clave estable en un nodo Code (Fase 1, lección 3), convertiste la inserción en un upsert ON CONFLICT con su restricción de unicidad (Fase 2, lección 4), blindaste el cobro con la cabecera Idempotency-Key (Fase 3, lección 5), y verificaste que no quedara ningún verificar-y-actuar (Fase 4, lección 6). Y lo más importante: definiste y ejecutaste el criterio de terminado —dos ejecuciones, una fila, un cobro, cero errores— que convierte "creo que es idempotente" en "demostré que es idempotente" (Fase 5). Aprendiste a presentarlo como lo haría un automation system owner: empezando por el problema, nombrando la propiedad, mostrando la clave elegida y dónde vive la unicidad, y cerrando con la prueba, no con la promesa. Y viste cómo la extensión con agente (lección 7) y el cambio de canal se acomodan sin rehacer el flujo, porque la identidad del evento vive siempre en un solo campo.

Con esto cierras la Fase 1 de la guía —la mentalidad de dueño de sistema (módulo 1) y una operación segura (módulo 2)—. Ya sabes hacer que un efecto, repetido cuantas veces sea, ocurra una sola vez.

El módulo 3 sube un nivel. Hasta aquí protegiste un workflow. Pero los sistemas reales son varios workflows que se llaman entre sí, y cuando uno le pasa datos a otro, necesita una promesa sobre qué forma tienen esos datos —un contrato—. Vas a ver qué es un contrato de workflow, cómo diseñar el esquema de entrada y de salida, cómo validar en la frontera del nodo Execute Workflow, y cómo versionar un contrato sin romper a quien lo llama. Y la clave de idempotencia que aprendiste aquí va a reaparecer: cuando un workflow le pasa un efecto a otro, el contrato es lo que garantiza que la clave llegue con la forma correcta. La operación segura ya la tienes; ahora la haces confiable entre workflows.

Recursos