Skip to main content

Automaciones

Sistema de notificaciones event-driven. Cualquier acción de negocio (crear reserva, completar encuesta, etc.) puede emitir un evento de dominio; los flujos configurados en el backoffice escuchan esos eventos y disparan acciones (push, in_app, tools_inbox, email, chat). Ruta del backoffice: /app/notifications/automations

Modelo conceptual

Cada flujo es lineal e inmediato: trigger → (condition opcional) → action. No hay step de “espera” — para disparos no-inmediatos (3 días antes, 24h después, etc.), se usan eventos derivados emitidos por crons diarios.

Step builder

El builder visual usa drag-and-drop sobre un canvas (xyflow). Tres tipos de step: Al guardar, el grafo se valida con validateDAG (un solo trigger, action es leaf node, sin ciclos).

Operadores de condición

Definidos en packages/common/src/types/automation-flow/conditions.ts y evaluados en apps/backend/src/notifications/engine/evaluate-rule.ts. present / absent tratan como ausente undefined, null y el string vacío o de solo espacios. El 0 y el "0" cuentan como presentes.
Para preguntar “¿vino este campo?”, usa present. Nunca neq contra "".neq compara strings: String(undefined) !== "" es true, así que una regla campo neq "" se cumple precisamente cuando el campo no llegó — lo contrario de lo que parece. El error simétrico también existe: condicionar sobre un campo que el emisor nunca manda deja la rama muerta para siempre, porque String(undefined) === "true" nunca se cumple. Dos flujos (FV001 y FV002) estuvieron meses sin disparar por eso, y no hay ninguna señal de que ocurra: una rama que no dispara no escribe en notification_dispatches ni deja error, solo un log efímero.Regla práctica: condiciona sobre la presencia del dato, no sobre un flag que el emisor debería mandar. Antes de usar un campo en una condición, confirma que está en el schema Zod del evento (apps/backend/src/notifications/engine/schemas/event-schemas.ts) y que el emisor lo envía de verdad.
Un operador no reconocido tampoco falla al guardar: el switch de evaluate-rule.ts cae en default y devuelve false, con el mismo resultado de rama muerta. Escríbelos siempre desde el desplegable del builder.

Catálogo de eventos

Lista vivamantenida (también disponible vía GET /public/events/registered-events y en el catálogo informativo de packages/common).

Emitidos por vivla-backend (HTTP POST)

Onboardinguser.welcomed, user.first_login Reservasbooking.created, booking.info_updated, booking.cancelled, booking.guest_invited, booking.rent_requested, booking.rent_approved (🔴 crítico), booking.rent_created_by_admin, booking.exchange_executed (bidireccional), booking.liberated, booking.third_home_published, booking.third_home_exchanged Llaveskeys.recovered, keys.expiring_soon, keys.extended, keys.fidelity_bonus_credited (los últimos tres desde crons internos del backend)

Emitidos internamente por vivla-tools

Encuestassurvey.completed (ramifica por lowestNormalized, la nota más baja normalizada a /10: absorbe los antiguos nps_high/nps_low) Ticketsticket.created, ticket.updated (discriminador field + previousValue/newValue), ticket.assigned, ticket.resolved (transición de status a resuelto/cerrado — identidad propia, sustituye a ticket.updated para esa transición), ticket.comment_added. Emitidos desde el módulo de tickets (mutaciones del panel + webhook de Zendesk). Los targets creatorId/assigneeId/actorId son users.id de staff interno, direccionables sin mapeos. Ver Avisos de tickets.

Emitidos por crons de Windmill

Oportunidadesbooking.stays_available_reminder (mensual) Lifecycle de usuariouser.inactive (60d sin uso de llaves)
Eventos descartados en la revisión con CX (no emitir, el ingest los rechaza): booking.listed_for_exchange, booking.exchange_requested, booking.exchange_matched, booking.dates_changed, booking.rent_rejected, booking.rent_opportunity, user.payment_not_setup, survey.nps_high, survey.nps_low. Detalle en Events API → Eventos descartados.
Eventos derivados (reemplazan los antiguos patterns delay+condition): T14b — mensajes conversacionales al canal de reserva (no tarjeta): booking.followup_7d_due y booking.arrival_greeting_due alimentan flows cuya acción chat lleva text_slug en vez de título/mensaje — el motor manda el texto de quick_replies (slug seguimiento-reserva / buen-viaje) interpolado, firmando Vivla, en vez de construir una tarjeta. Candidatos y exclusiones (cancelada, archivada, @vivla., propiedad inactiva) los resuelve GET /surveys/reports/survey-candidates?kind=followup|buen_viaje. Flows sembrados is_active=false.

API pública para integradores

POST /public/events/ingest — el único path por el que cualquier sistema externo (incluyendo vivla-backend) introduce eventos. Convenciones de la API v1: response envelope { object, id, type, status, created_at }, idempotencia vía header Idempotency-Key, errores estructurados con error.code, headers de rate limit y X-Request-ID. Detalle exhaustivo en Events API.

Request resumido

Nota v3.1: el body field event_id fue eliminado. La idempotencia se hace siempre vía header Idempotency-Key. Internamente se hashea de forma determinística al campo UNIQUE domain_events.event_id. Detalle en Idempotencia.

Response shapes

Garantías del servidor

  • Idempotencia vía hash determinístico de Idempotency-Keydomain_events.event_id UNIQUE.
  • Validación estricta por event_type — Zod schemas en apps/backend/src/notifications/engine/schemas/event-schemas.ts.
  • Whitelist de tipos — sólo se aceptan los registrados.
  • Rate limiting: 200 req/min con headers X-RateLimit-Limit/Remaining/Reset y Retry-After en 429.
  • Observabilidad — logs estructurados con request_id + métrica PostHog event.ingested.
  • Health checkGET /public/events/health.

Listar eventos soportados

Referencia completa con payload por evento, headers, ejemplos y status codes: ver Events API.

Endpoints CRUD del builder

Aparte del endpoint público de ingest, el backoffice usa los siguientes (auth Auth0 + RolesGuard, envelope v1):
Las rutas legacy /api/notifications/automations/* siguen vivas pero están marcadas como deprecadas — el frontend ya consume /v1. Ver Convenciones API para el envelope.

Endpoints de operaciones (admin)

Inspección del pipeline de eventos. Todos requieren rol admin.

Mantenimiento

Endpoints x-api-key-protegidos para crons de Windmill (no llamar manualmente):

Frequency warnings

SendingService corre un check soft antes de cada push: cuenta los pushes a ese user en últimas 24h y 7d. Si excede 1/día o 3/semana, NO bloquea el envío pero:
  • Loggea warning con notification_id, user_id, conteos.
  • Emite evento PostHog notification.frequency_warning con cap_type (daily / weekly / both).
El threshold viene del PRD initial (1/día, 3/semana) y por ahora es informativo. Si los warnings son consistentemente bajos, no se enforce; si son altos hay que decidir si se aplican como hard caps o se relajan.

Modelo de datos

automation_flows (vivla-tools Postgres): domain_events (vivla-tools Postgres):

Pipeline de procesamiento

v3.0: ya no existe el delay step ni la tabla scheduled_notifications. Los disparos no-inmediatos se modelan como eventos derivados emitidos por crons diarios.

Avisos de tickets (creador + asignado)

El creador y el asignado de un ticket reciben avisos por buzón interno de Tools (tools_inbox) + email de todo lo que le pasa a “su” ticket. Sembrados por las migraciones 171_seed_ticket_notification_flows.sql (created/updated/assigned/comment_added, plantillas + flows ACTIVOS al desplegar; UUIDs con segmento 2081) y 178_seed_ticket_resolved_notification_flow.sql (resolved). El link de cada plantilla apunta al detalle en Tools: https://tools.vivla.com/app/tickets/list/{{ticketId}}.

Metadata enriquecida para el buzón

Para los eventos ticket.*, el motor añade además un bloque metadata.ticket (ticketId, title, status, priority, ticketKind, propertyName, assigneeName) a la fila de inbox_messages — aditivo, no toca el metadata de otros flows — para que el sidebar pinte una card rica (TicketMessageCard) sin volver a pedir el ticket. Ver Inbox → Tickets via Automation Engine.

Anti-ruido: skipIfTargetEquals

ActionConfig.skipIfTargetEquals (opcional) es una lista de paths del payload: si el destinatario resuelto (target) coincide con el valor de alguno de esos campos, el motor salta la acción entera (engine.service.tsmatchedSkipField). Genérico y declarativo; se evalúa antes de cualquier canal y se ignora en test-mode (targetOverride). Los flows de tickets lo usan para dos cosas:
  • No avisarte de tu propio cambio — todas las acciones excluyen actorId (quién hizo el cambio; null = sistema/Zendesk, nunca suprime).
  • Deduplicar creador==asignado — la acción del creador excluye además assigneeId, así cuando son la misma persona solo se envía la copia del asignado (una notificación, no dos).
Valores null/vacíos del payload nunca matchean.

Suppressions (opt-out)

Ronda D agregó la tabla notification_suppressions (migración 089). El motor consulta PreferencesService.findActiveSuppression() antes de cada send (push/email/in_app/tools_inbox/chat) y skipea con un log si hay match — la flow se sigue ejecutando, simplemente este canal no dispara para ese destinatario. Reglas de match (en orden):
  1. scope='all' con el canal correspondiente bloquea todo el canal para ese user.
  2. scope='category' matchea cuando scope_value === template.category.
  3. scope='template' matchea cuando scope_value === template_id.
Origen típico: clicks de unsubscribe (source='list_unsubscribe'), también la UI de preferencias del usuario o inserciones manuales de support. Inspección desde SQL:
Eliminar manualmente (re-suscribir):
Detalle de las best practices transaccionales (preheader, List-Unsubscribe, UTM, Schema.org, color-scheme) en Email.

Referencias para integradores