Skip to content

Workflows — Reglas oficiales

Reglas fundamentales del sistema de Workflows y Automatizaciones de BoostCRM. Toda implementacion (backend, frontend, seeds, tests) debe cumplir estas reglas el 100% de las veces. No son sugerencias — son invariantes del sistema.


Workflow (scoped a entity_id)
├── Trigger (entry point — crea la ejecucion)
└── Steps (ordenados)
├── Step 1: entry point del flujo (el trigger lo activa)
└── Steps 2+: alcanzados por actions terminales
└── Actions (secuenciales dentro del step)
├── No-terminales: enviar, asignar, consultar, pausar
└── Terminal (ultima): flow.end, flow.goto_step, flow.condition, flow.evaluate_reply

Un workflow se activa por un trigger que crea la ejecucion, ejecuta actions en cada step, y puede ramificarse con actions terminales que transfieren a otros steps.

Nota conceptual: el trigger es logicamente del workflow, no del step. Es el mecanismo que crea la ejecucion — no ejecuta actions ni pertenece a la secuencia de un step. En la implementacion vive asociado al Step 1 por simplicidad de modelo de datos, pero conceptualmente el trigger es del workflow y el Step 1 es el primer step que ejecuta actions.


Si algo ya existe como servicio en el backend (asignar vendedor, enviar mensaje, crear evento), el action lo invoca, no lo reimplementa.

Razon: Mantenibilidad. Un bug se arregla en un solo lugar.

G2 — Todo workflow pertenece a una entity_id

Section titled “G2 — Todo workflow pertenece a una entity_id”

No existen workflows globales. Todo workflow esta scoped a una entity.

Razon: Consistencia con el modelo multi-tenant actual.

G3 — Un contacto puede estar en multiples workflows pero no en dos ejecuciones del mismo workflow

Section titled “G3 — Un contacto puede estar en multiples workflows pero no en dos ejecuciones del mismo workflow”

Un contacto puede tener activos un flujo de seguimiento y uno de calificacion al mismo tiempo. Pero no puede estar ejecutando dos instancias del mismo workflow.

Razon: Evita spam y loops, pero permite flexibilidad de automatizacion.

Cada ejecucion tiene un context_json que viaja por TODA la ejecucion. Es acumulativo — nunca se reinicia entre steps. Contiene:

  • Datos del trigger (contexto inicial)
  • Resultados de actions previos (context enrichment)
  • Respuestas del contacto (reply data)
  • Datos de consultas externas (Zauru, namespaced)

Las actions leen y enriquecen este contexto. Nunca lo reemplazan completo.

Razon: Permite encadenar actions. Ejemplo: zauru.read_invoices agrega context.zauru.recent_invoices para que flow.condition lo evalue y comms.send_template lo use en variables.

Nunca se pierde. Se marca status: paused_error con el log del error, y un admin puede retomar o cancelar.

Razon: No queremos perdida silenciosa de ejecuciones.

G6 — Un workflow JAMAS puede afectar a otra entity

Section titled “G6 — Un workflow JAMAS puede afectar a otra entity”

Un workflow de entity A no puede enviar mensajes, asignar vendedores, consultar datos ni ejecutar acciones sobre entity B. Esto aplica tambien para superadmins:

  • Si un superadmin esta en entity A y crea un workflow, ese workflow es de entity A y solo opera sobre entity A.
  • Si el superadmin cambia a entity B, los workflows que cree ahi son de entity B.
  • El WorkflowAuthService siempre resuelve el auth con el entity_id del workflow, no con el del usuario actual.
  • Los servicios internos (InteractionsService, RecordAssignmentsService, GroupInvoicesService) ya validan entity_id en todas sus queries.

Razon: Aislamiento multi-tenant. Un error de configuracion en una entity nunca puede causar dano en otra. Esto es un invariante de seguridad, no solo una conveniencia.


Para consultar datos de Zauru, usar un trigger programado (scheduled) combinado con actions de Zauru que consulten los datos.

Razon: Zauru no emite webhooks. Pollear como trigger es fragil y caro. Mejor controlamos nosotros cuando consultar.

El Step 1 tiene exactamente un trigger. No combinaciones AND/OR de triggers. Si necesitas dos triggers diferentes, son dos workflows diferentes.

Razon: Simplifica la UX y el engine.

T3 — Al crear un trigger, debe existir al menos un action en el mismo step

Section titled “T3 — Al crear un trigger, debe existir al menos un action en el mismo step”

Un trigger sin accion no tiene sentido. El sistema lo valida al guardar.

Razon: Prevencion de configuraciones invalidas.

T4 — Los triggers solo detectan, no transforman

Section titled “T4 — Los triggers solo detectan, no transforman”

Un trigger pone datos en el contexto (quien escribio, de que canal, metadata del evento) pero no ejecuta logica ni modifica datos.

Razon: Separacion clara: trigger = evento, action = reaccion.

T5 — Los triggers programados deben definir hora y zona horaria

Section titled “T5 — Los triggers programados deben definir hora y zona horaria”

Siempre se ejecutan en la timezone de la entity, no UTC.

Razon: Evita confusiones de “a las 8am” que corren a las 2am.

T6 — First-match wins para triggers de mensajes

Section titled “T6 — First-match wins para triggers de mensajes”

Un trigger message.inbound solo matchea mensajes que NO estan siendo procesados por otro workflow. El primer workflow que matchea (por prioridad definida por el usuario) gana.

Razon: Evita que un mismo mensaje dispare 5 workflows. El usuario define prioridad.

Pipeline de mensaje inbound (T6 + A12 unificados)

Section titled “Pipeline de mensaje inbound (T6 + A12 unificados)”

Cuando llega un mensaje de un contacto, el engine ejecuta este pipeline en orden estricto. Un mensaje SOLO puede ser consumido por UN destino.

Llega mensaje inbound de contacto+chat
1. ¿Existe ejecucion en estado `waiting_for_reply`
para este contacto+chat?
├── SI → reanudar esa ejecucion (A12)
│ El mensaje se consume. FIN.
2. Evaluar workflows activos por prioridad
(la prioridad la define el usuario en la UI)
3. Para cada workflow (de mayor a menor prioridad):
├── ¿El FilterEngine matchea? (evento + filtros)
│ │
│ ├── SI → crear nueva ejecucion (T6)
│ │ El mensaje se consume. FIN.
│ │
│ └── NO → siguiente workflow
4. Ningun workflow matcheo → mensaje no dispara nada.
(El mensaje se recibe normal en InteractionsService,
sin ejecucion de workflow)

Reglas del pipeline:

  • Un mensaje = un destino. Nunca dispara dos workflows ni reanuda y dispara al mismo tiempo.
  • waiting_for_reply siempre gana sobre triggers nuevos (A12). La ejecucion activa tiene prioridad absoluta.
  • La prioridad es por entity. Cada entity ordena sus workflows. El primer match gana (T6).
  • Si no hay match, no pasa nada. El mensaje existe en el sistema de interacciones pero no genera ejecucion de workflow.

S1 — Step 1 SIEMPRE tiene exactamente un trigger

Section titled “S1 — Step 1 SIEMPRE tiene exactamente un trigger”

Es el unico entry point del workflow. No hay otra forma de iniciar una ejecucion.

Solo se alcanzan por actions terminales de otro step. Poner un trigger en Step 2+ es un error de configuracion que el sistema rechaza.

S3 — Steps 2+ se alcanzan SOLO por actions terminales

Section titled “S3 — Steps 2+ se alcanzan SOLO por actions terminales”

Las unicas formas de llegar a otro step son:

  • flow.goto_step (transfer directo)
  • flow.condition (branching true/false)
  • flow.evaluate_reply (branching por respuesta)

No existe “next_step automatico”. Cada step debe terminar explicitamente.

S4 — Cada step DEBE terminar con exactamente una action terminal

Section titled “S4 — Cada step DEBE terminar con exactamente una action terminal”

La ultima action de un step debe ser una action terminal: flow.end, flow.goto_step, flow.condition, o flow.evaluate_reply.

Si un step no tiene action terminal, el sistema rechaza la configuracion.

Razon: Elimina ambiguedad de “que pasa despues”. Siempre esta explicito.

S5 — Actions dentro de un step ejecutan secuencialmente

Section titled “S5 — Actions dentro de un step ejecutan secuencialmente”

Misma regla que A4 aplicada a nivel de step. No hay ejecucion paralela.

S6 — Pausas ocurren DENTRO del mismo step

Section titled “S6 — Pausas ocurren DENTRO del mismo step”

response_policy.await_reply, flow.wait_for_reply y flow.delay pausan la ejecucion pero NO transfieren a otro step. Cuando la pausa termina, la siguiente action del MISMO step continua.


A1 — Toda action pertenece a exactamente una categoria

Section titled “A1 — Toda action pertenece a exactamente una categoria”

Las categorias son: comunicacion (comms.*), flujo (flow.*), crm (crm.*), notificaciones (notification.*), datos (zauru.*). No puede existir un action sin categoria.

Razon: Organizacion para la UI y para validaciones.

A2 — Las actions de flujo no producen efecto lateral

Section titled “A2 — Las actions de flujo no producen efecto lateral”

Solo controlan ejecucion: condicional, esperar, evaluar respuesta, ir a step, terminar. No envian mensajes, no crean records, no modifican datos.

Razon: Clara separacion entre “decidir/esperar” y “hacer”.

A3 — Las actions de comunicacion usan los servicios existentes de InteractionsModule

Section titled “A3 — Las actions de comunicacion usan los servicios existentes de InteractionsModule”

Nunca llaman directo al API de Communications.

Razon: Reutiliza validacion de sesion, permisos, rate limiting ya implementados.

A4 — Las actions se ejecutan secuencialmente dentro de un step

Section titled “A4 — Las actions se ejecutan secuencialmente dentro de un step”

Si una falla, las siguientes NO se ejecutan y el step se pausa con status: paused_error.

Razon: Consistencia. No queremos medio-step ejecutado sin saber cual fallo.

A5 — flow.wait_for_reply y response_policy.await_reply SIEMPRE con timeout

Section titled “A5 — flow.wait_for_reply y response_policy.await_reply SIEMPRE con timeout”

Minimo 1 hora, maximo 30 dias.

Razon: Evita ejecuciones zombie que esperan para siempre.

Si necesitas mas de 30 dias, probablemente es otro workflow.

Razon: Pragmatismo y limpieza de ejecuciones.

A7 — Las actions de Zauru son solo de LECTURA para MVP

Section titled “A7 — Las actions de Zauru son solo de LECTURA para MVP”

Consultar facturas, consultar ordenes. No crear ni aprobar. Cuando se habilite escritura post-MVP, se requiere validacion adicional y confirmacion explicita.

Razon: Reducir riesgo. Escritura en Zauru desde un workflow automatico puede ser destructivo.

A8 — Toda action recibe el context y puede enriquecerlo

Section titled “A8 — Toda action recibe el context y puede enriquecerlo”

Cada action declara un output_schema que define que campos agrega al contexto. Los datos de Zauru se guardan namespaced con output_key bajo context.zauru.*.

Razon: Permite encadenar: leer facturas -> condicional (tiene facturas?) -> enviar template con datos de factura.

A9 — Limites para actions de datos externos (Zauru)

Section titled “A9 — Limites para actions de datos externos (Zauru)”
Limite Valor Razon
Max items por consulta 100 Evitar contexto gigante
Timeout por request 15s Zauru puede ser lento, no bloquear
Campos devueltos Subset Solo lo util, no todo el JSON de Zauru
Retry on failure 1 Un intento mas, despues paused_error

A10 — Actions NUNCA crean ejecuciones nuevas

Section titled “A10 — Actions NUNCA crean ejecuciones nuevas”

Solo los triggers crean ejecuciones. Todas las actions — incluyendo flow.wait_for_reply y response_policy.await_reply — continuan una ejecucion existente con el mismo execution_id y el mismo context_json.

Razon: Esta es la linea que separa triggers de actions. Ver seccion “Frontera Trigger vs Action”.

A11 — Pausas de espera son continuaciones, no triggers

Section titled “A11 — Pausas de espera son continuaciones, no triggers”

response_policy.await_reply y flow.wait_for_reply son PAUSAS de una ejecucion existente. Cuando llega un mensaje que las reanuda, NO es un trigger nuevo — es la continuacion del mismo workflow, mismo execution_id, mismo contexto.

Razon: Si fueran triggers, crearian ejecuciones nuevas y perderian el contexto acumulado. Las pausas mantienen todo el estado.

A12 — Ejecucion pausada tiene prioridad sobre triggers nuevos

Section titled “A12 — Ejecucion pausada tiene prioridad sobre triggers nuevos”

Si un mensaje inbound llega y hay un workflow PAUSADO esperando respuesta de ese contacto+chat, ese mensaje REANUDA la ejecucion pausada. NO dispara el trigger de otro workflow para ese contacto+chat.

Razon: Si no, el wait_for_reply nunca recibiria respuestas porque los triggers se las “robarian”. La ejecucion activa siempre gana.

A13 — Actions de datos consultan activamente, triggers escuchan pasivamente

Section titled “A13 — Actions de datos consultan activamente, triggers escuchan pasivamente”

Las actions de datos (zauru.*) CONSULTAN — el workflow decide cuando. Los triggers ESCUCHAN — el evento externo decide cuando. Esta distincion es la razon por la que Zauru es action y no trigger (T1).

Razon: El workflow tiene control sobre cuando consultar. Los triggers reaccionan a eventos que no controlamos.

A14 — Un contacto+chat solo puede tener UNA ejecucion en waiting_for_reply

Section titled “A14 — Un contacto+chat solo puede tener UNA ejecucion en waiting_for_reply”

Si ya existe un workflow esperando respuesta de un contacto+chat, otro workflow que intente entrar en waiting_for_reply para ese mismo contacto+chat entra en queued_wait_conflict.

No es un error. Es un conflicto de recursos — el slot de espera esta ocupado. El workflow B no fallo, simplemente no puede esperar respuesta mientras el workflow A ya esta esperando.

Opciones para resolver:

  • Esperar: cuando el workflow A consuma la respuesta y libere el slot, el workflow B puede reintentar (manual o automatico)
  • Cancelar: un admin decide que el workflow B no es necesario

Razon: Evita ambiguedad de que workflow consume la respuesta. Usar un estado explicito (queued_wait_conflict) en vez de paused_error porque no es una excepcion tecnica — es una condicion esperada del sistema.

A15 — WorkflowAuthService resuelve el Auth para cada ejecucion

Section titled “A15 — WorkflowAuthService resuelve el Auth para cada ejecucion”

Todos los servicios del backend requieren un objeto Auth con entity_id, user_id, employee_id. Los workflows necesitan ejecutar estos servicios sin depender de que un usuario este logueado.

WorkflowAuthService resuelve el Auth segun el tipo de trigger:

Trigger ¿Como se resuelve el Auth?
manual Auth del usuario que hizo click (ya existe)
message.inbound Auth del sistema para la entity del contacto
scheduled Auth del sistema para la entity del workflow

El “sistema auth” usa un workflow_system_user_id configurado por entity. Este usuario tiene permisos admin para que los servicios internos (como assertConversationVisibleToActor en InteractionsService) no bloqueen la ejecucion.

Razon: Los workflows deben funcionar sin depender de que alguien este logueado. Un trigger programado a las 8am no tiene usuario activo.

A16 — Actions de Zauru usan un empleado API dedicado por entity

Section titled “A16 — Actions de Zauru usan un empleado API dedicado por entity”

Cada entity que use actions zauru.* en workflows configura un empleado “Boost API” creado exclusivamente en Zauru para ese entity.

  • Cada entity tiene su propio empleado Zauru → su propia sesion JWT independiente
  • El auto-refresh de JWT funciona normal (las credenciales/API key estan en DB)
  • Si 30 entities ejecutan workflows a la misma hora, cada una usa su propio JWT sin cola ni conflictos
  • Si un empleado real se va de la empresa, el workflow no se rompe

Un admin se logea UNA vez en Boost con las credenciales del empleado API de Zauru. A partir de ahi, ZauruSessionService.resolveRuntimeGraphqlJwt() refresca el JWT automaticamente con el API key guardado.

El workflow_zauru_user_id se configura por entity, no por workflow. WorkflowAuthService lo usa cuando ejecuta actions con prefijo zauru.*.

Razon: Un solo empleado Zauru global crearia un cuello de botella secuencial — cada JWT de Zauru esta atado a una entity. Con un empleado por entity, hay paralelismo total entre entities.


F1 — El Step 1 siempre es el entry point

Section titled “F1 — El Step 1 siempre es el entry point”

No se puede saltar al Step 3 desde fuera. Punto de entrada unico, trigger unico.

F2 — Un step puede apuntar a cualquier step posterior O anterior

Section titled “F2 — Un step puede apuntar a cualquier step posterior O anterior”

Esto permite loops (reintentos, “preguntale de nuevo”), pero con proteccion contra loops infinitos.

Mecanismo: step_visit_count

Cada ejecucion mantiene un contador de visitas por step:

{
"workflow": {
"step_visits": {
"step_1": 1,
"step_2": 3,
"step_bienvenida": 2
}
}
}

Cuando flow.goto_step transfiere a un step:

  1. Incrementa step_visits[target_step_id]
  2. Si step_visits[target_step_id] >= max_step_visitspaused_error con razon "max_step_visits_exceeded"
  3. Si no, ejecuta el step normalmente

Configuracion:

  • max_step_visits se configura por workflow. Default: 10.
  • El scope es por step individual, no por workflow ni por transicion.
  • Ejemplo: un step puede visitarse 10 veces maximo en una sola ejecucion.

Cuando se usa flow.goto_step hacia un step anterior con skip_trigger: true, se re-ejecutan las actions del step sin disparar el trigger.

Razon: Permite reintentos sin loops infinitos. El conteo por step es mas intuitivo que por transicion — “este step se ejecuto 10 veces” es una pregunta que un admin puede entender facilmente.

F3 — Sin action terminal explicita, el sistema rechaza el step

Section titled “F3 — Sin action terminal explicita, el sistema rechaza el step”

A diferencia de antes, NO asumimos “si no hay next_step, termina”. Ahora todo step requiere action terminal explicita (S4).

F4 — Solo flow.condition y flow.evaluate_reply pueden generar branching

Section titled “F4 — Solo flow.condition y flow.evaluate_reply pueden generar branching”

Son las unicas actions que pueden enviar la ejecucion a step A o step B. Las demas actions avanzan linealmente dentro del step. Estas actions DEBEN ser la ultima action del step (son terminales).

Razon: Claridad de que puede bifurcar y que no.


Esta seccion define la linea que NUNCA se cruza.

¿Crea una ejecucion nueva o continua una existente?

  • Si CREA → es trigger.
  • Si CONTINUA → es action.
Aspecto Trigger Action
¿Crea ejecucion? SI — nuevo execution_id NO — mismo execution_id
¿Genera contexto? SI — desde cero NO — enriquece el existente
Comportamiento Listener PASIVO Comando ACTIVO
¿Quien decide cuando? El evento externo El workflow decide ejecutar
¿Tiene timeout? NO — espera indefinidamente SI — timeout configurable
¿Scope? Todos los workflows que matcheen UNA ejecucion especifica
¿Donde vive? Solo Step 1 Cualquier step
FilterEngine Decide SI arranca Decide ADONDE va el flujo
¿Sabe del contexto? NO — lo crea SI — lo lee y enriquece

wait_for_reply y response_policy.await_reply NO son triggers aunque “escuchen” mensajes:

  1. No crean ejecucion nueva — reanudan la existente
  2. Estan atados a un contacto+chat especifico — no matchean globalmente
  3. Tienen timeout — los triggers no
  4. El workflow DECIDIO pausarse — es un comando activo, no un listener pasivo
  5. Mantienen el contexto acumulado — un trigger lo crearia desde cero
  • Un action que cree una ejecucion nueva de otro workflow
  • Un trigger en Step 2+
  • Un action que genere contexto desde cero (siempre enriquece)
  • Un trigger que modifique datos
  • Un action que escuche eventos globales (solo su propia ejecucion)

Ver documento dedicado: Workflows — Triggers

Resumen:

  • Capa 1 — Eventos reales: message.inbound, scheduled, manual
  • Capa 2 — FilterEngine universal: evalua context[field] operator value
  • Capa 3 — Presets: tarjetas de UI que pre-llenan evento + filtros

Ver documento dedicado: Workflows — Actions

Resumen:

  • ActionRegistry + Handlers: cada action type tiene un handler con execute(ctx, config)
  • TemplateEngine: resuelve {{variables}} contra el contexto
  • FilterEngine (reutilizado): flow.condition y flow.evaluate_reply usan el mismo motor
  • 5 categorias UI: Comunicacion, CRM, Flujo, Tiempo/Esperas, Datos

Action Codigo Categoria Terminal?
Enviar mensaje comms.send_message Comunicacion No
Enviar plantilla WA comms.send_template Comunicacion No
Enviar botones comms.send_interactive Comunicacion No
Condicion (if/else) flow.condition Flujo SI
Evaluar respuesta flow.evaluate_reply Flujo SI
Ir a step flow.goto_step Flujo SI
Finalizar workflow flow.end Flujo SI
Esperar tiempo flow.delay Tiempo No
Esperar respuesta flow.wait_for_reply Tiempo No
Asignar vendedor crm.assign_seller CRM No
Notificacion interna notification.send_internal CRM No
Buscar facturas zauru.read_invoices Datos No
Action Codigo Categoria
Enviar lista interactiva comms.send_list Comunicacion
Enviar email comms.send_email Comunicacion
Agregar etiqueta crm.add_tag CRM
Crear lead crm.create_lead CRM
Mover oportunidad crm.move_opportunity CRM
Actualizar campo crm.update_field CRM
Iterar sobre lista flow.for_each Flujo
Consultar productos zauru.read_products Datos
Crear orden zauru.create_order Datos
Webhook externo integration.webhook Datos

  • Eventos reales (engine): dominio.accion (ej: message.inbound, lead.assigned)
  • Codigo de action: categoria.verbo_sustantivo (ej: comms.send_template, flow.delay)
  • Categorias internas de actions: comms, flow, crm, notification, zauru
  • Categorias UI de actions: Comunicacion, Flujo, Tiempo/Esperas, CRM, Datos
  • Categorias UI de trigger presets: Mensajeria, Tiempo, Manual, CRM
  • Actions terminales: flow.end, flow.goto_step, flow.condition, flow.evaluate_reply
  • Estados de ejecucion:
    • running — ejecutando actions
    • waiting_for_reply — pausado esperando mensaje del contacto
    • waiting_delay — pausado esperando que pase el tiempo (flow.delay)
    • queued_wait_conflict — no puede esperar respuesta porque otro workflow ya ocupa el slot (A14). No es error.
    • paused_error — fallo tecnico (API caido, Zauru timeout, etc). Reintentable.
    • completed — workflow termino exitosamente (flow.end con status completed)
    • cancelled — workflow cancelado (manual o flow.end con status cancelled)
  • Filtros: { field, operator, value } — evaluados por el FilterEngine universal
  • Operadores MVP: eq, neq, contains, not_contains, in, not_in, is_empty, is_not_empty, gt, gte, lt, lte