Cargando
Miniatura del artículo

Arquitecto de automatización con IA: Día 55

Índice de contenido

Resumen:
Antes de la semana de pruebas, Valeria se detiene a revisar con calma todo lo que Trigo Lento construyó en el módulo 5: WhatsApp por la Cloud API de Meta, borradores de Gmail que solo salen con su firma, Slack como centro de operaciones, un orquestador que evita respuestas dobles e indicadores que avisan cuando algo se rompe. En esta entrada reunirá esas piezas en un solo mapa, comprobará su comprensión con doce preguntas de decisión que se corrigen solas, auditará con código cuatro workflows exportados contra los diez artículos del código de honor, medirá cuánto bajan los riesgos con los controles y consolidará el coste de la comunicación: $32.19$ USD de $45$, con $12.81$ libres para el módulo 6. Al terminar sabrá decir, mensaje por mensaje, quién contesta qué.
¿Cómo citar el presente artículo?
Romero, J. (27 de marzo de 2026). Arquitecto de automatización con IA: Día 55. python-data-engineer. https://www.python-data-engineer.com/post/aai/dia_55/.
Arquitecto de automatización con IA: Día 55 by Jeshua Romero Guadarrama, available under Attribution 4.0 International (CC BY 4.0) at https://www.python-data-engineer.com/post/aai/dia_55/.

Precios, límites y políticas revisados en octubre de 2026: planes, precios y límites de Make, n8n, Zapier, WhatsApp (Meta, Twilio, Wati) y de la API de Claude. Se contrastaron en octubre de 2026 la regla de la ventana de atención de 24 horas (fuera de ella solo se admiten plantillas aprobadas), el cobro de los mensajes de servicio a partir del número $1,001$ de cada mes desde el 1 de octubre de 2026 (fuentes de proveedores; la página de Meta se revisa cada trimestre) y la política de mensajería de WhatsApp Business. Las cifras de coste proceden de los días 30, 38 y 46 a 49; las preguntas, los riesgos y los consentimientos son casos inventados y no se recoge ningún dato del lector.

Día 55. Repaso del ecosistema de comunicación

Introducción


Ayer, en el Día 54 (Anatomía de un fallo real III: la plantilla pausada y los recordatorios que no salieron), Valeria reconstruyó con los registros, sin buscar culpables, cómo una plantilla de WhatsApp pasó de pausada a desactivada mientras el flujo seguía “en verde”, y Trigo Lento ganó TL Monitor de plantillas v1, una plantilla de respaldo y alertas que miran los recordatorios entregados y no las ejecuciones. Con ese incidente cerrado, el módulo 5 tiene todas sus piezas en su sitio, y hoy toca algo que ningún flujo hace por sí mismo: mirarlas juntas.

Es viernes 27 de marzo, por la tarde. El domingo es de Ramos y la semana que viene es de pruebas: el Día 56 pondrá a funcionar una semana entera de comunicación automatizada, con todos los canales a la vez. Valeria no quiere llegar a esa semana con una colección de flujos que solo ella entiende. Abre el cuaderno en una página en blanco y escribe tres preguntas: ¿qué tenemos?, ¿lo entiendo de verdad? y ¿qué puede salir mal? La cuarta se la hace Don Ernesto, que entra a la oficina con el delantal todavía puesto y mira la pantalla llena de cajas de colores: “Valeria, en estas dos semanas usted me ha enseñado el WhatsApp nuevo, el correo, el Slack ese y el orquestador. Yo ya no sé quién le contesta a quién. Si mañana una clienta se queja de lo que le dijeron, ¿a quién le pregunto? ¿Al aparato?”

La pregunta es justa y no se responde con otra explicación técnica. Se responde con un inventario, con un mapa y con una regla escrita para cada tipo de mensaje. Un repaso, además, no es releer: la investigación sobre memoria de Henry Roediger y Jeffrey Karpicke mostró que intentar recordar y decidir, con una prueba, fija lo aprendido mucho mejor que volver a leer los apuntes. Por eso el repaso de hoy no es un resumen, sino un examen que el propio lector corrige, seguido de una auditoría que se ejecuta sobre los flujos reales.

En la presente entrada, descubrirá cómo cerrar un módulo de automatización con criterio de arquitecto. Hará el inventario de los flujos de comunicación con su canal, su base de envío y la persona al mando, y dibujará el mapa del ecosistema. Responderá doce preguntas de decisión, como qué puede enviarle a una clienta que escribió hace $26$ horas, y las corregirá con un banco en Python que da retroalimentación por opción y dice qué días conviene repasar. Después auditará con código cuatro workflows exportados contra el código de honor del Día 6, construirá una matriz de riesgos antes y después de los controles, consolidará el coste mensual frente a los $45$ USD, revisará las bajas y los consentimientos que se quedaron atrás y terminará con la matriz que responde a Don Ernesto: quién contesta qué.

Objetivos de aprendizaje


Al terminar esta entrada, usted podrá:

  • Integrar en un solo mapa lo construido en los días 46 a 54: canales, bases de envío, proveedores, aprobaciones y orquestación.
  • Comprobar la comprensión con 12 preguntas autocorregibles centradas en decisiones, no en definiciones.
  • Auditar el ecosistema con el código de honor y con una matriz de riesgos de comunicación.
  • Consolidar el coste mensual de comunicación y de Claude frente al presupuesto.

El mapa del ecosistema de comunicación


Nueve días de trabajo dejan muchas piezas, y la primera tarea de un repaso es contarlas. Valeria empieza por el principio del módulo. El Día 46 (El reto de la mensajería: el mapa de canales y el coste real de WhatsApp) eligió la Cloud API de Meta directa desde n8n porque Twilio cobraba $0.005$ USD por cada mensaje recibido y enviado. El Día 47 (Gmail con criterio: OAuth, etiquetas y borradores contextuales con Claude) conectó el buzón de mayoristas con un solo alcance y sin ningún nodo de envío. El Día 48 (WhatsApp Business API: plantillas, ventana de 24 horas, secuencias y chatbots) registró las plantillas, escribió la regla puede_enviar y montó la secuencia de bienvenida. El Día 49 (Accesibilidad y multilingüismo de los mensajes) hizo legibles las plantillas y abrió el inglés con revisión humana. El Día 50 (Slack como centro de operaciones: webhooks, Block Kit y aprobaciones) sacó los avisos del teléfono personal de Valeria. El Día 51 llevó el método a una clínica dental. El Día 52 (Orquestación multicanal sin volverse loco: enrutamiento, deduplicación, horarios y SLA) puso una sola puerta de entrada; el Día 53 (Métricas y SLA de la atención: percentiles, presupuesto de error y alertas) la midió, y el Día 54 cerró el hueco de monitoreo que dejó pasar doce días sin recordatorios.

Un inventario útil no es una lista de nombres. Para cada flujo hay que poder responder cinco preguntas: en qué día nació, por qué canal habla, en qué dirección (entra, sale, conversa o se queda dentro del equipo), con qué base de envío escribe a un cliente y qué persona está al mando de lo que hace. La base de envío es la regla del módulo convertida en columna: un mensaje solo sale si el cliente escribió primero y la ventana de atención sigue abierta, o si hay consentimiento expreso para esa finalidad y se usa una plantilla aprobada por Meta en la categoría correcta. Ninguna fila puede quedar en blanco.

Comencemos por importar las bibliotecas, fijar la semilla de la serie y declarar los supuestos del módulo:

python
# Importaciones base, semilla de la serie, carpeta del día y supuestos fijos en Python:
import warnings
warnings.filterwarnings('ignore')
import json
import re
import sqlite3
import textwrap
from collections import Counter
from datetime import datetime, timedelta
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Polygon, RegularPolygon, Ellipse
from scipy import stats

# Generador aleatorio con la semilla global de la serie:
rng = np.random.default_rng(2026)
pd.set_option('display.width', 120)
pd.set_option('display.max_colwidth', 48)
proyecto = Path('trigo_lento')
carpeta = proyecto / 'dia_55'
carpeta.mkdir(parents = True, exist_ok = True)

# Supuestos de la serie (A.4.6 y A.5) y del módulo 5:
PRESUPUESTO = 45.00          # USD al mes para todo el ecosistema
MXN_POR_USD = 18.50          # tipo de cambio didáctico fijo
EVENTOS_MES = {'WhatsApp': 1560, 'Instagram': 400, 'Gmail': 300, 'Formulario': 70}   # Día 52
print('numpy', np.__version__, '| pandas', pd.__version__, '| carpeta:', carpeta.as_posix())
print(f'Eventos de comunicación al mes: {sum(EVENTOS_MES.values()):,}'.replace(',', ' '),
      '| presupuesto:', f'{PRESUPUESTO:.2f} USD ({PRESUPUESTO * MXN_POR_USD:.2f} MXN)')
numpy 2.4.6 | pandas 3.0.3 | carpeta: trigo_lento/dia_55
Eventos de comunicación al mes: 2 330 | presupuesto: 45.00 USD (832.50 MXN)

El volumen de referencia es el del Día 52: $2,330$ eventos al mes entre WhatsApp, Instagram, Gmail y el formulario, que se reparten en $1,560$, $400$, $300$ y $70$. Con ese volumen y con el presupuesto de $45$ USD ($832.50$ MXN al tipo didáctico) se comprobará todo lo demás. El siguiente código escribe el inventario como un DataFrame y lo guarda en inventario_modulo_5.csv, para que el Día 56 y el módulo 8 lo lean sin volver a teclearlo:

python
# Inventario de los flujos del módulo 5 con canal, base de envío, dueño y persona al mando en Python:
inventario = pd.DataFrame([
    ('TL Orquestador v1', 52, 'Todos', 'Entrada', 'No escribe: enruta', 'Valeria', 'Escala a #aprobaciones'),
    ('TL Responder por canal v1', 52, 'WhatsApp, Instagram', 'Salida', 'Ventana abierta', 'Valeria',
     'Acuse y respuestas fijas'),
    ('TL Agente de pedidos v2', 38, 'WhatsApp', 'Conversación', 'Ventana abierta', 'Valeria',
     'Aprueba pasteles y cambios'),
    ('TL Gmail mayoristas v1', 47, 'Gmail', 'Borrador', 'Relación mayorista', 'Valeria', 'Valeria envía'),
    ('TL WhatsApp secuencias v2', 54, 'WhatsApp', 'Salida', 'Opt-in o pedido en curso', 'Valeria',
     'Plantillas que aprobó Valeria'),
    ('TL Slack operaciones v1', 50, 'Slack', 'Interno', 'Equipo', 'Valeria', 'Valeria o Don Ernesto'),
    ('TL Métricas de atención v1', 53, 'Slack', 'Interno', 'Equipo', 'Valeria', 'Lee #alertas'),
    ('TL Monitor de plantillas v1', 54, 'Slack', 'Interno', 'Equipo', 'Valeria', 'Lee #alertas'),
], columns = ['flujo', 'dia', 'canal', 'direccion', 'base_de_envio', 'dueno', 'persona_al_mando'])
inventario.to_csv(carpeta / 'inventario_modulo_5.csv', index = False, encoding = 'utf-8')
print(inventario.drop(columns = 'dueno').to_string(index = False))
print('Flujos:', len(inventario), '| escriben a clientes:',
      int(inventario.direccion.isin(['Salida', 'Conversación']).sum()),
      '| sin base de envío declarada:', int(inventario.base_de_envio.eq('').sum()),
      '| dueños distintos:', inventario.dueno.nunique())
                      flujo  dia               canal    direccion            base_de_envio              persona_al_mando
          TL Orquestador v1   52               Todos      Entrada       No escribe: enruta        Escala a #aprobaciones
  TL Responder por canal v1   52 WhatsApp, Instagram       Salida          Ventana abierta      Acuse y respuestas fijas
    TL Agente de pedidos v2   38            WhatsApp Conversación          Ventana abierta    Aprueba pasteles y cambios
     TL Gmail mayoristas v1   47               Gmail     Borrador       Relación mayorista                 Valeria envía
  TL WhatsApp secuencias v2   54            WhatsApp       Salida Opt-in o pedido en curso Plantillas que aprobó Valeria
    TL Slack operaciones v1   50               Slack      Interno                   Equipo         Valeria o Don Ernesto
 TL Métricas de atención v1   53               Slack      Interno                   Equipo                  Lee #alertas
TL Monitor de plantillas v1   54               Slack      Interno                   Equipo                  Lee #alertas
Flujos: 8 | escriben a clientes: 3 | sin base de envío declarada: 0 | dueños distintos: 1

El inventario tiene $8$ flujos, de los cuales $3$ escriben a clientes (TL Responder por canal v1, TL Agente de pedidos v2 y TL WhatsApp secuencias v2), y ninguno sin base de envío declarada. El correo de mayoristas no cuenta como “escribe a clientes” porque deja borradores: quien envía es Valeria. La Tabla 1 resume el inventario con la base de cada flujo.

Tabla 1. Inventario de los flujos de comunicación de Trigo Lento al cierre del módulo 5.

FlujoDíaCanalDirecciónBase de envíoPersona al mando
TL Orquestador v152TodosEntradaNo escribe: enrutaEscala a #aprobaciones
TL Responder por canal v152WhatsApp, InstagramSalidaVentana abiertaAcuse y respuestas fijas
TL Agente de pedidos v238WhatsAppConversaciónVentana abiertaValeria aprueba pasteles y cambios
TL Gmail mayoristas v147GmailBorradorRelación mayoristaValeria revisa y envía
TL WhatsApp secuencias v254WhatsAppSalidaOpt-in o pedido en cursoPlantillas que aprobó Valeria
TL Slack operaciones v150SlackInternoEquipoValeria o Don Ernesto
TL Métricas de atención v153SlackInternoEquipoQuien lee #alertas
TL Monitor de plantillas v154SlackInternoEquipoQuien lee #alertas

Hay una cifra del inventario que pasa desapercibida y que conviene subrayar: un solo dueño. Los ocho flujos tienen a Valeria como responsable. En una panadería de seis personas es lo natural, pero es también un riesgo de operación: si Valeria se enferma la semana de Pascua, nadie sabe qué hacer con una alerta de plantilla pausada. El inventario no lo arregla, pero lo hace visible, y el módulo 8 dedicará un día entero a los manuales de operación que permiten que otra persona tome el relevo.

La tabla dice qué hay; el mapa dice cómo se conecta. El siguiente código dibuja el ecosistema con las formas fijas de la serie: los canales como óvalos verdes, el orquestador como rombo naranja porque decide, los flujos con Claude en morado, el monitor de plantillas en rojo porque maneja errores y las personas como hexágonos grises:

python
# Mapa del ecosistema de comunicación dibujado con las formas de la serie en Python:
FORMAS = {'canal': ('disparador', '#1f9d55'), 'flujo': ('accion', '#2a78d6'), 'decision': ('decision', '#eb6834'),
          'ia': ('ia', '#7a5cc9'), 'persona': ('humano', '#8a8a8a'), 'error': ('error', '#c8423b')}

def dibujar_flujo(nodos, aristas, titulo, ancho = 1.9, alto = 0.8, tamano = (15, 8.4)):
    fig, ax = plt.subplots(figsize = tamano)
    pos = {n['id']: (n['x'], n['y']) for n in nodos}
    medio = {n['id']: {'decision': (1.15, 0.62), 'humano': (0.6, 0.6)}.get(FORMAS[n['tipo']][0],
                                                                            (ancho / 2, alto / 2)) for n in nodos}
    for o, d, rotulo in aristas:
        (x1, y1), (x2, y2) = pos[o], pos[d]
        if x1 == x2:      # vertical: de borde inferior o superior al opuesto
            s = 1 if y2 > y1 else -1
            a, b = (x1, y1 + s * medio[o][1]), (x2, y2 - s * medio[d][1])
        else:             # de izquierda a derecha: del borde derecho al borde izquierdo
            a, b = (x1 + medio[o][0], y1), (x2 - medio[d][0], y2)
        ax.add_patch(FancyArrowPatch(a, b, arrowstyle = '-|>', mutation_scale = 14, color = '#7d7b76',
                                     linewidth = 1.3, shrinkA = 2, shrinkB = 3, zorder = 1))
        if rotulo:
            ax.text((x1 + x2) / 2, (y1 + y2) / 2 + 0.12, rotulo, fontsize = 9, color = '#52514e', ha = 'center')
    for n in nodos:
        forma, color = FORMAS[n['tipo']]
        x, y = n['x'], n['y']
        if forma == 'decision':
            ax.add_patch(Polygon([(x - 1.15, y), (x, y + 0.62), (x + 1.15, y), (x, y - 0.62)],
                                 facecolor = color, edgecolor = 'white', linewidth = 2, zorder = 2))
        elif forma == 'disparador':
            ax.add_patch(Ellipse((x, y), ancho, alto, facecolor = color, edgecolor = 'white', linewidth = 2, zorder = 2))
        elif forma == 'humano':
            ax.add_patch(RegularPolygon((x, y), 6, radius = 0.62, orientation = np.pi / 6, facecolor = color,
                                        edgecolor = 'white', linewidth = 2, zorder = 2))
        else:
            ax.add_patch(FancyBboxPatch((x - ancho / 2, y - alto / 2), ancho, alto, boxstyle = 'round,pad=0.04',
                                        facecolor = color, edgecolor = 'white', linewidth = 2, zorder = 2))
        ax.text(x, y, textwrap.fill(n['id'], 15), ha = 'center', va = 'center', color = 'white',
                fontsize = 9, zorder = 3)
    xs = [p[0] for p in pos.values()]
    ys = [p[1] for p in pos.values()]
    ax.set_xlim(min(xs) - 1.3, max(xs) + 1.3)
    ax.set_ylim(min(ys) - 0.9, max(ys) + 0.9)
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_title(titulo, fontsize = 15)
    return fig, ax

nodos = ([{'id': c, 'tipo': 'canal', 'x': 0, 'y': y} for c, y in
          [('WhatsApp', 4), ('Instagram', 2.6), ('Gmail', 1.2), ('Formulario', -0.2), ('Teléfono', -1.6)]] +
         [{'id': 'TL Orquestador v1', 'tipo': 'decision', 'x': 3.3, 'y': 1.2},
          {'id': 'TL Responder por canal v1', 'tipo': 'flujo', 'x': 6.6, 'y': 4.3},
          {'id': 'TL Agente de pedidos v2', 'tipo': 'ia', 'x': 6.6, 'y': 2.9},
          {'id': 'TL Gmail mayoristas v1', 'tipo': 'ia', 'x': 6.6, 'y': 1.5},
          {'id': 'TL WhatsApp secuencias v2', 'tipo': 'flujo', 'x': 6.6, 'y': 0.1},
          {'id': 'TL Monitor de plantillas v1', 'tipo': 'error', 'x': 6.6, 'y': -1.3},
          {'id': 'TL Slack operaciones v1', 'tipo': 'flujo', 'x': 9.9, 'y': 1.5},
          {'id': 'TL Métricas de atención v1', 'tipo': 'flujo', 'x': 9.9, 'y': -0.3},
          {'id': 'Valeria', 'tipo': 'persona', 'x': 12.9, 'y': 4.0},
          {'id': 'Don Ernesto', 'tipo': 'persona', 'x': 12.9, 'y': 1.5},
          {'id': 'Mostrador', 'tipo': 'persona', 'x': 12.9, 'y': -1.0}])
aristas = ([(c, 'TL Orquestador v1', '') for c in ['WhatsApp', 'Instagram', 'Gmail', 'Formulario', 'Teléfono']] +
           [('TL Orquestador v1', f, '') for f in ['TL Responder por canal v1', 'TL Agente de pedidos v2',
                                                   'TL Gmail mayoristas v1']] +
           [('TL Agente de pedidos v2', 'TL Slack operaciones v1', 'aprobación'),
            ('TL Gmail mayoristas v1', 'Valeria', 'borrador'),
            ('TL WhatsApp secuencias v2', 'TL Monitor de plantillas v1', ''),
            ('TL Monitor de plantillas v1', 'TL Métricas de atención v1', ''),
            ('TL Métricas de atención v1', 'TL Slack operaciones v1', '#alertas'),
            ('TL Slack operaciones v1', 'Valeria', ''), ('TL Slack operaciones v1', 'Don Ernesto', 'relevo'),
            ('TL Slack operaciones v1', 'Mostrador', '')])
nombres = {n['id'] for n in nodos}
assert all(o in nombres and d in nombres for o, d, _ in aristas), 'Arista hacia un nodo inexistente'
print('Nodos:', len(nodos), '| aristas:', len(aristas), '|', dict(Counter(n['tipo'] for n in nodos)))
grado = Counter([o for o, _, _ in aristas] + [d for _, d, _ in aristas])
print('Nodo con más conexiones:', grado.most_common(1)[0])
dibujar_flujo(nodos, aristas, 'Ecosistema de comunicación de Trigo Lento al cierre del módulo 5')
plt.tight_layout()
plt.show()
Nodos: 16 | aristas: 16 | {'canal': 5, 'decision': 1, 'flujo': 4, 'ia': 2, 'error': 1, 'persona': 3}
Nodo con más conexiones: ('TL Orquestador v1', 8)

Gráfica: El mapa del ecosistema de comunicación. Mapa del ecosistema de comunicación dibujado con las formas de la serie

El mapa tiene $16$ nodos y $16$ conexiones, y el nodo con más conexiones es el orquestador, con $8$: cinco entradas y tres salidas. Se lee de izquierda a derecha. Los cinco canales entran por una sola puerta, TL Orquestador v1, que reconoce al cliente, descarta los duplicados y manda cada evento al flujo que le toca. Los flujos con Claude, en morado, nunca terminan en un cliente sin pasar por una persona: el agente pide aprobación en Slack y el de Gmail deja un borrador en la bandeja de Valeria. La secuencia de WhatsApp tiene ahora debajo al monitor de plantillas del Día 54, que alimenta las métricas del Día 53, que a su vez escriben en #alertas. Y a la derecha, las tres personas que cierran cada camino.

La Figura 1 reúne en un mapa conceptual las ideas que sostienen ese dibujo: cada mensaje recorre canal, base de envío, ventana o plantilla, proveedor, aprobación y orquestador, y termina medido contra su SLA.

Mapa conceptual del módulo 5: cada mensaje recorre canal, base de envío, ventana, plantilla, proveedor, aprobación y orquestador, y termina medido contra su SLA; cada concepto lleva su regla y el día que la construyó.

Figura 1. Mapa conceptual del módulo 5: cada mensaje recorre canal, base de envío, ventana, plantilla, proveedor, aprobación y orquestador, y termina medido contra su SLA; cada concepto lleva su regla y el día que la construyó.

Conviene notar lo que el mapa no muestra: el teléfono no tiene flujo propio, porque las llamadas las atiende el mostrador y solo se registran en el formulario interno; e Instagram entra al orquestador, pero no tiene secuencias ni plantillas, porque el Día 48 decidió que el pan de temporada se anuncia en la red social y no por WhatsApp. Un mapa honesto también enseña lo que se decidió no automatizar.

Doce preguntas de decisión


Un repaso que pregunta definiciones ("¿qué es una plantilla de utilidad?") mide memoria, no criterio. Las preguntas de hoy plantean situaciones que Valeria encontrará la semana que viene y piden una decisión. Están agrupadas en cuatro temas de tres preguntas cada uno: ventana y plantillas, consentimiento y orquestación, control humano, y medición y costes. Cada pregunta indica el día en el que se trató el asunto, para que quien falle sepa adónde volver.

La primera pregunta es la que Valeria se hizo esta misma mañana. Una clienta encargó un pastel el miércoles a las 15:00 y el jueves a las 17:00, $26$ horas después de su último mensaje, nadie le ha confirmado la hora de recogida. ¿Qué se le puede enviar? La regla de la ventana se escribe así:

$$ \text{texto libre permitido} \iff t_{\text{envío}} - t_{\text{último mensaje del cliente}} < 24\ \text{h} $$

La lógica detrás de esta regla es la siguiente:

  • $t_{\text{último mensaje del cliente}}$ es la marca de tiempo del último mensaje que el cliente envió; los mensajes del negocio no abren ni renuevan la ventana.
  • Si la diferencia es menor de $24$ horas, el negocio puede responder con texto libre, botones o listas; desde el 1 de octubre de 2026, cada respuesta por encima de las $1,000$ del mes se cobra a la tarifa de utilidad.
  • Si la diferencia es de $24$ horas o más, solo cabe una plantilla aprobada, y su categoría tiene que corresponder a la base: utilidad para el pedido en curso, marketing solo con consentimiento de marketing.

A las $26$ horas la ventana está cerrada, pero la clienta tiene un pedido en curso: cabe una plantilla de utilidad aprobada sobre ese pedido, como el recordatorio de recogida. No cabe un texto libre ni, menos aún, aprovechar para ofrecerle conchas. La Tabla 2 reúne las doce preguntas con sus cuatro opciones.

Tabla 2. Doce preguntas de decisión del módulo 5, con cuatro opciones y el día que trata cada caso.

#PreguntaABCDDía
1Una clienta escribió hace 26 horas y pregunta si su pastel ya está listo. ¿Qué se le puede enviar?Un texto libre con la respuestaUna plantilla de marketing con el pastelUna plantilla de utilidad aprobada sobre su pedido en cursoNada hasta que vuelva a escribir48
2Valeria registra como utilidad una plantilla que dice “Su pedido está listo. ¡Y llévese 10 % en conchas!”. ¿Qué ocurre?Nada: la categoría la elige el negocioMeta la trata como marketingSe cobra igual que antesSolo cambia el pie del mensaje48
3Al enviar un recordatorio, la Cloud API responde 400 porque la plantilla está pausada. ¿Qué debe hacer el flujo?Reintentar cada 5 segundosMarcarlo como enviadoCambiar la plantilla por un texto libreRegistrar el fallo, avisar en #alertas y usar la plantilla de respaldo54
4Un cliente dio su consentimiento solo para confirmaciones de pedido. ¿Se le puede enviar la bienvenida de novedades?No: falta el consentimiento de marketingSí, si se envía en horarioSí, una sola vezSí, como plantilla de utilidad48
5Una clienta responde “ya no” a un recordatorio de recogida. ¿Qué ocurre?Se le pregunta el motivo primeroSe le da de baja solo de los recordatoriosBaja inmediata en todas las finalidadesValeria decide al final del día48
6Una clienta pide el mismo pastel por WhatsApp y por Instagram con 10 minutos de diferencia. ¿Qué hace el orquestador?Responde en los dos canalesFusiona los eventos por su huella y responde una sola vezBorra el segundo mensajeLe pregunta por cuál canal seguir52
7Marisol escribe un correo con un cambio de pedido y una queja por una entrega tardía. ¿Qué deja TL Gmail mayoristas v1?Un borrador con un hueco para que Valeria conteste la quejaLa respuesta completa, ya enviadaNada: las quejas se ignoranUna disculpa redactada por Claude47
8Llega a n8n la pulsación de un botón de Slack con firma válida y una marca de tiempo de hace 10 minutos. ¿Qué hace el flujo?La acepta, porque la firma es válidaLa acepta si viene de ValeriaLa guarda para el día siguienteLa rechaza: la tolerancia es de 5 minutos50
9Un cliente encarga un pastel y avisa que su hija es alérgica a la nuez. ¿Quién le confirma el pedido?El agente, si la receta lo permiteEl chatbot, con un mensaje fijoValeria, después de consultar a Don ErnestoNadie: se rechaza el pedido38
10El p95 de WhatsApp es de 11.9 minutos y el del formulario de 35.3. ¿Cuál es el p95 de los dos canales juntos?23.6 minutos, la media de los dos35.3 minutos, el peor de los dos11.9 minutos, el del canal con más eventosEl que resulte de calcularlo sobre todos los eventos juntos53
11Con un SLO del 95 % y 1 560 mensajes de WhatsApp al mes, ¿cuál es el presupuesto de error?1 482 incumplimientos78 incumplimientos95 incumplimientos7.8 incumplimientos53
12¿Por qué la mensajería de WhatsApp por Twilio costaba 19.13 USD al mes?Por el recargo de 0.005 USD por cada mensaje recibido y enviadoPor las tarifas de marketingPor la cuota fija de TwilioPor los mensajes de servicio46

Antes de seguir, conviene que usted conteste las doce preguntas en un papel, sin mirar los días anteriores. El valor de la prueba está justo en el esfuerzo de recordar y decidir. Las respuestas, con la razón de cada una, están plegadas al final de la siguiente sección.

Las preguntas no se escribieron solo en una tabla: viven también en un banco de datos que se puede corregir, ampliar y reutilizar. El siguiente código guarda cada pregunta con su tema, su día, su respuesta correcta y una retroalimentación por opción, es decir, una frase que explica por qué cada respuesta incorrecta es incorrecta, que es lo que de verdad enseña:

python
# Banco de 12 preguntas de decisión con retroalimentación por opción en Python:
def pregunta(n, tema, dia, enunciado, correcta, opciones):
    # opciones: cuatro pares (texto, retroalimentación) en el orden A, B, C, D:
    return {'n': n, 'tema': tema, 'dia': dia, 'enunciado': enunciado, 'correcta': correcta,
            'opciones': {l: t for l, (t, _) in zip('ABCD', opciones)},
            'retro': {l: r for l, (_, r) in zip('ABCD', opciones)}}

BANCO = [
    pregunta(1, 'Ventana y plantillas', 48, 'Escribió hace 26 h y pregunta por su pastel: ¿qué se le envía?', 'C', [
        ('Un texto libre con la respuesta', 'A las 24 horas la ventana se cerró: ya no cabe texto libre.'),
        ('Una plantilla de marketing con el pastel', 'Sin opt-in de marketing, y además no es promoción.'),
        ('Una plantilla de utilidad aprobada sobre su pedido', 'Correcto: el pedido en curso es la base.'),
        ('Nada hasta que vuelva a escribir', 'Sí hay base: su pedido en curso admite una plantilla de utilidad.')]),
    pregunta(2, 'Ventana y plantillas', 48, 'Utilidad con "llévese 10 % en conchas": ¿qué pasa?', 'B', [
        ('Nada: la categoría la elige el negocio', 'La categoría la decide el contenido, no quien la registra.'),
        ('Meta la trata como marketing', 'Correcto: más cara y exige consentimiento de marketing.'),
        ('Se cobra igual que antes', 'Pasa de 0.0085 a 0.0397 USD por mensaje.'),
        ('Solo cambia el pie del mensaje', 'Cambian categoría, tarifa y consentimiento exigido.')]),
    pregunta(3, 'Ventana y plantillas', 54, 'La Cloud API responde 400: plantilla pausada. ¿Qué hace el flujo?', 'D', [
        ('Reintenta cada 5 segundos', 'Un 400 no es un error de red: reintentar no cambia nada.'),
        ('Lo marca como enviado', 'Es justo lo que ocultó el incidente del Día 54.'),
        ('Cambia la plantilla por texto libre', 'Fuera de la ventana el texto libre no está permitido.'),
        ('Registra, avisa en #alertas y usa el respaldo', 'Correcto: se ve el fallo y el recordatorio sale.')]),
    pregunta(4, 'Consentimiento y orquestación', 48, 'Opt-in solo de confirmaciones: ¿bienvenida de novedades?', 'A', [
        ('No: falta el consentimiento de marketing', 'Correcto: el consentimiento es por finalidad.'),
        ('Sí, si se envía en horario', 'El horario no sustituye al consentimiento.'),
        ('Sí, una sola vez', 'Ni una vez: la finalidad no coincide.'),
        ('Sí, como plantilla de utilidad', 'Una bienvenida comercial no es utilidad.')]),
    pregunta(5, 'Consentimiento y orquestación', 48, 'Responde "ya no" a un recordatorio: ¿qué ocurre?', 'C', [
        ('Se le pregunta el motivo primero', 'La baja no se condiciona a nada.'),
        ('Baja solo de recordatorios', 'La baja se aplica a todas las finalidades.'),
        ('Baja inmediata en todas las finalidades', 'Correcto: en el mismo minuto, sin borrar el registro.'),
        ('Valeria decide al final del día', 'No hay nada que decidir.')]),
    pregunta(6, 'Consentimiento y orquestación', 52, 'Mismo pastel por WhatsApp e Instagram en 10 min: ¿qué hace?', 'B', [
        ('Responde en los dos canales', 'Es la respuesta doble que el orquestador evita.'),
        ('Fusiona por huella y responde una vez', 'Correcto: ventana de 30 min con huella del pedido.'),
        ('Borra el segundo mensaje', 'No se borra nada: se fusiona y queda registrado.'),
        ('Pregunta a la clienta por cuál siguen', 'Solo se pregunta si la coincidencia es dudosa.')]),
    pregunta(7, 'Control humano', 47, 'Correo de Marisol con queja y cambio de pedido: ¿qué deja el flujo?', 'A', [
        ('Un borrador con hueco para la queja', 'Correcto: la queja la escribe y firma Valeria.'),
        ('La respuesta enviada sola', 'TL Gmail mayoristas v1 no tiene ningún nodo de envío.'),
        ('Nada: las quejas se ignoran', 'Se etiqueta y se deja lista para Valeria.'),
        ('Una disculpa redactada por Claude', 'Las quejas no las redacta el modelo.')]),
    pregunta(8, 'Control humano', 50, 'Botón de Slack con firma válida y marca de hace 10 min: ¿qué hace?', 'D', [
        ('La acepta: la firma es válida', 'Una firma vieja puede ser una repetición.'),
        ('La acepta si viene de Valeria', 'La identidad no corrige una marca vencida.'),
        ('La guarda para mañana', 'No hay nada que guardar: se rechaza.'),
        ('La rechaza: tolerancia de 5 minutos', 'Correcto: firma, marca y que nadie haya decidido antes.')]),
    pregunta(9, 'Control humano', 38, 'Pastel con alergia a la nuez: ¿quién confirma al cliente?', 'C', [
        ('El agente, si la receta lo permite', 'Las recetas no entran en ningún modelo.'),
        ('El chatbot con un mensaje fijo', 'Una alergia no tiene respuesta fija segura.'),
        ('Valeria, tras consultar a Don Ernesto', 'Correcto: es una puerta humana desde el Día 6.'),
        ('Nadie: se rechaza el pedido', 'Se atiende, pero lo decide una persona.')]),
    pregunta(10, 'Medición y costes', 53, 'p95 de WhatsApp 11.9 y del formulario 35.3: ¿p95 conjunto?', 'D', [
        ('23.6 minutos, la media', 'Los percentiles no se promedian.'),
        ('35.3, el peor de los dos', 'El conjunto mezcla volúmenes muy distintos.'),
        ('11.9, el canal con más eventos', 'Hay que mirar todos los eventos juntos.'),
        ('Se calcula sobre todos los eventos juntos', 'Correcto: percentil de la muestra conjunta.')]),
    pregunta(11, 'Medición y costes', 53, 'SLO del 95 % y 1 560 mensajes al mes: ¿presupuesto de error?', 'B', [
        ('1 482 incumplimientos', 'Es el número de mensajes buenos, no el presupuesto.'),
        ('78 incumplimientos', 'Correcto: B = (1 - 0.95) * 1 560 = 78.'),
        ('95 incumplimientos', 'Confundió el SLO con un número de eventos.'),
        ('7.8 incumplimientos', 'Usó un SLO del 99.5 %.')]),
    pregunta(12, 'Medición y costes', 46, '¿Por qué WhatsApp por Twilio costaba 19.13 USD?', 'A', [
        ('Recargo de 0.005 por mensaje recibido y enviado', 'Correcto: 85 % de la factura era recargo.'),
        ('Por las tarifas de marketing', 'Trigo Lento casi no enviaba marketing.'),
        ('Por la cuota fija de Twilio', 'Twilio no tiene cuota fija en ese uso.'),
        ('Por los mensajes de servicio', 'Antes de octubre de 2026 Meta no los cobraba.')]),
]
TEMAS = ['Ventana y plantillas', 'Consentimiento y orquestación', 'Control humano', 'Medición y costes']
(carpeta / 'banco_preguntas_dia_55.json').write_text(json.dumps(BANCO, ensure_ascii = False, indent = 2),
                                                    encoding = 'utf-8')
banco = pd.DataFrame(BANCO)
print('Preguntas por tema:', banco.groupby('tema', sort = False).size().to_dict())
print('Respuestas correctas por letra:', banco['correcta'].value_counts().sort_index().to_dict())
print('Días que cubre el banco:', sorted(banco['dia'].unique().tolist()))
Preguntas por tema: {'Ventana y plantillas': 3, 'Consentimiento y orquestación': 3, 'Control humano': 3, 'Medición y costes': 3}
Respuestas correctas por letra: {'A': 3, 'B': 3, 'C': 3, 'D': 3}
Días que cubre el banco: [38, 46, 47, 48, 50, 52, 53, 54]

El banco tiene tres preguntas por tema y las respuestas correctas están repartidas por igual entre las cuatro letras, $3$ cada una, para que nadie acierte por adivinar un patrón (“casi siempre es la C”). Cubre ocho días del módulo y de los anteriores: $38$, $46$, $47$, $48$, $50$, $52$, $53$ y $54$. El Día 49 no tiene pregunta propia, porque su revisor de accesibilidad se pondrá a prueba en la práctica el Día 56, y el Día 51 trató otro negocio.

Autocorrección en Python


Con el banco listo, corregir es comparar. La función corregir recibe las respuestas de un lector como un diccionario y devuelve una fila por pregunta con el acierto y la retroalimentación de la opción elegida. Después se agrupa por tema y se compara con un umbral del $80%$, el mismo que usaron las evaluaciones de los días 9, 32 y 44. La puntuación de un tema $t$ es:

$$ p_t = \frac{1}{n_t}\sum_{i \in t} \mathbb{1}[,r_i = c_i,], \qquad P(X \geq 10) = \sum_{k=10}^{12}\binom{12}{k},0.25^{k},0.75^{12-k} $$

La lógica detrás de estas dos fórmulas es la siguiente:

  • $r_i$ es la respuesta elegida en la pregunta $i$ y $c_i$ la correcta; la función indicadora $\mathbb{1}[\cdot]$ vale $1$ si coinciden y $0$ si no, de modo que $p_t$ es la proporción de aciertos del tema.
  • Con $n_t = 3$ preguntas por tema, un solo fallo deja el tema en $66.7%$, por debajo del umbral: el diagnóstico es exigente a propósito, porque cada tema es una regla que se aplica cada día.
  • $P(X \geq 10)$ es la probabilidad de acertar $10$ o más de $12$ preguntas contestando al azar, con $X$ binomial de parámetros $n = 12$ y $p = 0.25$; si es pequeñísima, un buen resultado no es suerte.

El lector de ejemplo es inventado y sirve para ver cómo responde el banco a un repaso casi bueno:

python
# Corrección, diagnóstico por tema con umbral del 80 % y días a repasar en Python:
def corregir(respuestas, banco = BANCO):
    # Una fila por pregunta con el acierto y la retroalimentación de la opción elegida:
    return pd.DataFrame([{'n': p['n'], 'tema': p['tema'], 'dia': p['dia'],
                          'elegida': respuestas.get(p['n'], '-'), 'correcta': p['correcta'],
                          'acierto': respuestas.get(p['n']) == p['correcta'],
                          'retro': p['retro'].get(respuestas.get(p['n']), 'Sin respuesta.')} for p in banco])

# Respuestas de un lector de ejemplo (inventadas para la demostración):
lector = {1: 'C', 2: 'B', 3: 'D', 4: 'A', 5: 'C', 6: 'B', 7: 'A', 8: 'D', 9: 'C', 10: 'A', 11: 'B', 12: 'B'}
resultado = corregir(lector)
aciertos = int(resultado['acierto'].sum())
print(f'Aciertos: {aciertos} de {len(resultado)} ({aciertos / len(resultado):.1%})')
for _, f in resultado[~resultado['acierto']].iterrows():
    print(f'Pregunta {f.n} (Día {f.dia}): eligió {f.elegida}, era {f.correcta}. {f.retro}')
por_tema = resultado.groupby('tema', sort = False)['acierto'].agg(['sum', 'count'])
por_tema['porcentaje'] = 100 * por_tema['sum'] / por_tema['count']
print(por_tema.to_string())
print('Temas bajo el umbral del 80 %:', por_tema.index[por_tema.porcentaje < 80].tolist(),
      '| días a repasar:', sorted(resultado.loc[~resultado.acierto, 'dia'].unique().tolist()))
print(f'P(10 o más aciertos al azar) = {stats.binom.sf(9, 12, 0.25):.2e} | media al azar: '
      f'{stats.binom.mean(12, 0.25):.0f} aciertos')

fig, ax = plt.subplots(figsize = (9, 4.2))
colores = ['#3a9d5d' if v >= 80 else '#eb6834' for v in por_tema['porcentaje']]
ax.barh(por_tema.index[::-1], por_tema['porcentaje'][::-1], color = colores[::-1], height = 0.55)
for i, (v, s, c) in enumerate(zip(por_tema['porcentaje'][::-1], por_tema['sum'][::-1], por_tema['count'][::-1])):
    ax.text(v - 2, i, f'{v:.0f} % ({s} de {c})', ha = 'right', va = 'center', color = 'white', fontsize = 11)
ax.axvline(80, color = '#52514e', linestyle = '--', linewidth = 1.5)
ax.text(81, 3.45, 'umbral 80 %', fontsize = 10, color = '#52514e')
ax.set_xlim(0, 108)
ax.set_ylim(-0.5, 3.75)
ax.set_xlabel('Aciertos (%)')
ax.set_title('Lector de ejemplo: puntuación por tema del módulo 5', fontsize = 15)
ax.grid(axis = 'x', color = '#e6e5e1')
ax.set_axisbelow(True)
plt.tight_layout()
plt.show()
Aciertos: 10 de 12 (83.3%)
Pregunta 10 (Día 53): eligió A, era D. Los percentiles no se promedian.
Pregunta 12 (Día 46): eligió B, era A. Trigo Lento casi no enviaba marketing.
                               sum  count  porcentaje
tema
Ventana y plantillas             3      3  100.000000
Consentimiento y orquestación    3      3  100.000000
Control humano                   3      3  100.000000
Medición y costes                1      3   33.333333
Temas bajo el umbral del 80 %: ['Medición y costes'] | días a repasar: [46, 53]
P(10 o más aciertos al azar) = 3.76e-05 | media al azar: 3 aciertos

Gráfica: Autocorrección en Python. Corrección, diagnóstico por tema con umbral del 80 % y días a repasar

El lector acierta $10$ de $12$ preguntas, un $83.3%$, y supera el umbral global, pero el diagnóstico por tema es más fino que el total: tres temas están al $100%$ y “Medición y costes” se queda en el $33%$, con dos fallos. En la pregunta 10 promedió los percentiles de dos canales, justo el error que el Día 53 desmontó, y en la pregunta 12 atribuyó la factura de Twilio a las tarifas de marketing, cuando el $85%$ era recargo por mensaje. La retroalimentación no dice solo “incorrecto”: dice por qué, y la salida termina con los días que conviene releer, el $46$ y el $53$.

La probabilidad de lograr $10$ aciertos o más al azar es de $3.76 \times 10^{-5}$, unas cuatro veces entre cien mil, y la media al azar es de $3$ aciertos: el resultado del lector refleja lo que sabe, y sus dos fallos también. La gráfica deja ver de un vistazo lo que el total esconde: tres barras verdes por encima de la línea del $80%$ y una barra naranja muy por debajo.

Un buen resultado global no autoriza a pasar de largo un tema débil. En la semana de pruebas, quien confunda percentiles promediará los tiempos de respuesta de los canales y le prometerá a Marisol un servicio que no existe; quien no sepa de dónde salían los $19.13$ USD de Twilio no reconocerá el siguiente recargo escondido. Por eso el diagnóstico se lee por tema y la recomendación se da por día.

Ver solución

Tabla 3. Respuestas correctas de las doce preguntas y el error típico de cada una.

#CorrectaPor quéError típico que conviene revisar
1CLa ventana se cerró a las 24 horas, pero el pedido en curso es base suficiente para una plantilla de utilidad aprobada.Pensar que con la ventana cerrada ya no se puede escribir nada, o lo contrario, que un texto libre “corto” no cuenta.
2BLa categoría la decide el contenido: un descuento convierte la plantilla en marketing, que cuesta 0.0397 USD en lugar de 0.0085 y exige consentimiento de marketing.Creer que la etiqueta que se elige al registrar la plantilla manda sobre lo que dice.
3DUn 400 por plantilla pausada no se arregla reintentando: se registra, se avisa en #alertas y el recordatorio sale con la plantilla de respaldo.Marcarlo como enviado, que es exactamente lo que escondió el incidente del Día 54.
4AEl consentimiento es por finalidad: quien aceptó confirmaciones no aceptó novedades.Pensar que el horario o “una sola vez” sustituyen al consentimiento.
5CLa baja se aplica en el mismo minuto a todas las finalidades, se confirma una vez y el registro se conserva.Pedir explicaciones o aplazar la baja.
6BEl orquestador calcula la huella del pedido y fusiona los eventos dentro de una ventana de 30 minutos; responde una sola vez.Responder en los dos canales, la respuesta doble que el Día 52 eliminó.
7AEl flujo no tiene nodo de envío: deja un borrador en el hilo y un hueco para que Valeria conteste la queja con sus palabras.Pensar que el modelo puede disculparse por la panadería.
8DLa firma se verifica sobre una marca de tiempo con tolerancia de 5 minutos; una petición vieja puede ser una repetición.Confiar en la firma sin mirar la marca de tiempo.
9CUna alergia es un dato de salud y una puerta humana desde el Día 6; las recetas no entran en ningún modelo.Dejar que el agente “consulte la receta”.
10DLos percentiles no se promedian: el p95 conjunto se calcula sobre todos los eventos juntos.Promediar 11.9 y 35.3.
11BB = (1 - 0.95) · 1 560 = 78 incumplimientos al mes.Confundir los eventos buenos (1 482) con el presupuesto.
12ATwilio cobraba 0.005 USD por cada mensaje recibido y enviado: 16.35 de los 19.13 USD, el 85.4 %.Culpar a las tarifas de Meta, que eran la parte pequeña.

Auditoría con el código de honor


El examen dice lo que sabe Valeria; la auditoría dice lo que hacen sus flujos. Desde el Día 6 (Ética y seguridad: el código de honor del arquitecto de automatización), ningún flujo de Trigo Lento entra en producción si no cumple los diez artículos del código de honor: humano al mando, consentimiento, minimización, transparencia, secretos, entradas verificadas, dato y no orden, registro sin datos sensibles, coste y caída, y lo que no sale (las recetas de Don Ernesto). Aquel día se comprobaron sobre una descripción del flujo escrita a mano. Hoy se comprueban sobre lo que de verdad se importaría en n8n: el JSON exportado de cuatro workflows del módulo.

La diferencia importa. Una descripción dice lo que su autor cree que hace el flujo; el JSON dice lo que hace. El siguiente código lee TL Gmail mayoristas v1 (Día 47), TL WhatsApp secuencias v1 (Día 48), TL Slack operaciones v1 (Día 50) y TL Orquestador v1 (Día 52) y aplica a cada uno una regla por artículo. Las reglas son deliberadamente sencillas y se pueden leer en una línea: si el flujo llama a la API de Claude, debe tener un nodo Seudonimizar y su instrucción de sistema debe decir que el texto del cliente es un dato; si escribe a clientes por la Cloud API, su consulta debe cruzar los consentimientos y sus plantillas deben identificarse como automáticas; si recibe un webhook, debe verificar una firma o una cabecera; ninguna ejecución correcta debe guardar sus datos; ningún error debe silenciarse sin un workflow de errores que avise. Un artículo que no se aplica (un flujo interno no escribe a clientes) se marca “no aplica”, no “cumple”:

python
# Auditoría automática del código de honor sobre los cuatro workflows exportados del módulo en Python:
RUTAS = {'TL Gmail mayoristas v1': 'dia_47/flujos/n8n/gmail_mayoristas_v1.json',
         'TL WhatsApp secuencias v1': 'dia_48/flujos/n8n/whatsapp_secuencias_v1.json',
         'TL Slack operaciones v1': 'dia_50/flujos/n8n/slack_operaciones_v1.json',
         'TL Orquestador v1': 'dia_52/flujos/n8n/orquestador_v1.json'}
PLANTILLAS = {p['name']: p for p in json.loads((proyecto / 'dia_48' / 'plantillas_trigo_lento.json')
                                              .read_text(encoding = 'utf-8'))}
# Plantillas que envía cada flujo según su máquina de estados (Día 48):
ENVIA = {'TL WhatsApp secuencias v1': ['bienvenida_novedades', 'bienvenida_favoritos']}
SECRETO = re.compile(r'(?i)(sk-ant-[\w-]{8,}|xox[bp]-[\w-]+|hooks\.slack\.com/services/T\w+|'
                     r'"bearer [\w.-]{12,}|"x-api-key"\s*:\s*"[^"=]{8,}")')

def auditar(nombre, wf, texto):
    nodos = wf['nodes']
    urls = [n['parameters'].get('url', '') for n in nodos]
    tipos = [n['type'].split('.')[-1] for n in nodos]
    a_clientes = any('graph.facebook.com' in u and u.endswith('/messages') for u in urls)
    usa_claude = any('api.anthropic.com' in u for u in urls)
    con_webhook = 'webhook' in tipos
    plantillas = ENVIA.get(nombre, [])
    silenciados = [n['name'] for n in nodos if n.get('onError') == 'continueRegularOutput' or
                   '"neverError": true' in json.dumps(n.get('parameters', {}))]
    r = {}
    r[1] = ('cumple' if not any('/send' in u for u in urls) and (not a_clientes or 'template' in texto)
            else 'pendiente')
    r[2] = ('no aplica' if not a_clientes and 'consentimientos' not in texto else
            'cumple' if 'consentimientos' in texto and ('baja' in texto) else 'pendiente')
    r[3] = ('no aplica' if not usa_claude else 'cumple' if 'Seudonimizar' in [n['name'] for n in nodos]
            else 'pendiente')
    pies = [c.get('text', '') for p in plantillas for c in PLANTILLAS.get(p, {}).get('components', [])
            if c['type'] == 'FOOTER']
    r[4] = ('no aplica' if not a_clientes else 'cumple' if pies and all('automático' in t for t in pies)
            else 'pendiente')
    r[5] = 'pendiente' if SECRETO.search(texto) else 'cumple'
    r[6] = ('no aplica' if not con_webhook else 'cumple' if ('headerAuth' in texto or 'createHmac' in texto)
            else 'pendiente')
    r[7] = ('no aplica' if not usa_claude else 'cumple' if 'dato, no una instrucción' in texto else 'pendiente')
    r[8] = 'cumple' if wf['settings'].get('saveDataSuccessExecution') == 'none' else 'pendiente'
    r[9] = 'cumple' if wf['settings'].get('errorWorkflow') and not silenciados else 'pendiente'
    r[10] = 'pendiente' if re.search(r'(?i)receta', texto) else 'cumple'
    return r, silenciados

ARTICULOS = ['Humano al mando', 'Consentimiento', 'Minimización', 'Transparencia', 'Secretos',
             'Entradas verificadas', 'Dato, no orden', 'Registro sin datos sensibles', 'Coste y caída',
             'Lo que no sale']
auditoria, notas = {}, {}
for nombre, ruta in RUTAS.items():
    texto = (proyecto / ruta).read_text(encoding = 'utf-8')
    wf = json.loads(texto)
    assert wf['name'] == nombre, f'El archivo {ruta} no es {nombre}'
    auditoria[nombre], notas[nombre] = auditar(nombre, wf, texto)
tabla_honor = pd.DataFrame(auditoria)
tabla_honor.index = [f'{i}. {a}' for i, a in zip(tabla_honor.index, ARTICULOS)]
print(tabla_honor.rename(columns = lambda c: c.replace('TL ', '').replace(' v1', '')).to_string())
for nombre in RUTAS:
    col = tabla_honor[nombre]
    print(f'{nombre}: cumple {int((col == "cumple").sum())} de {int((col != "no aplica").sum())} aplicables;'
          f' pendientes: {[a.split(". ")[1] for a in col.index[col == "pendiente"]] or "ninguno"}')
print('Nodos con errores silenciados:', {k: v for k, v in notas.items() if v} or 'ninguno')
print('Aceptados para producción sin cambios:', [n for n in RUTAS if (tabla_honor[n] != 'pendiente').all()])
                                Gmail mayoristas WhatsApp secuencias Slack operaciones Orquestador
1. Humano al mando                        cumple              cumple            cumple      cumple
2. Consentimiento                      no aplica              cumple         no aplica      cumple
3. Minimización                           cumple           no aplica         no aplica   no aplica
4. Transparencia                       no aplica           pendiente         no aplica   no aplica
5. Secretos                               cumple              cumple            cumple      cumple
6. Entradas verificadas                no aplica           no aplica            cumple      cumple
7. Dato, no orden                         cumple           no aplica         no aplica   no aplica
8. Registro sin datos sensibles           cumple           pendiente            cumple      cumple
9. Coste y caída                       pendiente           pendiente            cumple   pendiente
10. Lo que no sale                        cumple              cumple            cumple      cumple
TL Gmail mayoristas v1: cumple 6 de 7 aplicables; pendientes: ['Coste y caída']
TL WhatsApp secuencias v1: cumple 4 de 7 aplicables; pendientes: ['Transparencia', 'Registro sin datos sensibles', 'Coste y caída']
TL Slack operaciones v1: cumple 6 de 6 aplicables; pendientes: ninguno
TL Orquestador v1: cumple 6 de 7 aplicables; pendientes: ['Coste y caída']
Nodos con errores silenciados: {'TL WhatsApp secuencias v1': ['Enviar plantilla'], 'TL Orquestador v1': ['Registrar ejecución']}
Aceptados para producción sin cambios: ['TL Slack operaciones v1']

El resultado es mejor de lo que Valeria temía y peor de lo que le gustaría. Solo TL Slack operaciones v1 se acepta sin cambios, con $6$ de $6$ artículos aplicables. El correo de mayoristas y el orquestador cumplen $6$ de $7$; la secuencia de WhatsApp, $4$ de $7$. La Tabla 4 recoge los hallazgos con su corrección.

Tabla 4. Artículos pendientes del código de honor en los cuatro workflows auditados y su corrección.

FlujoArtículo pendienteQué encontró el códigoCorrección
TL WhatsApp secuencias v14. TransparenciaLas plantillas bienvenida_novedades y bienvenida_favoritos dicen cómo darse de baja, pero no que son mensajes automáticosPie “Mensaje automático de Trigo Lento. Para no recibir novedades, escriba BAJA”, con nueva aprobación de Meta
TL WhatsApp secuencias v18. Registro sin datos sensiblesSin saveDataSuccessExecution: none: n8n guarda cada ejecución correcta con el teléfono y el nombre del clienteFijar none como en los otros tres flujos
TL WhatsApp secuencias v19. Coste y caídaEl nodo Enviar plantilla usa neverError y el flujo no tiene workflow de erroresRama de error hacia #alertas y workflow de errores del Día 54
TL Gmail mayoristas v19. Coste y caídaSin workflow de errores: si Gmail o Claude fallan tres veces, nadie se enteraAsignar el workflow de errores
TL Orquestador v19. Coste y caídaRegistrar ejecución continúa ante error y no hay workflow de errores; sin registro, los indicadores del Día 53 se quedan ciegosWorkflow de errores y alerta si el registro falla

Los tres hallazgos de la secuencia cuentan una historia coherente con el Día 54 (Anatomía de un fallo real III: la plantilla pausada y los recordatorios que no salieron). El nodo que envía la plantilla nunca da error, por diseño, para que el flujo pueda leer el código HTTP y avanzar la secuencia; el problema es que nadie mira ese código fuera del propio flujo. Es el mismo patrón del incidente: un error convertido en dato y un dato que nadie lee. La versión 2 de la secuencia, construida ayer, corrigió el filtro de pedidos pendientes y el tratamiento de las plantillas pausadas; los tres artículos de hoy se revisan también sobre ella antes del lunes, porque una corrección puntual no garantiza que el resto del flujo cumpla.

El hallazgo de transparencia es el más fácil de pasar por alto. Las plantillas de utilidad del Día 48 llevan el pie “Mensaje automático de Trigo Lento”; las de marketing llevan “Para no recibir novedades, escriba BAJA”, que es obligatorio, pero no dicen que nadie las escribió en ese momento. El artículo 4 nació el Día 6 con una frase de Valeria a su padre: “Usted siempre dice que al cliente no se le engaña ni con el peso del pan.” Una línea más en el pie lo resuelve, a cambio de volver a enviar dos plantillas a revisión.

El artículo 9 aparece en tres de los cuatro flujos, y no es casualidad. Los flujos se construyeron en días distintos, cada uno con su propio manejo de errores, y solo el de Slack, que el Día 50 diseñó con la fatiga de alertas en mente, apunta a un workflow de errores. La corrección es la misma para los tres y cabe en una tarde: asignar en la configuración de cada workflow el manejador de errores que el Día 54 ya creó.

Atención aquí: Una auditoría automática solo encuentra lo que sus reglas saben buscar. Las de este bloque leen textos y configuraciones del JSON: no prueban que la firma se verifique bien, que la consulta de consentimientos sea correcta ni que la plantilla de respaldo esté aprobada. Un “cumple” significa “no se encontró el problema”, no “el flujo es seguro”. Las pruebas de punta a punta del módulo 8 y la revisión humana de cada cambio siguen siendo necesarias, y el código de honor se vuelve a pasar cada vez que un flujo cambia de versión.

Riesgos de la comunicación automatizada


El código de honor revisa flujo por flujo. Una matriz de riesgos mira el ecosistema entero y pregunta otra cosa: ¿qué puede salir mal entre los flujos, con los proveedores o con las personas, y cuánto daño haría? Valeria reúne nueve riesgos que el módulo ya encontró, uno por día, y les pone dos números del $1$ al $5$: la probabilidad de que ocurra en un mes y su impacto si ocurre (un $5$ es perder a la Cafetería Brújula o una sanción; un $1$, una molestia que se arregla con una disculpa). El riesgo de cada uno y la exposición del ecosistema son:

$$ R_j = P_j \cdot I_j, \qquad E = \sum_{j=1}^{9} R_j, \qquad \Delta E = 1 - \frac{E_{\text{después}}}{E_{\text{antes}}} $$

La lógica detrás de estas fórmulas es la siguiente:

  • $P_j$ e $I_j$ son la probabilidad y el impacto del riesgo $j$ en una escala ordinal de $1$ a $5$; su producto $R_j$ va de $1$ a $25$ y ordena los riesgos, pero no es una probabilidad ni un importe.
  • Un control casi siempre baja la probabilidad (la regla puede_enviar hace muy raro un mensaje fuera de ventana) y a veces el impacto (la plantilla de respaldo hace que una pausa cueste horas y no días); el riesgo residual es $R_j’ = P_j’ \cdot I_j’$.
  • $E$ suma los riesgos para comparar el antes y el después del módulo, y $\Delta E$ es la fracción de exposición que eliminaron los controles.

El siguiente código construye la matriz, calcula el riesgo antes y después de los controles y dibuja los dos mapas de calor, uno junto al otro:

python
# Matriz de riesgos de la comunicación antes y después de los controles, con mapa de calor, en Python:
riesgos = pd.DataFrame([
    ('R1', 'Mensaje libre fuera de ventana', 4, 3, 'puede_enviar antes de cada envío', 48, 1, 3),
    ('R2', 'Plantilla mal categorizada', 3, 3, 'Validador de plantillas', 48, 1, 3),
    ('R3', 'Borrador enviado sin revisar', 2, 5, 'Sin nodo de envío en Gmail', 47, 1, 5),
    ('R4', 'Alerta perdida o ignorada', 4, 4, 'Severidades y agrupación en #alertas', 50, 2, 3),
    ('R5', 'Respuesta doble entre canales', 4, 2, 'Huella y ventana de 30 min', 52, 2, 2),
    ('R6', 'Datos de clientes en Slack', 3, 4, 'Lista blanca de campos', 50, 1, 4),
    ('R7', 'Cambio de tarifas de Meta', 3, 2, 'Taxímetro y revisión trimestral', 46, 3, 1),
    ('R8', 'Plantilla pausada sin aviso', 3, 4, 'Webhook de estado y respaldo', 54, 2, 2),
    ('R9', 'Traducción que cambia el sentido', 2, 3, 'Glosario y revisión humana', 49, 1, 3),
], columns = ['id', 'riesgo', 'p', 'i', 'control', 'dia', 'p_res', 'i_res'])
riesgos['antes'] = riesgos.p * riesgos.i
riesgos['despues'] = riesgos.p_res * riesgos.i_res
riesgos['reduccion'] = 1 - riesgos.despues / riesgos.antes
print(riesgos[['id', 'riesgo', 'p', 'i', 'antes', 'p_res', 'i_res', 'despues', 'dia']].to_string(index = False))
print(f'Exposición total: {riesgos.antes.sum()} antes y {riesgos.despues.sum()} después '
      f'({1 - riesgos.despues.sum() / riesgos.antes.sum():.1%} menos)')
print('Riesgos que siguen en 6 o más:', riesgos.loc[riesgos.despues >= 6, 'id'].tolist(),
      '| el de mayor riesgo residual:', riesgos.loc[riesgos.despues.idxmax(), 'riesgo'])

fig, ejes = plt.subplots(1, 2, figsize = (13, 5.6), sharey = True)
fondo = np.add.outer(np.arange(1, 6), np.arange(1, 6))
for ax, (titulo, pc, ic) in zip(ejes, [('Antes de los controles', 'p', 'i'),
                                        ('Después de los controles', 'p_res', 'i_res')]):
    malla = np.outer(np.arange(1, 6), np.arange(1, 6))
    ax.imshow(malla, origin = 'lower', cmap = 'YlOrRd', vmin = 1, vmax = 25, extent = (0.5, 5.5, 0.5, 5.5))
    celdas = riesgos.groupby([pc, ic])['id'].apply(list)
    for (p, i), ids in celdas.items():
        ax.text(i, p, '\n'.join([' '.join(ids[k:k + 2]) for k in range(0, len(ids), 2)]), ha = 'center',
                va = 'center', fontsize = 11, fontweight = 'bold', color = '#1f1f1f')
    ax.set_xticks(range(1, 6))
    ax.set_yticks(range(1, 6))
    ax.set_xlabel('Impacto (1 a 5)')
    ax.set_title(f'{titulo}: exposición {riesgos[pc].mul(riesgos[ic]).sum()}', fontsize = 13)
ejes[0].set_ylabel('Probabilidad (1 a 5)')
fig.suptitle('Riesgos de la comunicación automatizada de Trigo Lento', fontsize = 15)
plt.tight_layout()
plt.show()
id                           riesgo  p  i  antes  p_res  i_res  despues  dia
R1   Mensaje libre fuera de ventana  4  3     12      1      3        3   48
R2       Plantilla mal categorizada  3  3      9      1      3        3   48
R3     Borrador enviado sin revisar  2  5     10      1      5        5   47
R4        Alerta perdida o ignorada  4  4     16      2      3        6   50
R5    Respuesta doble entre canales  4  2      8      2      2        4   52
R6       Datos de clientes en Slack  3  4     12      1      4        4   50
R7        Cambio de tarifas de Meta  3  2      6      3      1        3   46
R8      Plantilla pausada sin aviso  3  4     12      2      2        4   54
R9 Traducción que cambia el sentido  2  3      6      1      3        3   49
Exposición total: 91 antes y 35 después (61.5% menos)
Riesgos que siguen en 6 o más: ['R4'] | el de mayor riesgo residual: Alerta perdida o ignorada

Gráfica: Riesgos de la comunicación automatizada. Matriz de riesgos de la comunicación antes y después de los controles, con mapa de calor

La exposición baja de $91$ a $35$ puntos, un $61.5%$ menos, y la Tabla 5 muestra cada riesgo con su control y el día que lo construyó.

Tabla 5. Matriz de riesgos de la comunicación de Trigo Lento: probabilidad, impacto, control y día que lo resuelve.

Id.RiesgoP × I antesControlP × I despuésDía
R1Mensaje libre fuera de la ventana de 24 horas4 × 3 = 12puede_enviar antes de cada envío1 × 3 = 348
R2Plantilla mal categorizada3 × 3 = 9Validador de plantillas antes de enviarlas a Meta1 × 3 = 348
R3Borrador de correo enviado sin revisar2 × 5 = 10Ningún nodo de envío en TL Gmail mayoristas v11 × 5 = 547
R4Alerta perdida o ignorada4 × 4 = 16Severidades, agrupación por sesiones y #alertas2 × 3 = 650
R5Respuesta doble entre canales4 × 2 = 8Huella del pedido y ventana de 30 minutos2 × 2 = 452
R6Datos de clientes en Slack3 × 4 = 12Lista blanca de campos antes de publicar1 × 4 = 450
R7Cambio de tarifas de Meta3 × 2 = 6Taxímetro de los webhooks y revisión trimestral3 × 1 = 346
R8Plantilla pausada sin aviso3 × 4 = 12Webhook de estado, alerta y plantilla de respaldo2 × 2 = 454
R9Traducción que cambia el sentido2 × 3 = 6Glosario del pan y revisión humana1 × 3 = 349

Tres lecturas salen de la tabla y de la gráfica. La primera: los controles bajan casi siempre la probabilidad y casi nunca el impacto. Un borrador enviado sin revisar a Marisol seguiría siendo un $5$; lo que cambió es que el flujo no puede enviarlo. Por eso en el mapa de la derecha los riesgos se apilan en la fila de abajo, pero siguen repartidos en horizontal.

La segunda: el cambio de tarifas de Meta, R7, es el único que no baja de probabilidad. Meta revisa sus tarifas cada trimestre y Trigo Lento no lo controla; lo único que puede hacer es enterarse el mismo día, con el taxímetro del Día 46, y tener reserva para absorberlo. Es el ejemplo de un riesgo que se acepta y se vigila, no que se elimina.

La tercera: el riesgo residual más alto es R4, la alerta perdida o ignorada, con $6$ puntos. No es casualidad que coincida con el artículo 9 de la auditoría: tres flujos no tienen todavía un workflow de errores, y el incidente del Día 54 fue, en el fondo, una alerta que nadie podía ver. Ese es el primer punto de la lista del lunes.

El coste consolidado


El módulo empezó con una cifra subrayada en rojo, los $19.13$ USD de WhatsApp por Twilio frente a un rubro de $12$, y cada día añadió una línea al gasto. Hoy se suman todas. El coste mensual del ecosistema y el margen que queda son:

$$ C = C_{\text{WA}} + \sum_{f} C_{\text{Claude},f} + C_{\text{Make}} + C_{\text{servidor}} + C_{\text{Slack}}, \qquad M = 45 - C $$

La lógica detrás de esta suma es la siguiente:

  • $C_{\text{WA}}$ es lo que Meta cobra por la Cloud API directa: las respuestas y los recordatorios del Día 46 en el régimen de octubre de 2026 ($6.25$ USD) más la secuencia de bienvenida del Día 48 ($3.18$).
  • $\sum_f C_{\text{Claude},f}$ suma el gasto de Claude de cada flujo $f$ que lo usa en la comunicación: la cascada de clasificación del Día 30, el agente de pedidos del Día 38, la clasificación y los borradores de Gmail del Día 47, el chatbot del Día 48 y la traducción al inglés del Día 49.
  • $C_{\text{Make}}$ y $C_{\text{servidor}}$ son fijos ($10.59$ y $6.00$ USD), y $C_{\text{Slack}} = 0$ porque Trigo Lento vive en el plan gratuito; $M$ es lo que queda libre de los $45$ USD.

El siguiente código hace la suma con las cifras de cada día, la compara con los rubros del presupuesto revisado el Día 46 y dibuja el reparto:

python
# Coste mensual consolidado de la comunicación frente a los 45 USD en Python:
claude = pd.Series({'Cascada de clasificación (Día 30)': 1.47, 'Agente de pedidos (Día 38)': 2.88,
                    'Gmail: Haiku y Sonnet (Día 47)': 0.039 + 1.43, 'Chatbot: Haiku (Día 48)': 0.0254,
                    'Traducción al inglés (Día 49)': 0.33})
whatsapp = pd.Series({'Respuestas y recordatorios (Día 46)': 6.25, 'Secuencia de bienvenida (Día 48)': 3.18})
coste = pd.Series({'WhatsApp (Cloud API)': whatsapp.sum(), 'API de Claude': claude.sum(), 'Make Core': 10.59,
                   'Servidor n8n': 6.00, 'Slack (plan gratuito)': 0.00})
rubro = pd.Series({'WhatsApp (Cloud API)': 12.00, 'API de Claude': 8.00, 'Make Core': 10.59,
                   'Servidor n8n': 6.00, 'Slack (plan gratuito)': 0.00})
tabla3 = pd.DataFrame({'gasto': coste.round(2), 'rubro': rubro})
tabla3['uso_del_rubro'] = (tabla3.gasto / tabla3.rubro).map(lambda v: f'{v:.0%}' if np.isfinite(v) else '-')
print(claude.round(4).to_string())
print(tabla3.to_string())
total = coste.sum()
print(f'Total: {total:.2f} USD ({total * MXN_POR_USD:.2f} MXN) | libre: {PRESUPUESTO - total:.2f} USD '
      f'({(PRESUPUESTO - total) / PRESUPUESTO:.1%} del presupuesto)')
# Escenario de volumen de diseño del Día 46 (todo cliente responde y todo encargo lleva recordatorio):
diseno = total - whatsapp.iloc[0] + 8.74
print(f'Con el volumen de diseño: WhatsApp {8.74 + whatsapp.iloc[1]:.2f} USD de 12 y total {diseno:.2f} USD '
      f'(libre {PRESUPUESTO - diseno:.2f})')

fig, ax = plt.subplots(figsize = (11, 3.6))
colores = ['#3a9d5d', '#7a5cc9', '#2a78d6', '#52514e']
izquierda = 0
for (nombre, v), color in zip(coste[coste > 0].items(), colores):
    ax.barh(0, v, left = izquierda, color = color, edgecolor = 'white', label = nombre)
    ax.text(izquierda + v / 2, 0, f'{v:.2f}', ha = 'center', va = 'center', color = 'white', fontsize = 11)
    izquierda += v
ax.barh(0, PRESUPUESTO - total, left = total, color = '#e6e5e1', edgecolor = 'white', label = 'Libre')
ax.text(total + (PRESUPUESTO - total) / 2, 0, f'libre {PRESUPUESTO - total:.2f}', ha = 'center', va = 'center',
        fontsize = 11, color = '#1f1f1f')
ax.axvline(PRESUPUESTO, color = '#c8423b', linestyle = '--', linewidth = 1.5)
ax.axvline(diseno, color = '#52514e', linestyle = ':', linewidth = 1.5)
ax.text(diseno, 0.56, f'volumen de diseño: {diseno:.2f}', ha = 'center', fontsize = 10, color = '#52514e')
ax.set_yticks([])
ax.set_xlim(0, 47)
ax.set_ylim(-0.5, 0.8)
ax.set_xlabel('USD al mes')
ax.set_title(f'Gasto mensual por rubro: {total:.2f} de {PRESUPUESTO:.0f} USD', fontsize = 15)
ax.legend(loc = 'upper center', bbox_to_anchor = (0.5, -0.32), ncol = 5, frameon = False)
plt.tight_layout()
plt.show()
Cascada de clasificación (Día 30)    1.4700
Agente de pedidos (Día 38)           2.8800
Gmail: Haiku y Sonnet (Día 47)       1.4690
Chatbot: Haiku (Día 48)              0.0254
Traducción al inglés (Día 49)        0.3300
                       gasto  rubro uso_del_rubro
WhatsApp (Cloud API)    9.43  12.00           79%
API de Claude           6.17   8.00           77%
Make Core              10.59  10.59          100%
Servidor n8n            6.00   6.00          100%
Slack (plan gratuito)   0.00   0.00             -
Total: 32.19 USD (595.60 MXN) | libre: 12.81 USD (28.5% del presupuesto)
Con el volumen de diseño: WhatsApp 11.92 USD de 12 y total 34.68 USD (libre 10.32)

Gráfica: El coste consolidado. Coste mensual consolidado de la comunicación frente a los 45 USD

El ecosistema de comunicación cuesta $32.19$ USD al mes ($595.60$ MXN al tipo didáctico) y deja $12.81$ USD libres, el $28.5%$ del presupuesto. La Tabla 6 lo detalla.

Tabla 6. Coste mensual consolidado de la comunicación de Trigo Lento frente a su rubro (USD, revisado en octubre de 2026).

RubroDesgloseGastoRubro asignadoUso
WhatsApp (Cloud API)Respuestas y recordatorios 6.25 (Día 46) + secuencia de bienvenida 3.18 (Día 48)9.4312.0079 %
API de ClaudeCascada 1.47 (Día 30) + agente 2.88 (Día 38) + Gmail 1.47 (Día 47) + chatbot 0.03 (Día 48) + traducción 0.33 (Día 49)6.178.0077 %
Make CorePlan mensual10.5910.59100 %
Servidor n8nVPS pequeño (supuesto de la serie)6.006.00100 %
SlackPlan gratuito0.000.00-
Total32.1936.59Libre: 12.81 de 45

Ningún rubro variable se rompe. WhatsApp usa el $79%$ del suyo y Claude el $77%$, y en Claude manda el agente de pedidos: sus $2.88$ USD son casi la mitad de todo el gasto en el modelo, porque cada conversación arrastra su historial. Los dos rubros fijos, Make y el servidor, suman $16.59$ USD y son ya más de la mitad del gasto, una proporción que el Día 84 tendrá que revisar, porque Make se mantiene para los escenarios que no se migraron el Día 45.

La Figura 2 pone el mismo reparto en una balanza, con los cuatro rubros en un platillo y los $45$ USD en el otro.

La balanza del presupuesto: WhatsApp 9.43, Claude 6.17, Make 10.59 y servidor 6.00 USD suman 32.19; con 12.81 libres equilibran los 45 USD del otro platillo. Con el volumen de diseño el gasto sube a 34.68.

Figura 2. La balanza del presupuesto: WhatsApp 9.43, Claude 6.17, Make 10.59 y servidor 6.00 USD suman 32.19; con 12.81 libres equilibran los 45 USD del otro platillo. Con el volumen de diseño el gasto sube a 34.68.

La línea punteada de la gráfica responde a una pregunta prudente: ¿y si todo crece? Con el volumen de diseño del Día 46, en el que todo cliente responde y todo encargo lleva recordatorio, WhatsApp sube a $11.92$ USD, a ocho centavos de su rubro, y el total a $34.68$, con $10.32$ libres. El presupuesto aguanta, pero WhatsApp queda sin holgura: cualquier difusión de marketing, como ya advirtió el Día 48, rompería el rubro. Por eso la regla se mantiene: el pan de temporada se anuncia por Instagram.

Don Ernesto lee la cifra y hace la cuenta a su manera: “Treinta y dos dólares son unos seiscientos pesos. Lo que vendemos en dos pasteles de chocolate.” No es una mala manera de verlo. El Día 1 calculó en $46.8$ horas al mes el tiempo que Valeria dedicaba a los encargos; a $75$ MXN la hora, son $3,510$ MXN, casi seis veces el gasto mensual de toda la comunicación automatizada.

Bajas y consentimientos antes del fin de semana


Un repaso del módulo de comunicación no está completo si solo mira flujos y dinero. La regla del módulo dice que ningún mensaje sale sin base registrada, y esa base cambia todos los días: hay clientes que piden la baja, consentimientos que envejecen y personas que ya no recuerdan haberse suscrito. Antes de una semana en la que todos los flujos funcionarán a la vez, Valeria hace dos comprobaciones en la base de datos.

La primera busca bajas pendientes: clientes que escribieron “BAJA” o “ya no” esta semana y que, sin embargo, siguen con un consentimiento activo. Debería ser cero, porque el orquestador del Día 52 aplica la baja en el mismo minuto, pero un mensaje que entró por un canal mal normalizado o un registro que falló en silencio (el artículo 9 de la auditoría) basta para que alguien siga recibiendo lo que pidió no recibir. La segunda busca consentimientos de marketing con más de doce meses sin renovar. Ninguna ley mexicana fija ese plazo; es una regla interna de Trigo Lento, que prefiere preguntar otra vez a seguir escribiendo a quien quizá ya cambió de opinión.

El siguiente código reproduce la revisión en una base SQLite en memoria con $320$ consentimientos inventados y siete palabras de baja recibidas esta semana, de las cuales el orquestador aplicó cinco:

python
# Revisión de bajas pendientes y consentimientos caducados en SQLite antes del fin de semana en Python:
con = sqlite3.connect(':memory:')
con.executescript('''
CREATE TABLE consentimientos (cliente_id TEXT, finalidad TEXT, canal TEXT, otorgado_en TEXT, baja_en TEXT);
CREATE TABLE mensajes_entrantes (cliente_id TEXT, recibido_en TEXT, texto_normalizado TEXT);''')
hoy = datetime(2026, 3, 27, 9, 0)
filas = []
for k in range(1, 321):
    cid = f'C{k:04d}'
    otorgado = hoy - timedelta(days = int(rng.integers(3, 520)))
    finalidad = 'marketing' if k % 3 == 0 else 'confirmaciones'
    baja = (otorgado + timedelta(days = int(rng.integers(1, 60)))).isoformat() if rng.random() < 0.06 else None
    filas.append((cid, finalidad, 'whatsapp', otorgado.isoformat(), baja))
con.executemany('INSERT INTO consentimientos VALUES (?, ?, ?, ?, ?)', filas)
# Palabras de baja recibidas esta semana (seudonimizadas; el texto ya llega normalizado a la palabra clave):
activos = [f[0] for f in filas if f[4] is None]
bajas_recibidas = [(c, (hoy - timedelta(hours = int(h))).isoformat(), 'BAJA')
                   for c, h in zip(rng.choice(activos, 7, replace = False), rng.integers(2, 120, 7))]
con.executemany('INSERT INTO mensajes_entrantes VALUES (?, ?, ?)', bajas_recibidas)
con.executemany('UPDATE consentimientos SET baja_en = ? WHERE cliente_id = ?',
                [(t, c) for c, t, _ in bajas_recibidas[:5]])     # cinco ya aplicadas por el orquestador
pendientes = con.execute('''
SELECT m.cliente_id, m.recibido_en FROM mensajes_entrantes m JOIN consentimientos c USING (cliente_id)
WHERE m.texto_normalizado = 'BAJA' AND c.baja_en IS NULL ORDER BY m.recibido_en''').fetchall()
caducados = con.execute('''
SELECT COUNT(*) FROM consentimientos WHERE finalidad = 'marketing' AND baja_en IS NULL
AND julianday(?) - julianday(otorgado_en) > 365''', (hoy.isoformat(),)).fetchone()[0]
vigentes = con.execute('''SELECT finalidad, COUNT(*) FROM consentimientos WHERE baja_en IS NULL
GROUP BY finalidad''').fetchall()
print('Consentimientos vigentes por finalidad:', dict(vigentes))
print('Bajas recibidas esta semana:', len(bajas_recibidas), '| sin aplicar:', len(pendientes))
for c, t in pendientes:
    print(f'  {c} pidió la baja el {t[:16].replace("T", " ")} y sigue activo')
print('Consentimientos de marketing con más de 12 meses sin renovar:', caducados)
con.close()
Consentimientos vigentes por finalidad: {'confirmaciones': 196, 'marketing': 101}
Bajas recibidas esta semana: 7 | sin aplicar: 2
  C0064 pidió la baja el 2026-03-25 13:00 y sigue activo
  C0167 pidió la baja el 2026-03-26 19:00 y sigue activo
Consentimientos de marketing con más de 12 meses sin renovar: 32

Hay $297$ consentimientos vigentes: $196$ de confirmaciones y $101$ de marketing. De las $7$ bajas recibidas esta semana, $2$ siguen sin aplicar: la del cliente C0064, del miércoles 25 a las 13:00, y la de C0167, del jueves 26 a las 19:00. La simulación no dice por qué fallaron; en la realidad, el primer sospechoso es el nodo Registrar ejecución del orquestador, que continúa ante error sin avisar a nadie. Valeria las aplica a mano en ese momento, antes de cerrar el cuaderno, y anota la causa en la lista del lunes junto al artículo 9.

La segunda comprobación encuentra $32$ consentimientos de marketing con más de doce meses. No se les escribe más novedades hasta renovar el permiso, y la renovación tampoco puede hacerse con una plantilla de marketing a quien ya no tiene un consentimiento vigente para ella: se les preguntará en el mostrador o la próxima vez que escriban ellos, cuando la ventana esté abierta. Es más lento, pero es la única forma de no convertir una renovación en spam.

Lo que dijo Don Ernesto


Queda la pregunta con la que empezó el día: quién le contesta a quién. La respuesta no es una frase, es una matriz. Valeria clasifica los $2,330$ eventos del mes por tipo de mensaje y asigna a cada tipo una de cinco maneras de contestar: la máquina sola (respuestas fijas, acuses, bajas), la máquina con aviso al equipo (el encargo sencillo que se confirma y aparece en #pedidos-del-dia), la máquina con aprobación previa de una persona (pasteles y cambios de pedido), el borrador y persona (el modelo propone y una persona revisa y firma) y solo persona (quejas, alergias y lo que no encaja en nada). Los volúmenes por tipo son supuestos didácticos que suman los eventos del Día 52:

python
# Matriz "quién contesta qué" por tipo de mensaje y volumen mensual en Python:
quien = pd.DataFrame([
    ('Horario, precios y ubicación', 620, 'Máquina sola', 'Chatbot con reglas y botones'),
    ('Fuera de horario', 380, 'Máquina sola', 'Acuse automático y cola'),
    ('Baja ("BAJA", "ya no")', 30, 'Máquina sola', 'Baja inmediata'),
    ('Correo informativo (molino, boletín)', 170, 'Máquina sola', 'Etiqueta en Gmail, sin respuesta'),
    ('Encargo sencillo', 420, 'Máquina con aviso', 'Agente de pedidos y #pedidos-del-dia'),
    ('Pastel por encargo', 40, 'Máquina con aprobación', 'Agente y #aprobaciones'),
    ('Cambio de pedido', 120, 'Máquina con aprobación', 'Agente y Valeria o Don Ernesto'),
    ('Correo que pide respuesta', 130, 'Borrador y persona', 'Gmail: Valeria revisa y envía'),
    ('Mensaje libre en inglés', 90, 'Borrador y persona', 'Sonnet propone y una persona revisa'),
    ('Queja', 60, 'Solo persona', 'Valeria contesta y firma'),
    ('Alergia o ingrediente', 40, 'Solo persona', 'Valeria consulta a Don Ernesto'),
    ('Otros o ambiguos', 230, 'Solo persona', 'Mostrador o Valeria'),
], columns = ['tipo', 'al_mes', 'quien_contesta', 'como'])
assert quien.al_mes.sum() == sum(EVENTOS_MES.values()), 'La matriz no suma los 2 330 eventos'
resumen = quien.groupby('quien_contesta', sort = False)['al_mes'].sum().to_frame()
resumen['porcentaje'] = (100 * resumen.al_mes / resumen.al_mes.sum()).round(1)
print(quien[['tipo', 'al_mes', 'quien_contesta']].to_string(index = False))
print(resumen.to_string())
persona = resumen.loc[['Borrador y persona', 'Solo persona'], 'al_mes'].sum()
print(f'Mensajes que firma o decide una persona: {persona} de {quien.al_mes.sum()} '
      f'({persona / quien.al_mes.sum():.1%}); con aprobación previa: '
      f'{resumen.loc["Máquina con aprobación", "al_mes"]}; sin ninguna persona: '
      f'{resumen.loc["Máquina sola", "al_mes"]}')
                                tipo  al_mes         quien_contesta
        Horario, precios y ubicación     620           Máquina sola
                    Fuera de horario     380           Máquina sola
              Baja ("BAJA", "ya no")      30           Máquina sola
Correo informativo (molino, boletín)     170           Máquina sola
                    Encargo sencillo     420      Máquina con aviso
                  Pastel por encargo      40 Máquina con aprobación
                    Cambio de pedido     120 Máquina con aprobación
           Correo que pide respuesta     130     Borrador y persona
             Mensaje libre en inglés      90     Borrador y persona
                               Queja      60           Solo persona
               Alergia o ingrediente      40           Solo persona
                    Otros o ambiguos     230           Solo persona
                        al_mes  porcentaje
quien_contesta
Máquina sola              1200        51.5
Máquina con aviso          420        18.0
Máquina con aprobación     160         6.9
Borrador y persona         220         9.4
Solo persona               330        14.2
Mensajes que firma o decide una persona: 550 de 2330 (23.6%); con aprobación previa: 160; sin ninguna persona: 1200

La matriz responde a Don Ernesto con cifras. De $2,330$ mensajes al mes, $1,200$ los contesta la máquina sola ($51.5%$), y todos son mensajes sin decisión: el horario, los precios, el acuse fuera de horario, las bajas y los correos informativos del molino. Otros $420$, los encargos sencillos, los confirma el agente y avisa al equipo. En $550$ mensajes ($23.6%$) una persona firma o decide, y en otros $160$ aprueba antes de que el cliente reciba nada. La Tabla 7 resume la matriz.

Tabla 7. Quién contesta qué en Trigo Lento: tipos de mensaje, volumen mensual y forma de respuesta.

Tipo de mensajeAl mesQuién contestaCómo
Horario, precios y ubicación620Máquina solaChatbot con reglas y botones
Fuera de horario380Máquina solaAcuse automático y cola
Baja (“BAJA”, “ya no”)30Máquina solaBaja inmediata en todas las finalidades
Correo informativo (molino, boletín)170Máquina solaEtiqueta en Gmail, sin respuesta
Encargo sencillo420Máquina con avisoAgente de pedidos y #pedidos-del-dia
Pastel por encargo40Máquina con aprobaciónAgente y #aprobaciones
Cambio de pedido120Máquina con aprobaciónAgente y Valeria, o Don Ernesto a los 20 minutos
Correo que pide respuesta130Borrador y personaGmail: Valeria revisa y envía
Mensaje libre en inglés90Borrador y personaSonnet propone y una persona revisa
Queja60Solo personaValeria contesta y firma
Alergia o ingrediente40Solo personaValeria consulta a Don Ernesto
Otros o ambiguos230Solo personaMostrador o Valeria

Valeria le imprime la tabla y se la deja junto a la báscula. Don Ernesto la lee de arriba abajo, despacio, como lee las facturas del molino. Se detiene en la fila de las alergias y asiente; se detiene en la de las quejas y levanta la vista: “O sea que si una clienta se queja, le contesta usted, no el aparato.” Valeria le dice que sí, y que si la queja es de un pastel, el agente ni siquiera lo intenta. Don Ernesto dobla la hoja en cuatro y se la guarda en la bolsa del delantal: “Ahora sí sé quién contesta qué. Y si un día la máquina contesta algo que no está en esta hoja, me avisa, que la hoja se cambia antes que el pan.”

Esa última frase es, en el fondo, el artículo 1 del código de honor dicho por un panadero: la regla escrita va primero y la automatización la obedece. Para que Don Ernesto la reciba cada lunes sin abrir Slack, el resumen de la semana se puede redactar con Claude a partir de las cifras agregadas de esta entrada, nunca de los mensajes de los clientes. El siguiente bloque llama a Claude Opus 5.5, el modelo predeterminado de la serie, con un esfuerzo bajo, porque la tarea es de redacción breve sobre datos ya calculados, y pide la respuesta con un esquema JSON fijo. Después comprueba que cada cifra del resumen existe en la entrada; si el modelo inventa un número, se ve:

python
# Resumen de una página para Don Ernesto con Claude Opus 5.5 a partir de cifras agregadas en Python (bloque API, no se ejecuta aquí):
import json
import re
import anthropic

# Solo cifras agregadas de los bloques 6, 7, 8, 9 y 10: ni nombres, ni teléfonos, ni textos de clientes:
cifras = {
    'flujos_auditados': 4, 'aceptados_sin_cambios': ['TL Slack operaciones v1'],
    'pendientes': {'TL WhatsApp secuencias v1': ['Transparencia', 'Registro sin datos sensibles', 'Coste y caída'],
                   'TL Gmail mayoristas v1': ['Coste y caída'], 'TL Orquestador v1': ['Coste y caída']},
    'exposicion_riesgo': {'antes': 91, 'despues': 35, 'mayor_residual': 'Alerta perdida o ignorada'},
    'coste_usd': {'whatsapp': 9.43, 'claude': 6.17, 'make': 10.59, 'servidor': 6.00, 'total': 32.19, 'libre': 12.81},
    'bajas_sin_aplicar': 2, 'consentimientos_marketing_vencidos': 32,
    'quien_contesta': {'maquina_sola': 1200, 'maquina_con_aviso': 420, 'con_aprobacion': 160,
                       'borrador_y_persona': 220, 'solo_persona': 330},
}
ESQUEMA = {'type': 'object', 'additionalProperties': False,
           'required': ['titular', 'lo_que_ya_funciona', 'lo_que_falta', 'decision_para_el_lunes'],
           'properties': {'titular': {'type': 'string'},
                          'lo_que_ya_funciona': {'type': 'array', 'items': {'type': 'string'}},
                          'lo_que_falta': {'type': 'array', 'items': {'type': 'string'}},
                          'decision_para_el_lunes': {'type': 'string'}}}
SISTEMA = ('Usted escribe para Don Ernesto, dueño de la panadería Trigo Lento, un resumen semanal de una página. '
           'Escriba en español, de usted, con frases cortas y sin palabras técnicas en inglés. Use solo las cifras '
           'del bloque <cifras>; no invente ninguna y no redondee los importes. Diga primero quién contesta qué, '
           'después lo que falta y termine con una sola decisión que él pueda tomar el lunes.')

cliente = anthropic.Anthropic()   # lee ANTHROPIC_API_KEY del entorno
respuesta = cliente.beta.messages.create(
    model = 'claude-opus-5-5',
    max_tokens = 1500,
    betas = ['server-side-fallback-2026-07-01'],
    fallbacks = 'default',                      # respaldo del lado del servidor ante un rechazo
    output_config = {'effort': 'low',            # sin temperature ni top_p
                     'format': {'type': 'json_schema', 'schema': ESQUEMA}},
    system = SISTEMA,
    messages = [{'role': 'user', 'content': f'<cifras>{json.dumps(cifras, ensure_ascii = False)}</cifras>'}],
)
if respuesta.stop_reason != 'end_turn':
    raise SystemExit(f'Sin resumen ({respuesta.stop_reason}): Valeria lo escribe a mano')
resumen = json.loads(next(b.text for b in respuesta.content if b.type == 'text'))
# Comprobación barata: cada cifra del resumen debe existir en las cifras de entrada:
permitidas = set(re.findall(r'\d+(?:\.\d+)?', json.dumps(cifras)))
inventadas = set(re.findall(r'\d+(?:\.\d+)?', json.dumps(resumen, ensure_ascii = False))) - permitidas
print(json.dumps(resumen, ensure_ascii = False, indent = 2))
print('Cifras que no estaban en la entrada:', sorted(inventadas) or 'ninguna')
print('Tokens:', respuesta.usage.input_tokens, 'de entrada y', respuesta.usage.output_tokens, 'de salida')

# Salida de ejemplo (puede variar):
# {
#   "titular": "La máquina contesta sola 1200 mensajes al mes; 330 los atiende solo una persona.",
#   "lo_que_ya_funciona": ["Slack de operaciones cumple todo el código de honor.",
#                          "El riesgo baja de 91 a 35 puntos con los controles.",
#                          "La comunicación cuesta 32.19 USD y quedan 12.81 libres."],
#   "lo_que_falta": ["Aplicar 2 bajas que siguen sin aplicar.",
#                    "Pedir de nuevo el permiso a 32 clientes de novedades.",
#                    "Que los errores de tres flujos avisen en Slack."],
#   "decision_para_el_lunes": "Autorizar que las novedades digan que son mensajes automáticos."
# }
# Cifras que no estaban en la entrada: ninguna
# Tokens: 518 de entrada y 214 de salida

El bloque no se ejecutó en esta entrada, y la salida es un ejemplo de lo que puede devolver. Su diseño, sin embargo, es lo importante: el modelo recibe solo cifras agregadas, ningún nombre ni teléfono; responde con un esquema que obliga a separar lo que funciona de lo que falta; y la comprobación final rechaza cualquier número que no estuviera en la entrada. Con unos $520$ tokens de entrada y $210$ de salida, a $4$ y $20$ USD por millón, cada resumen cuesta unos $0.006$ USD y los de un mes, menos de tres centavos: no cambia la Tabla 6. Valeria lee el resumen antes de imprimirlo, como lee cada borrador de Gmail.

La Figura 3 lleva la matriz a un dibujo que cabe en la pared del obrador: cada columna es una manera de contestar, con sus tipos de mensaje debajo, y la altura de cada barra es su volumen.

Quién contesta qué: de 2 330 mensajes al mes, la máquina contesta sola 1 200, avisa al equipo en 420, pide aprobación en 160, deja un borrador a una persona en 220 y 330 los atiende solo una persona.

Figura 3. Quién contesta qué: de 2 330 mensajes al mes, la máquina contesta sola 1 200, avisa al equipo en 420, pide aprobación en 160, deja un borrador a una persona en 220 y 330 los atiende solo una persona.

Consideraciones finales


El día empezó con la pregunta de un panadero que ya no sabía quién le contestaba a sus clientes y termina con una hoja doblada en la bolsa de su delantal. Entre una y otra, el repaso hizo cuatro cosas que ningún flujo hace solo: contar las piezas (ocho flujos, un solo dueño), comprobar el criterio con doce decisiones (el lector de ejemplo aprueba con $83.3%$ y se lleva dos días para repasar), auditar el código real (un flujo aceptado sin cambios, cinco correcciones pendientes y un patrón repetido de errores que nadie ve) y poner números al riesgo y al dinero ($91$ puntos de exposición que bajan a $35$, y $32.19$ USD de $45$).

Las cifras de hoy tienen límites que conviene decir. Las probabilidades y los impactos de la matriz son juicios de Valeria en una escala ordinal, no mediciones; el producto ordena los riesgos, pero un $12$ no es “el doble de malo” que un $6$. Las reglas de la auditoría leen texto y configuración, no comportamiento. Los costes vienen de meses simulados en días distintos y suponen que las tarifas de Meta revisadas en octubre de 2026 no cambian; los volúmenes de la matriz “quién contesta qué” son supuestos que el orquestador confirmará con sus registros. Y el lector de ejemplo es inventado: la única puntuación que importa es la suya.

Antes de dar por cerrado cualquier módulo de automatización de la comunicación, conviene hacerse algunas preguntas:

  • ¿Cada flujo que escribe a un cliente tiene una base de envío declarada, y se puede demostrar para cada mensaje?
  • ¿Quién más, además de su autor, sabe qué hacer cuando un flujo falla un sábado?
  • ¿Qué errores se convierten en datos que nadie lee, y quién se entera de ellos?
  • ¿Las bajas de esta semana están aplicadas, y cuántos consentimientos envejecieron sin renovarse?
  • ¿Podría explicar en una hoja, a alguien que no usa ninguna de las herramientas, quién contesta qué?

Un gran libro que habla de esto es Patrones de integración empresarial (o Enterprise Integration Patterns), de Gregor Hohpe y Bobby Woolf, que dio nombre a casi todas las piezas del módulo: el enrutador basado en contenido que es el orquestador, el receptor idempotente que evita las respuestas dobles, el canal de mensajes inválidos y el bus de control que vigila a los demás. Leerlo después de construir un ecosistema pequeño tiene una ventaja: cada patrón deja de ser un diagrama abstracto y se convierte en el nombre de un problema que ya tuvo la panadería, y en la solución que otras empresas encontraron antes.

Un ecosistema de comunicación está listo cuando cualquiera en el negocio puede decir quién contesta qué, con qué permiso y cuánto cuesta. Trigo Lento llega a la semana de pruebas con ese mapa, con cinco correcciones anotadas, dos bajas aplicadas a mano y $12.81$ USD libres para lo que viene.

Conclusiones clave


Canales y bases de envío

  • El módulo 5 deja $8$ flujos de comunicación con un solo dueño; $3$ escriben a clientes y todos declaran su base de envío: ventana abierta, consentimiento por finalidad o pedido en curso.
  • Fuera de la ventana de $24$ horas solo cabe una plantilla aprobada de la categoría correcta: a las $26$ horas, una plantilla de utilidad sobre el pedido en curso, nunca un texto libre ni una promoción.
  • Antes de la semana de pruebas, $2$ de $7$ bajas seguían sin aplicar y $32$ consentimientos de marketing tenían más de doce meses: la revisión de la base se hace cada semana, no solo cuando algo falla.

Control humano

  • De $2,330$ mensajes al mes, la máquina contesta sola $1,200$ ($51.5%$), todos sin decisión; en $550$ ($23.6%$) una persona firma o decide, y en $160$ aprueba antes de que el cliente reciba nada.
  • La auditoría automática del código de honor acepta sin cambios solo TL Slack operaciones v1; la secuencia de WhatsApp tiene tres pendientes (transparencia, registro y errores) y Gmail y el orquestador uno cada uno.
  • El banco de doce preguntas corrige por tema con un umbral del $80%$: el lector de ejemplo acierta $10$ de $12$, pero “Medición y costes” se queda en el $33%$ y el diagnóstico lo envía a los días $46$ y $53$.

Costes y riesgos

  • La comunicación cuesta $32.19$ USD al mes: WhatsApp $9.43$, Claude $6.17$, Make $10.59$ y servidor $6.00$; quedan $12.81$ libres, y con el volumen de diseño el total sube a $34.68$ y WhatsApp queda a ocho centavos de su rubro.
  • Los controles del módulo bajan la exposición al riesgo de $91$ a $35$ puntos ($61.5%$), casi siempre por probabilidad y no por impacto; el riesgo residual más alto es la alerta perdida, con $6$.
  • El cambio de tarifas de Meta es el único riesgo que no se puede reducir: se vigila con el taxímetro de los webhooks y se absorbe con la reserva.

Mañana, en el Día 56: Sesión práctica: la semana de comunicación automatizada de Trigo Lento. Con el mapa, las correcciones y el presupuesto de hoy, Valeria pondrá a funcionar todos los canales a la vez durante una semana de Pascua, en modo sombra primero, y comprobará con los registros del orquestador que ninguna respuesta sale doble, fuera de su base o sin la persona que le corresponde.

Referencias


  1. Roediger, H. L. y Karpicke, J. D. (2006). Test-Enhanced Learning: Taking Memory Tests Improves Long-Term Retention. Psychological Science, 17(3), 249-255. https://doi.org/10.1111/j.1467-9280.2006.01693.x
  2. Meta (2026). WhatsApp Business Messaging Policy. https://business.whatsapp.com/policy (revisado en octubre de 2026).
  3. Hohpe, G. y Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Addison-Wesley.
  4. Meta (2026). Precios de WhatsApp Business Platform y ventana de atención al cliente. https://developers.facebook.com/docs/whatsapp/pricing (revisado en octubre de 2026).
  5. Floridi, L. y Cowls, J. (2019). A Unified Framework of Five Principles for AI in Society. Harvard Data Science Review, 1(1). https://doi.org/10.1162/99608f92.8cd550d1
comments powered by Disqus