Workflows — Actions
Documentacion interna completa del sistema de actions para Workflows. Para las reglas e invariantes, ver Workflows — Reglas oficiales. Para triggers, ver Workflows — Triggers.
Principio central
Section titled “Principio central”Igual que triggers: el codigo no decide, el codigo ejecuta. Cada action es un bloque atomico que llama a un servicio existente (G1). El usuario combina actions para armar su automatizacion.
Una action no deberia decidir negocio. Una action ejecuta una tarea pequena, clara y reutilizable.
Ejemplo malo: calificar_lead_y_asignar_si_interesado
Ejemplo bueno: comms.send_interactive + flow.wait_for_reply + flow.evaluate_reply + crm.assign_seller
Arquitectura: 3 piezas universales
Section titled “Arquitectura: 3 piezas universales”Pieza 1 — ActionRegistry + Handlers
Section titled “Pieza 1 — ActionRegistry + Handlers”Cada action type tiene un handler que implementa la misma interfaz:
interface ActionHandler { type: string; execute(context: WorkflowContext, config: ActionConfig): Promise<ActionResult>;}
interface ActionResult { success: boolean; error?: string; context_enrichment?: Record<string, any>;}El WorkflowExecutor los llama secuencialmente (A4).
Si success: false → estado paused_error, se detiene el step.
Cada handler llama al servicio existente correspondiente:
| Action | Servicio real |
|---|---|
comms.send_message |
InteractionsService.sendTextMessage() |
comms.send_template |
InteractionsService.sendTemplateMessage() |
comms.send_interactive |
InteractionsService.sendInteractiveButtonMessage() |
crm.assign_seller |
RecordAssignmentsService.createLeadAssignment() |
notification.send_internal |
NotificationsService.emitToUsers() |
zauru.read_invoices |
GroupInvoicesService |
Pieza 2 — TemplateEngine (variables)
Section titled “Pieza 2 — TemplateEngine (variables)”Se construye una vez. Antes de ejecutar un handler, resuelve {{variables}}
contra el contexto:
TemplateEngine.resolve("Hola {{trigger.sender_name}}", context)// → "Hola Rolando Osorio"
TemplateEngine.resolve("Factura #{{zauru.recent_invoices.items[0].number}}", context)// → "Factura #F-2458"Se usa en cualquier action con texto configurable:
comms.send_message→ bodycomms.send_template→ variables[]comms.send_interactive→ body, header, footer, button titles
Variables disponibles: todo lo que este en context_json — campos del trigger
inicial mas lo que cada action haya enriquecido.
Pieza 3 — FilterEngine (reutilizado)
Section titled “Pieza 3 — FilterEngine (reutilizado)”El MISMO motor de filtros de triggers se reutiliza en actions de flujo:
// flow.conditionFilterEngine.evaluate(context, conditions) → true/false
// flow.evaluate_replyFilterEngine.evaluate(context, match.conditions) → goto stepMismos operadores: eq, contains, in, gt, etc.
Misma evaluacion. Cero codigo nuevo para condicionales.
Categorias de actions
Section titled “Categorias de actions”Categorias internas (backend)
Section titled “Categorias internas (backend)”| Prefijo | Rol |
|---|---|
comms.* |
Enviar mensajes por cualquier canal |
flow.* |
Controlar flujo y ejecucion |
crm.* |
Modificar datos del CRM |
notification.* |
Notificaciones internas del sistema |
zauru.* |
Consultar datos de Zauru (solo lectura MVP) |
Categorias UI (lo que ve el usuario)
Section titled “Categorias UI (lo que ve el usuario)”| Categoria UI | Actions internas |
|---|---|
| Comunicacion | comms.send_message, comms.send_template, comms.send_interactive |
| Flujo | flow.condition, flow.evaluate_reply, flow.goto_step, flow.end |
| Tiempo / Esperas | flow.delay, flow.wait_for_reply |
| CRM | crm.assign_seller, notification.send_internal |
| Datos / Zauru | zauru.read_invoices |
Internamente flow.delay y flow.wait_for_reply son flow.*, pero en la UI
se muestran en una categoria separada “Tiempo / Esperas” porque para el usuario
son conceptos distintos a la logica condicional.
Actions terminales vs no-terminales
Section titled “Actions terminales vs no-terminales”Terminales (DEBEN ser la ultima action del step)
Section titled “Terminales (DEBEN ser la ultima action del step)”| Action | Que hace |
|---|---|
flow.end |
Termina el workflow (completed o cancelled) |
flow.goto_step |
Transfiere a otro step |
flow.condition |
Evalua contexto → rama true a un step, false a otro |
flow.evaluate_reply |
Evalua respuesta → cada match va a un step diferente |
No-terminales (pueden estar en cualquier posicion)
Section titled “No-terminales (pueden estar en cualquier posicion)”| Action | Que hace | ¿Puede pausar? |
|---|---|---|
comms.send_message |
Envia mensaje de texto | Si (response_policy) |
comms.send_template |
Envia template de WhatsApp | Si (response_policy) |
comms.send_interactive |
Envia mensaje con botones | Si (response_policy) |
crm.assign_seller |
Asigna vendedor | No |
notification.send_internal |
Notifica a usuarios internos | No |
zauru.read_invoices |
Consulta facturas | No |
flow.delay |
Pausa por tiempo | Si |
flow.wait_for_reply |
Pausa esperando mensaje | Si |
Regla: dentro de un step, todo es lineal. Si necesitas branching, eso es una action terminal que manda a steps diferentes. No hay if/else dentro del step — el if/else ES el ultimo paso del step.
Los 3 tipos de espera
Section titled “Los 3 tipos de espera”Tres conceptos DIFERENTES que NUNCA se mezclan. Cada uno tiene un caso de uso distinto y claro.
Decision tree: ¿cual usar?
Section titled “Decision tree: ¿cual usar?”¿Necesitas esperar una respuesta del contacto? │ ├── NO → ¿necesitas esperar tiempo? │ │ │ ├── SI → flow.delay (tipo 3) │ └── NO → no necesitas espera │ └── SI → ¿acabas de enviar un mensaje con botones? │ ├── SI → response_policy en comms.send_interactive (tipo 1) │ Una sola tarjeta. No agregar nada mas. │ └── NO → flow.wait_for_reply (tipo 2) Tarjeta independiente.Tipo 1 — response_policy (integrado en la action de comunicacion)
Section titled “Tipo 1 — response_policy (integrado en la action de comunicacion)”¿Cuando? Cuando envias un mensaje y quieres esperar la respuesta dentro de la misma tarjeta. El caso mas comun: enviar botones interactivos.
¿Que ve el usuario en la UI? UNA sola tarjeta: “Enviar botones”. No necesita agregar otra tarjeta de “esperar respuesta”.
{ "action_type": "comms.send_interactive", "config": { "body": "¿Que necesitas?", "buttons": [{"id": "ventas", "title": "Ventas"}, {"id": "soporte", "title": "Soporte"}], "response_policy": { "await_reply": true, "timeout_duration": 24, "timeout_unit": "hours", "on_timeout_step": "step_timeout" } }}Cada comms.* action declara en su definicion si soporta response_policy:
| Action | supports_response_policy |
Default | ¿Por que? |
|---|---|---|---|
comms.send_interactive |
true | ON | Envias botones → obviamente esperas respuesta |
comms.send_template |
true | OFF | A veces solo informas, no esperas respuesta |
comms.send_message |
true | OFF | Texto informativo por defecto |
Futuro: comms.send_sms |
false | N/A | SMS no tiene canal bidireccional confiable |
Futuro: comms.send_email |
false | N/A | Email no es conversacional en tiempo real |
Cuando response_policy.await_reply esta ON, el handler internamente:
- Envia el mensaje
- Cambia estado de ejecucion a
waiting_for_reply - Cuando llega respuesta → enriquece contexto → continua siguiente action
Importante: no coexiste con flow.wait_for_reply en el mismo step para
el mismo proposito. Si ya tienes response_policy.await_reply: true en
send_interactive, NO necesitas un flow.wait_for_reply despues.
Tipo 2 — flow.wait_for_reply (tarjeta independiente)
Section titled “Tipo 2 — flow.wait_for_reply (tarjeta independiente)”¿Cuando? Cuando quieres esperar respuesta PERO:
- No acabas de enviar un mensaje interactivo
- O enviaste un mensaje de texto simple y quieres esperar la respuesta
- O quieres esperar sin haber enviado nada (el workflow arranco y quiere capturar el proximo mensaje)
¿Que ve el usuario en la UI? Dos tarjetas separadas:
- “Enviar mensaje” (o nada)
- “Esperar respuesta”
{ "action_type": "flow.wait_for_reply", "config": { "timeout_duration": 24, "timeout_unit": "hours", "on_timeout_step": "step_timeout" }}Ejemplo de uso real:
Step: Seguimiento 1. comms.send_message → "Hola {{sender_name}}, ¿como te fue con la cotizacion?" 2. flow.wait_for_reply → timeout 24h 3. flow.evaluate_reply → evaluar respuestaAqui comms.send_message NO tiene response_policy porque es texto simple.
La espera la hace flow.wait_for_reply como tarjeta separada.
Tipo 3 — flow.delay (solo tiempo)
Section titled “Tipo 3 — flow.delay (solo tiempo)”¿Cuando? Cuando solo necesitas esperar tiempo. NO escucha mensajes. Si el contacto responde durante el delay, el workflow NO se entera — el mensaje va al inbox normal.
{ "action_type": "flow.delay", "config": { "duration": 24, "unit": "hours" }}Implementacion: Bull delayed job.
Resumen visual: flujos tipicos
Section titled “Resumen visual: flujos tipicos”Flujo con botones (tipo 1 — una tarjeta):
send_interactive (response_policy: await_reply) → evaluate_reply → ...Flujo con texto libre (tipo 2 — dos tarjetas):
send_message → wait_for_reply → evaluate_reply → ...Flujo con delay (tipo 3):
send_message → delay 24h → send_template → endAnti-patron — NUNCA hacer esto:
send_interactive (response_policy: await_reply) → wait_for_reply → ... ↑ REDUNDANTESemantica: first-response-wins
Section titled “Semantica: first-response-wins”Tanto response_policy.await_reply como flow.wait_for_reply siguen
la misma semantica: el PRIMER mensaje que llegue del contacto+chat
reanuda el workflow.
Si el contacto escribe “no quiero” y despues presiona el boton “Si”, solo cuenta “no quiero” — el workflow ya resumio.
Contexto que genera la respuesta
Section titled “Contexto que genera la respuesta”Cuando llega una respuesta (de response_policy o flow.wait_for_reply),
se guarda en el namespace reply del contexto:
Si fue boton interactivo:
{ "reply": { "text": "Ventas", "type": "interactive_button", "button_id": "ventas", "message_id": "msg_...", "received_at": "2026-06-19T15:00:00Z" }}Si fue texto libre:
{ "reply": { "text": "no quiero saber", "type": "text", "button_id": null, "message_id": "msg_...", "received_at": "2026-06-19T15:01:00Z" }}Ambos usan el mismo formato. flow.evaluate_reply evalua estos campos
usando FilterEngine (ej: reply.button_id eq "ventas").
Detalle de cada action MVP
Section titled “Detalle de cada action MVP”comms.send_message
Section titled “comms.send_message”Envia un mensaje de texto por el canal activo.
{ "action_type": "comms.send_message", "config": { "body": "Hola {{trigger.sender_name}}, gracias por escribirnos." }}Resuelve automaticamente desde el contexto: sender_identifier,
entity_channel_account_id, channel_type_code. El usuario no configura eso.
supports_response_policy: true (default OFF).
Servicio: InteractionsService.sendTextMessage().
Enriquece contexto: { comms.last_sent_message_id }.
comms.send_template
Section titled “comms.send_template”Envia un template aprobado de WhatsApp.
{ "action_type": "comms.send_template", "config": { "template_name": "seguimiento_factura", "language_code": "es", "components": [ { "type": "body", "parameters": [{"type": "text", "text": "{{trigger.sender_name}}"}] } ] }}Necesario cuando pasaron 24h de la ventana de sesion de WhatsApp.
supports_response_policy: true (default OFF).
Servicio: InteractionsService.sendTemplateMessage().
Enriquece contexto: { comms.last_sent_template_name }.
comms.send_interactive
Section titled “comms.send_interactive”Envia un mensaje con botones interactivos.
{ "action_type": "comms.send_interactive", "config": { "body": "¿Que necesitas?", "header": "Bienvenido", "footer": "Boost CRM", "buttons": [ { "id": "ventas", "title": "Ventas" }, { "id": "soporte", "title": "Soporte" } ], "response_policy": { "await_reply": true, "timeout_duration": 24, "timeout_unit": "hours", "on_timeout_step": "step_timeout" } }}Limite: maximo 3 botones (restriccion de WhatsApp). Titulo maximo 20 chars.
supports_response_policy: true (default ON).
Servicio: InteractionsService.sendInteractiveButtonMessage().
Enriquece contexto: { comms.last_sent_interactive_id, comms.button_options[] }.
Cuando response_policy.await_reply esta ON, la respuesta se guarda en reply.*.
crm.assign_seller
Section titled “crm.assign_seller”Asigna vendedor a un lead/contacto/grupo.
{ "action_type": "crm.assign_seller", "config": { "mode": "round_robin", "employee_ids": [5, 8, 12], "level": "lead" }}Modos MVP:
specific: asigna aemployee_idexactoround_robin: reparte entreemployee_ids[]
Niveles: group, lead, contact.
Servicio: RecordAssignmentsService.
Enriquece contexto:
{ "crm": { "assigned_employee_id": 5, "assigned_employee_name": "Carlos Lopez", "assignment_status": "assigned" }}notification.send_internal
Section titled “notification.send_internal”Envia una notificacion interna a uno o mas usuarios del CRM.
Usa el servicio existente NotificationsService.emitToUsers() que crea
notificaciones persistidas y las envia por WebSocket en tiempo real.
{ "action_type": "notification.send_internal", "config": { "recipient_mode": "specific", "recipient_employee_ids": [5, 8], "event_type": "workflow.notification", "title": "Nuevo lead asignado", "message": "Se te asigno {{trigger.sender_name}} desde el workflow de calificacion" }}Modos de destinatario:
specific:recipient_employee_ids[]fijoscontext:recipient_fieldlee un employee_id del contexto (ej:assigned_employee_id)
Servicio: NotificationsService.emitToUsers().
Enriquece contexto: { comms.last_notification_id, comms.notification_recipient_count }.
Caso tipico: despues de crm.assign_seller, notificar al vendedor asignado:
{ "action_type": "notification.send_internal", "config": { "recipient_mode": "context", "recipient_field": "crm.assigned_employee_id", "event_type": "workflow.assignment", "title": "Nuevo lead", "message": "{{trigger.sender_name}} fue asignado a tu cartera" }}zauru.read_invoices
Section titled “zauru.read_invoices”Consulta facturas de Zauru. Solo lectura (A7).
{ "action_type": "zauru.read_invoices", "config": { "date_mode": "days_ago", "days_ago": 2, "status": "emitted", "limit": 50, "output_key": "recent_invoices" }}Servicio: GroupInvoicesService.
Limites: max 100 items, timeout 15s, 1 retry (A9).
Enriquece contexto (namespaced):
{ "zauru": { "recent_invoices": { "items": [ { "number": "F-2458", "total": 1500, "customer_name": "Acme", "status": "emitted" } ], "count": 3, "total": 4500 } }}El output_key evita colisiones. Si consultas dos veces con keys diferentes,
ambas coexisten: zauru.recent_invoices y zauru.overdue_invoices.
flow.condition
Section titled “flow.condition”Evalua condiciones sobre el contexto y bifurca. Usa FilterEngine.
{ "action_type": "flow.condition", "config": { "conditions": [ { "field": "zauru.recent_invoices.count", "operator": "gt", "value": 0 } ], "on_true_step": "step_send_template", "on_false_step": "step_end" }}Action TERMINAL — debe ser la ultima del step. Sin efectos laterales (A2).
flow.evaluate_reply
Section titled “flow.evaluate_reply”Evalua la respuesta del contacto y bifurca. Usa FilterEngine.
{ "action_type": "flow.evaluate_reply", "config": { "matches": [ { "conditions": [{ "field": "reply.button_id", "operator": "eq", "value": "ventas" }], "goto_step_id": "step_ventas" }, { "conditions": [{ "field": "reply.button_id", "operator": "eq", "value": "soporte" }], "goto_step_id": "step_soporte" }, { "conditions": [{ "field": "reply.text", "operator": "contains", "value": "precio" }], "goto_step_id": "step_cotizacion" } ], "default_step_id": "step_no_entendimos" }}Evalua por reply_button_id (mas estable que comparar texto del boton).
Action TERMINAL — debe ser la ultima del step.
Sin efectos laterales (A2).
flow.delay
Section titled “flow.delay”Pausa la ejecucion por un tiempo. No escucha mensajes.
{ "action_type": "flow.delay", "config": { "duration": 24, "unit": "hours" }}Unidades: minutes, hours, days. Maximo 30 dias (A6).
Implementacion: Bull delayed job.
No es terminal — despues del delay, la siguiente action del step continua.
flow.wait_for_reply
Section titled “flow.wait_for_reply”Pausa hasta que llegue un mensaje del contacto+chat. First-response-wins.
{ "action_type": "flow.wait_for_reply", "config": { "timeout_duration": 24, "timeout_unit": "hours", "on_timeout_step": "step_timeout" }}Timeout obligatorio (A5). Cuando expira, transfiere a on_timeout_step.
Enriquece contexto con reply_text, reply_type, reply_button_id, etc.
No es terminal.
flow.goto_step
Section titled “flow.goto_step”Transfiere la ejecucion a otro step.
{ "action_type": "flow.goto_step", "config": { "target_step_id": "step_2", "skip_trigger": true }}skip_trigger: true — si el target es Step 1, no re-evalua el trigger.
Action TERMINAL.
flow.end
Section titled “flow.end”Termina el workflow.
{ "action_type": "flow.end", "config": { "status": "completed" }}Status: completed o cancelled.
Action TERMINAL.
ActionDefinition completa
Section titled “ActionDefinition completa”Cada action se registra con esta estructura:
interface ActionDefinition { action_type: string; ui_category: string; label: string; description: string;
config_schema: JSONSchema; input_requirements: string[]; output_schema?: Record<string, string>;
supports_response_policy: boolean; response_policy_default: boolean; is_terminal: boolean;
run_policy: { timeout_ms: number; retries: number; };}input_requirements
Section titled “input_requirements”Cada action declara que campos necesita del contexto para poder ejecutarse:
| Action | Requiere en contexto |
|---|---|
comms.send_message |
trigger.sender_identifier, trigger.entity_channel_account_id, trigger.channel_type_code |
comms.send_template |
trigger.sender_identifier, trigger.entity_channel_account_id |
comms.send_interactive |
trigger.sender_identifier, trigger.entity_channel_account_id, trigger.channel_type_code |
crm.assign_seller |
trigger.lead_id o trigger.contact_id o trigger.group_id |
notification.send_internal |
trigger.entity_id (recipients se configuran o vienen del contexto) |
zauru.read_invoices |
trigger.entity_id |
flow.evaluate_reply |
reply.text (generado por response_policy o wait_for_reply) |
Si el trigger es scheduled (no tiene trigger.sender_identifier), la UI puede
advertir: “Esta accion necesita un contacto activo. El trigger programado
no provee conversacion.”
Esto es validacion preventiva, no bloqueo — el usuario ve la advertencia.
Execution Context Schema (context_json)
Section titled “Execution Context Schema (context_json)”El context_json es un JSON unico por ejecucion que viaja por todos
los steps (G4). Para evitar colisiones de nombres, todo dato vive dentro
de un namespace oficial.
Namespaces
Section titled “Namespaces”interface ExecutionContext { // Datos del trigger — inmutables despues de la creacion trigger: { type: string; // "message.inbound" | "scheduled" | "manual" entity_id: number; contact_id?: number; lead_id?: number; group_id?: number; sender_identifier?: string; sender_name?: string; message_text?: string; channel_type_code?: string; entity_channel_account_id?: number; external_chat_id?: string; // ...todos los campos del trigger original };
// Ultima respuesta del contacto reply: { text: string; type: "text" | "interactive_button"; button_id: string | null; message_id: string; received_at: string; // ISO 8601 };
// Datos de actions de comunicacion comms: { last_sent_message_id?: string; last_sent_template_name?: string; last_sent_interactive_id?: string; button_options?: Array<{ id: string; title: string }>; last_notification_id?: number; notification_recipient_count?: number; };
// Datos de actions CRM crm: { assigned_employee_id?: number; assigned_employee_name?: string; assignment_status?: string; };
// Datos de Zauru — sub-namespaced por output_key zauru: { [output_key: string]: { items: any[]; count: number; total: number; }; };
// Metadata interna del workflow workflow: { workflow_id: number; workflow_name: string; execution_id: string; current_step_id: string; step_visits: Record<string, number>; // F2 loop counter source: "workflow"; };}Reglas del contexto
Section titled “Reglas del contexto”- Cada action SOLO escribe en su namespace.
comms.*actions escriben encomms,crm.*encrm,zauru.*enzauru. triggeres inmutable. Ningun action puede modificar datos del trigger.replyse sobrescribe cada vez que llega una nueva respuesta.workflowes gestionado por el engine, no por los handlers.- Los handlers leen de cualquier namespace pero solo escriben en el suyo.
Acceso en templates y filtros
Section titled “Acceso en templates y filtros”{{trigger.sender_name}} → nombre del contacto{{zauru.recent_invoices.items[0].number}} → numero de factura{{crm.assigned_employee_name}} → vendedor asignado{{reply.button_id}} → boton presionado{{workflow.step_visits.step_2}} → veces visitado step 2Los filtros del FilterEngine usan el mismo path:
{ "field": "reply.button_id", "operator": "eq", "value": "ventas" }{ "field": "zauru.recent_invoices.count", "operator": "gt", "value": 0 }{ "field": "crm.assignment_status", "operator": "eq", "value": "assigned" }Action Presets
Section titled “Action Presets”Misma filosofia que trigger presets. El usuario ve tarjetas simples.
Internamente es action_type + config pre-llenado.
Concepto
Section titled “Concepto”Tarjeta visible: "Enviar bienvenida"Internamente: action_type: comms.send_message config: { body: "Hola {{trigger.sender_name}}, gracias por escribirnos." } editable: true (el usuario puede modificar el body)El engine ignora el preset — solo ejecuta action_type + config.
Presets MVP
Section titled “Presets MVP”| Tarjeta (UI) | Action real | Config pre-llenada | Editable |
|---|---|---|---|
| Enviar mensaje | comms.send_message |
body: "" (usuario llena) |
body |
| Enviar bienvenida | comms.send_message |
body: "Hola {{trigger.sender_name}}..." |
body |
| Enviar plantilla | comms.send_template |
template_name: "" (usuario selecciona) |
template, vars |
| Enviar botones | comms.send_interactive |
body: "", buttons: [] (usuario llena) |
todo |
| Asignar vendedor | crm.assign_seller |
mode: "specific", employee_id: null |
employee |
| Repartir entre equipo | crm.assign_seller |
mode: "round_robin", employee_ids: [] |
employees |
| Condicion | flow.condition |
conditions: [] |
conditions |
| Esperar respuesta | flow.wait_for_reply |
timeout: 24h |
timeout |
| Evaluar respuesta | flow.evaluate_reply |
matches: [] |
matches |
| Esperar tiempo | flow.delay |
duration: 1, unit: "hours" |
duration |
| Finalizar | flow.end |
status: "completed" |
status |
| Notificar vendedor | notification.send_internal |
recipient_mode: "context", recipient_field: "assigned_employee_id" |
message |
| Notificar equipo | notification.send_internal |
recipient_mode: "specific", recipient_employee_ids: [] |
recipients, message |
| Buscar facturas | zauru.read_invoices |
days_ago: 1 |
days_ago |
Workflow Templates (post-MVP)
Section titled “Workflow Templates (post-MVP)”Un workflow template = grupo de steps pre-armados. Diferente de un action preset.
| Template | Steps incluidos |
|---|---|
| Calificar lead con botones | send_interactive → evaluate_reply → assign_seller → end |
| Seguimiento de factura | read_invoices → condition → send_template → end |
| Bienvenida y asignacion | send_message → wait_for_reply → assign_seller → send_message → end |
Esto es post-MVP. Para MVP solo hay action presets.
Flujo de ejecucion
Section titled “Flujo de ejecucion”1. Leer contexto (context_json) ↓2. Validar input_requirements ↓3. Resolver {{variables}} con TemplateEngine ↓4. Ejecutar handler (servicio existente) ↓5. Si response_policy.await_reply ON → pausar (waiting_for_reply) ↓ ↓ ↓ llega respuesta → enriquecer contexto → continuar ↓6. Enriquecer contexto con output del handler ↓7. Si hay siguiente action en el step → ejecutar Si es action terminal → transferir/terminar ↓ Si fallo → paused_error (A4)Persistencia
Section titled “Persistencia”Lo que se guarda en workflow_step_actions:
{ "action_type": "comms.send_interactive", "position": 1, "config": { "body": "Hola {{trigger.sender_name}}, ¿que necesitas?", "buttons": [ { "id": "ventas", "title": "Ventas" }, { "id": "soporte", "title": "Soporte" } ], "response_policy": { "await_reply": true, "timeout_duration": 24, "timeout_unit": "hours", "on_timeout_step": "step_timeout" } }}El config es JSON libre — cada handler sabe que esperar.
El ActionRegistry valida el config contra el config_schema del handler
antes de guardar.
Contrato con el frontend
Section titled “Contrato con el frontend”GET /api/workflows/action-presets— lista de tarjetas disponibles con label, icono, categoria UI y descripcion.GET /api/workflows/action-schemas/:action_type— devuelve elconfig_schemapara renderizar el formulario de configuracion.- Al agregar una action a un step, el frontend envia
action_type+config. - El backend valida
configcontra el schema yinput_requirementscontra el contexto que el trigger produce.
WorkflowAuthService — auth sin usuario logueado
Section titled “WorkflowAuthService — auth sin usuario logueado”Todos los servicios del backend (InteractionsService, RecordAssignmentsService,
GroupInvoicesService, NotificationsService) requieren un objeto Auth con
entity_id, id (user_id), employee_id.
En un trigger manual el Auth existe — es el del usuario que hizo click.
En triggers message.inbound y scheduled NO hay usuario logueado.
WorkflowAuthService es un servicio interno que resuelve el Auth:
class WorkflowAuthService { resolveAuth(execution: WorkflowExecution): Auth { const trigger = execution.context.trigger_type;
if (trigger === 'manual') { return this.getUserAuth(execution.context.triggered_by_user_id); }
// message.inbound y scheduled → auth del sistema const systemUserId = this.getSystemUserId(execution.context.entity_id); return this.buildSystemAuth(systemUserId, execution.context.entity_id); }
resolveZauruAuth(execution: WorkflowExecution): Auth { const zauruUserId = this.getZauruWorkflowUserId(execution.context.entity_id); return this.buildSystemAuth(zauruUserId, execution.context.entity_id); }}Configuracion por entity
Section titled “Configuracion por entity”Cada entity necesita dos campos (configurados una sola vez por un admin):
| Campo | Que es |
|---|---|
workflow_system_user_id |
User con permisos admin para actions internas |
workflow_zauru_user_id |
User del empleado “Boost API” en Zauru para esta entity |
El workflow_system_user_id tiene permisos admin para que servicios como
assertConversationVisibleToActor() (InteractionsService) no bloqueen.
El workflow_zauru_user_id se usa SOLO para actions zauru.*.
Resolucion de auth por action
Section titled “Resolucion de auth por action”| Action | ¿Que Auth usa? |
|---|---|
comms.send_message |
resolveAuth() → sistema o usuario manual |
comms.send_template |
resolveAuth() → sistema o usuario manual |
comms.send_interactive |
resolveAuth() → sistema o usuario manual |
crm.assign_seller |
resolveAuth() → sistema o usuario manual |
notification.send_internal |
resolveAuth() → sistema o usuario manual |
zauru.read_invoices |
resolveZauruAuth() → empleado API Zauru dedicado |
flow.* |
No necesitan Auth — son puro control de flujo |
Estrategia Zauru — un empleado API por entity
Section titled “Estrategia Zauru — un empleado API por entity”El problema
Section titled “El problema”El JWT de Zauru esta atado a una entity. Si usaramos un solo empleado global de Zauru para todos los workflows:
- Tendria que cambiar de entity antes de cada consulta
- Con 30 entities ejecutando workflows a las 8am → cola secuencial
- Un error en una entity podria bloquear a todas las demas
La solucion
Section titled “La solucion”Cada entity crea un empleado “Boost API” en Zauru con todos los permisos de lectura necesarios. Cada empleado tiene su propia sesion JWT independiente.
Entity "Restaurante A" → zauru_user: "boost-api-a@..." → JWT independienteEntity "Restaurante B" → zauru_user: "boost-api-b@..." → JWT independienteEntity "Restaurante C" → zauru_user: "boost-api-c@..." → JWT independienteSetup (una sola vez por entity)
Section titled “Setup (una sola vez por entity)”- Crear empleado “Boost API” en Zauru con permisos de lectura
- Un admin de Boost se logea con esas credenciales → se guarda
zauru_user_session - Configurar
workflow_zauru_user_iden la entity
A partir de ahi, ZauruSessionService.resolveRuntimeGraphqlJwt() refresca
el JWT automaticamente cuando expira, usando el API key guardado en DB.
Beneficios
Section titled “Beneficios”- Paralelismo total entre entities (cada una con su JWT)
- Si un empleado real se va de la empresa, el workflow no se rompe
- Si Zauru tiene un problema con una entity, las demas no se afectan
- El mecanismo existente no cambia — solo le pasamos un auth diferente
Escalabilidad
Section titled “Escalabilidad”Agregar una action nueva
Section titled “Agregar una action nueva”- Crear handler que implemente
ActionHandler - Registrar en
ActionRegistryconActionDefinition - Crear action preset(s) de UI
- Cero cambios en WorkflowExecutor, TemplateEngine o FilterEngine
Agregar una categoria nueva
Section titled “Agregar una categoria nueva”- Definir prefijo (ej:
calendar.*) - Crear handlers
- Registrar categoria UI
- Cero cambios en la arquitectura
