0010 — Los subflujos se aplanan al compilar
Fecha: 2026-09-23 · Estado: aceptada
Contexto
Hasta ahora nada era reutilizable entre bots. bot_flow_versions está clavada a un bot_id con FK
y CASCADE, y las plantillas del editor son un catálogo estático del bundle del frontend, con ids de
nodo fijos y semántica de reemplazo total del lienzo. Un tenant que quiere el mismo trozo de
conversación —"verificar identidad", "agendar", "cobrar"— en cinco bots lo dibuja cinco veces y lo
corrige cinco veces.
Decisiones de producto (2026-09-22): la biblioteca es del tenant; el bot fija una versión concreta y editar el subflujo no le afecta hasta que actualice; se comparten todas las variables de la conversación; y se editan en una pantalla propia con el mismo lienzo.
Decisión
El subflujo se aplana en tiempo de compilación: antes de construir el grafo, cada nodo
"Subflujo" se sustituye por los pasos de la versión que tiene fijada, con los ids prefijados
sf__<idDelNodo>__<idInterno>. No se usan los subgrafos de LangGraph (addNode(nombre, grafoCompilado)).
Por qué no subgrafos
En este motor todo se indexa por el id plano del nodo. Un subgrafo real convertiría cada subflujo en UN solo nodo para seis mecanismos a la vez:
| Mecanismo | Qué pasaría con un subgrafo |
|---|---|
Router de START (resume.nodeId, enterAt) | No se puede reanudar ni volver a un paso interno: el pathMap solo conoce el contenedor |
Medición por nodo (bot_node_runs, bot_nodes_daily) | Una fila por subflujo entero: se pierde qué paso falla, cuesta o tarda |
Continuación de bot_approvals | Su node_id no casaría con ninguna arista del grafo |
Guarda de interrupts huérfanos (turn-input.ts) | tasks[].name pasa a ser el nombre del contenedor |
Send de "Repetir por cada elemento" | Solo funciona dentro del mismo grafo |
defer de "Juntar respuestas" y el graph.stream de BotsService | Cambian de ámbito y de nombre |
Aplanando, los seis siguen funcionando sin tocarlos. Solo cambian los ids.
El prefijo
sf__<idDelNodoQueLoInserta>__<idInterno>, y anidado compone solo (sf__c1__sf__c2__x).
- Lleva el id del contenedor, que es estable en el flujo del bot: así el mismo subflujo se puede
insertar dos veces sin chocar, y siempre se puede volver del id expandido al nodo que el usuario
ve en el lienzo (
containerOf). - El separador no puede ser
:ni|: LangGraph los reserva para el espacio de nombres de sus propios subgrafos y rechaza el nodo al compilar. Los ids que genera el editor son<tipo>-<fecha>-<n>, así que__no aparece en ninguno; además, el editor rechaza un id escrito a mano que empiece porsf__.
Consecuencias, y dónde se resuelven
- La revisión se calcula sobre el flujo expandido.
computeFlowRevisionno cambió: cambia lo que se le pasa. Efecto buscado: re-fijar una versión de contenido idéntico da la misma revisión y no manda a Inicio a las conversaciones vivas. Y un flujo sin subflujos devuelve el mismo objeto, así que su revisión no se mueve ni una coma. - El flujo expandido vive en
BotGraphInstance(bots.registry.ts), junto a su revisión y a los problemas de la expansión. Quien casa un id con el grafo mira ahí: la medición al registrar la revisión (si no, los pasos internos se quedan sin etiqueta), las aprobaciones al buscar la rama de continuación, y el inspector al etiquetar y al calcularresumeStale. - Un subflujo que no se puede resolver falla el turno. El nodo sobrevive a la expansión y el runtime lo compila como un nodo que lanza. Saltarse en silencio un paso que puede ser "verificar identidad" es peor que fallar.
Versión fijada e inmutabilidad
bot_subflow_versions es inmutable, con trigger. Es lo que hace que "versión fijada" signifique
algo: si el contenido de una versión pudiera cambiar, los bots que la usan cambiarían de
comportamiento sin enterarse. De paso hace el grafo de referencias acíclico por construcción:
solo se puede fijar una versión que ya existía.
Publicar una versión no recarga ningún bot. Actualizar es un acto explícito por bot, y no es gratis: cambia la revisión de su flujo, así que sus conversaciones en curso vuelven a empezar por Inicio, como con cualquier edición.
Accesos
bot_subflow_versions: cerrada aanonyauthenticated, comobot_flow_versions.flow_definitiones código ejecutable y puede traer secretos dentro (las cabeceras de un nodo "Llamar API" en línea). Solo la lee el backend con service-role.bot_subflows(la metadata):GRANT SELECTcon política de pertenencia ybot.view. La necesitan el selector del nodo y el aviso de "hay versión nueva", y no contiene nada sensible.- Sin permisos nuevos. Editar un subflujo es un subconjunto estricto de lo que
bot.updateya concede —quien puede editar el flujo de un bot ya puede dibujar esos mismos nodos dentro del bot— y publicar no afecta a nadie. Un permiso nuevo obligaría a un backfill por rol en todos los tenants, con riesgo de dejar fuera a quien hoy edita flujos sin ser admin.
Variables
Se comparten todas las variables de la conversación: no hay entradas ni salidas declaradas. Es
lo más simple de entender y lo que pidió el usuario, pero tiene un riesgo que conviene decir en voz
alta: un subflujo que escribe result pisa el result del flujo que lo usa. Convención
recomendada: prefijar las variables del subflujo con su código (identidad_cedula).
Las salidas sí son explícitas: cada nodo Fin con output_name es un handle del nodo que lo
inserta. Si no se nombra ninguna, el subflujo tiene una sola salida, "Fin".
Fuera de alcance (a propósito)
- Anidar subflujos. Las guardas de profundidad y de ciclo están implementadas, pero el editor no deja hacerlo todavía.
- Actualizar la versión en lote. Escribir treinta flujos, crear treinta versiones y reiniciar treinta conjuntos de conversaciones vivas merece su propia fase, con vista previa y confirmación.
- Un subflujo como cuerpo de "Repetir por cada elemento". La regla "el cuerpo converge en Juntar respuestas" cruza la frontera y el editor del bot no puede comprobarla.
- Agrupar métricas por subflujo en el informe y ofrecérselos al asistente. El prefijo deja ambas cosas preparadas.