Workflows — Triggers
Documentacion interna completa del sistema de triggers. Para las reglas e invariantes, ver Workflows — Reglas oficiales.
Arquitectura de 3 capas
Section titled “Arquitectura de 3 capas”Por que esta separacion
Section titled “Por que esta separacion”El codigo de Boost no decide logica de negocio. Expone bloques reutilizables. El usuario arma su logica con workflows.
Si crearamos un trigger por cada caso de uso, terminariamos con
message.inbound.unassigned.whatsapp.first_message y explotaria
combinatorialmente. En su lugar:
- Pocos eventos reales (capa 1)
- Un motor de filtros universal (capa 2)
- Muchos presets bonitos (capa 3)
Capa 1 — Eventos reales (engine)
Section titled “Capa 1 — Eventos reales (engine)”Son los eventos puros que ocurren en el sistema. El engine solo conoce estos.
Eventos MVP
Section titled “Eventos MVP”| Evento | Donde se emite | Que significa |
|---|---|---|
message.inbound |
InteractionsService (webhook) |
Llego un mensaje de cualquier canal |
scheduled |
Cron job interno (Bull queue) | Es la hora configurada |
manual |
POST /api/workflows/:id/execute |
Usuario hizo click en ejecutar |
Eventos post-MVP
Section titled “Eventos post-MVP”Agregar = 1 linea de emit + definir contexto. El FilterEngine no cambia.
| Evento | Donde se emite |
|---|---|
message.outbound |
InteractionsService.sendMessage() |
contact.created |
ContactsService.create() |
lead.created |
LeadsService.create() |
lead.assigned |
RecordAssignmentsService.createLeadAssignment() |
lead.unassigned |
RecordAssignmentsService (delete assignment) |
opportunity.state_changed |
OpportunitiesService.moveState() |
quote.approved |
QuotesService.approve() |
quote.rejected |
QuotesService.reject() |
order.created |
OrdersService |
Capa 2 — Motor universal de filtros (FilterEngine)
Section titled “Capa 2 — Motor universal de filtros (FilterEngine)”Un solo FilterEngine construido una vez. Recibe contexto + filtros,
devuelve true o false.
{ field: "trigger.channel_type_code", operator: "eq", value: "whatsapp_api" }
FilterEngine.evaluate(context, filters) -> booleanOperadores MVP
Section titled “Operadores MVP”| Operador | Descripcion | Ejemplo |
|---|---|---|
eq |
Igual a | trigger.assignment_status eq "unassigned" |
neq |
No igual a | trigger.channel_type_code neq "instagram_dm" |
contains |
Texto contiene | trigger.message_text contains "precio" |
not_contains |
Texto no contiene | trigger.message_text not_contains "spam" |
in |
Valor en lista | trigger.channel_type_code in ["whatsapp_api", "whatsapp_business"] |
not_in |
Valor no en lista | trigger.channel_type_code not_in ["instagram_dm"] |
is_empty |
Es null o vacio | trigger.sender_name is_empty |
is_not_empty |
Tiene valor | trigger.message_text is_not_empty |
gt |
Mayor que | zauru.recent_invoices.count gt 0 |
gte |
Mayor o igual | zauru.recent_invoices.total gte 1000 |
lt |
Menor que | zauru.recent_invoices.count lt 10 |
lte |
Menor o igual | workflow.step_visits.step_2 lte 5 |
Operadores post-MVP
Section titled “Operadores post-MVP”Logica compuesta: OR y 1 AND (2 OR 3). Operadores de fecha: before, after.
Logica
Section titled “Logica”- MVP: AND — todos los filtros deben cumplirse.
- Post-MVP: OR y logica compuesta (
1 AND (2 OR 3)).
Field Registry
Section titled “Field Registry”message.inbound fields
Section titled “message.inbound fields”| Field | Tipo | Operadores | Descripcion |
|---|---|---|---|
trigger.channel_type_code |
string | eq, neq, in, not_in | whatsapp_api, whatsapp_business, facebook_messenger, instagram_dm |
trigger.assignment_status |
string | eq | "assigned" o "unassigned" |
trigger.message_text |
string | contains, not_contains, is_empty, is_not_empty | Cuerpo del mensaje |
trigger.is_first_message |
boolean | eq | Primer mensaje de este contacto |
trigger.sender_name |
string | contains, eq, is_empty, is_not_empty | Nombre del remitente |
scheduled fields
Section titled “scheduled fields”| Field | Tipo | Operadores | Descripcion |
|---|---|---|---|
run_at_time |
string | eq | Hora "HH:mm". Requerido |
timezone |
string | eq | IANA tz. Default: entity tz |
active_days |
number[] | in | 1=Lun, 7=Dom. Default: todos |
manual fields
Section titled “manual fields”Sin filtros.
Capa 3 — Presets (lo que el usuario ve)
Section titled “Capa 3 — Presets (lo que el usuario ve)”Cada preset = evento + filtros pre-llenados + label.
El usuario ve una tarjeta. La selecciona. Tiene su trigger configurado.
No necesita saber que es un message.inbound con filtros.
Presets MVP
Section titled “Presets MVP”| Tarjeta (label UI) | Preset code | Evento | Filtros pre-llenados | Categoria |
|---|---|---|---|---|
| Mensaje de persona no asignada | msg_unassigned |
message.inbound |
trigger.assignment_status eq "unassigned" |
Mensajeria |
| Mensaje de persona asignada | msg_assigned |
message.inbound |
trigger.assignment_status eq "assigned" |
Mensajeria |
| Primer mensaje de contacto | msg_first |
message.inbound |
trigger.is_first_message eq true |
Mensajeria |
| Cualquier mensaje entrante | msg_any |
message.inbound |
sin filtros | Mensajeria |
| Mensaje con palabra clave | msg_keyword |
message.inbound |
trigger.message_text contains "" (usuario llena) |
Mensajeria |
| Todos los dias a una hora | sched_daily |
scheduled |
active_days in [1,2,3,4,5,6,7] |
Tiempo |
| Solo entre semana | sched_weekdays |
scheduled |
active_days in [1,2,3,4,5] |
Tiempo |
| Ejecutar manualmente | manual |
manual |
sin filtros | Manual |
Presets post-MVP
Section titled “Presets post-MVP”| Tarjeta | Evento | Categoria |
|---|---|---|
| Lead creado | lead.created |
CRM |
| Lead asignado | lead.assigned |
CRM |
| Lead sin asignar | lead.unassigned |
CRM |
| Contacto creado | contact.created |
CRM |
| Cotizacion aprobada | quote.approved |
CRM |
| Oportunidad cambio de estado | opportunity.state_changed |
CRM |
Contexto por evento
Section titled “Contexto por evento”Todos los datos del trigger se guardan bajo el namespace trigger.*
en el context_json de la ejecucion. Ver workflows-actions.md para
el schema completo de contexto.
message.inbound
Section titled “message.inbound”{ "trigger": { "type": "message.inbound", "entity_id": 1, "contact_id": 42, "lead_id": 15, "group_id": 7, "channel_type_code": "whatsapp_api", "assignment_status": "unassigned", "assigned_employee_ids": [], "sender_identifier": "50242358270", "sender_name": "Rolando Osorio", "message_text": "Hola, quiero informacion", "message_id": "msg_...", "external_chat_id": "chat_...", "entity_channel_account_id": 3, "is_first_message": true, "occurred_at": "2026-06-19T14:10:00.000Z" }}scheduled
Section titled “scheduled”{ "trigger": { "type": "scheduled", "entity_id": 1, "execution_date": "2026-06-19", "execution_time": "08:00", "timezone": "America/Guatemala", "day_of_week": 4 }}manual
Section titled “manual”{ "trigger": { "type": "manual", "entity_id": 1, "triggered_by_employee_id": 5, "triggered_by_user_id": 3, "contact_id": 42, "lead_id": 15, "group_id": 7, "triggered_at": "2026-06-19T16:30:00.000Z" }}Persistencia
Section titled “Persistencia”Lo que se guarda en workflow_step_triggers:
{ "trigger_type": "message.inbound", "preset_code": "msg_unassigned", "filters": [ { "field": "trigger.assignment_status", "operator": "eq", "value": "unassigned" }, { "field": "trigger.channel_type_code", "operator": "in", "value": ["whatsapp_api"] } ]}El engine nunca mira preset_code. Solo evalua trigger_type + filters.
Contrato con el frontend
Section titled “Contrato con el frontend”GET /api/workflows/trigger-presets— lista de tarjetas disponibles.- Al crear workflow, enviar
preset_code. - El backend resuelve evento + filtros.
- Post-MVP: panel de “filtros avanzados” consultando el FieldRegistry.
Escalabilidad
Section titled “Escalabilidad”- Nuevo evento = 1 emit + fields + presets. 0 cambios en FilterEngine.
- Nuevo filtro = 1 field en el registry. 0 cambios en FilterEngine.
- Nuevo preset = evento + filtros pre-llenados. 0 codigo en el engine.
