Skip to content

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.


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


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

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 → body
  • comms.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.

El MISMO motor de filtros de triggers se reutiliza en actions de flujo:

// flow.condition
FilterEngine.evaluate(context, conditions) → true/false
// flow.evaluate_reply
FilterEngine.evaluate(context, match.conditions) → goto step

Mismos operadores: eq, contains, in, gt, etc. Misma evaluacion. Cero codigo nuevo para condicionales.


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)
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.


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.


Tres conceptos DIFERENTES que NUNCA se mezclan. Cada uno tiene un caso de uso distinto y claro.

¿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:

  1. Envia el mensaje
  2. Cambia estado de ejecucion a waiting_for_reply
  3. 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:

  1. “Enviar mensaje” (o nada)
  2. “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 respuesta

Aqui comms.send_message NO tiene response_policy porque es texto simple. La espera la hace flow.wait_for_reply como tarjeta separada.

¿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.

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 → end

Anti-patron — NUNCA hacer esto:

send_interactive (response_policy: await_reply) → wait_for_reply → ...
↑ REDUNDANTE

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.

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").


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 }.

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 }.

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.*.

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 a employee_id exacto
  • round_robin: reparte entre employee_ids[]

Niveles: group, lead, contact. Servicio: RecordAssignmentsService. Enriquece contexto:

{
"crm": {
"assigned_employee_id": 5,
"assigned_employee_name": "Carlos Lopez",
"assignment_status": "assigned"
}
}

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[] fijos
  • context: recipient_field lee 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"
}
}

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.

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).

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).

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.

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.

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.

Termina el workflow.

{
"action_type": "flow.end",
"config": {
"status": "completed"
}
}

Status: completed o cancelled. Action TERMINAL.


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;
};
}

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.


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.

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";
};
}
  1. Cada action SOLO escribe en su namespace. comms.* actions escriben en comms, crm.* en crm, zauru.* en zauru.
  2. trigger es inmutable. Ningun action puede modificar datos del trigger.
  3. reply se sobrescribe cada vez que llega una nueva respuesta.
  4. workflow es gestionado por el engine, no por los handlers.
  5. Los handlers leen de cualquier namespace pero solo escriben en el suyo.
{{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 2

Los 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" }

Misma filosofia que trigger presets. El usuario ve tarjetas simples. Internamente es action_type + config pre-llenado.

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.

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

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.


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)

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.


  1. GET /api/workflows/action-presets — lista de tarjetas disponibles con label, icono, categoria UI y descripcion.
  2. GET /api/workflows/action-schemas/:action_type — devuelve el config_schema para renderizar el formulario de configuracion.
  3. Al agregar una action a un step, el frontend envia action_type + config.
  4. El backend valida config contra el schema y input_requirements contra 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);
}
}

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.*.

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 JWT de Zauru esta atado a una entity. Si usaramos un solo empleado global de Zauru para todos los workflows:

  1. Tendria que cambiar de entity antes de cada consulta
  2. Con 30 entities ejecutando workflows a las 8am → cola secuencial
  3. Un error en una entity podria bloquear a todas las demas

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 independiente
Entity "Restaurante B" → zauru_user: "boost-api-b@..." → JWT independiente
Entity "Restaurante C" → zauru_user: "boost-api-c@..." → JWT independiente
  1. Crear empleado “Boost API” en Zauru con permisos de lectura
  2. Un admin de Boost se logea con esas credenciales → se guarda zauru_user_session
  3. Configurar workflow_zauru_user_id en la entity

A partir de ahi, ZauruSessionService.resolveRuntimeGraphqlJwt() refresca el JWT automaticamente cuando expira, usando el API key guardado en DB.

  • 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

  1. Crear handler que implemente ActionHandler
  2. Registrar en ActionRegistry con ActionDefinition
  3. Crear action preset(s) de UI
  4. Cero cambios en WorkflowExecutor, TemplateEngine o FilterEngine
  1. Definir prefijo (ej: calendar.*)
  2. Crear handlers
  3. Registrar categoria UI
  4. Cero cambios en la arquitectura