Pular para o conteúdo

Motor de Automações

O motor de automações é o coração do x17. Permite criar fluxos visuais no estilo n8n que executam ações automaticamente quando triggers são disparados.

Fonte do Trigger
|
v
dispatchTrigger() / dispatchEventToAutomations() [trigger-dispatcher.ts]
|
+-> Buscar automações ativas com trigger_type/trigger_metric
+-> Para cada automação:
+-> Match conditions (trigger-registry.ts)
+-> Re-entry check (once/always/cooldown)
+-> Loop detection (chain depth <= 5)
+-> Concurrency check (execution-limiter.ts, max 10)
+-> executeAutomation() [executor.ts]
|
+-> Criar execution record
+-> Encontrar primeiro step (sem incoming edges)
+-> Loop:
+-> Flow filters check [filter-evaluator.ts]
+-> Step filters check [filter-evaluator.ts]
+-> Resolver variaveis [variable-resolver.ts]
+-> executeStep() [step-handlers.ts]
+-> Se wait > 30s: suspend
+-> Se condition: branch true/false
+-> Se switch: branch por rule
+-> Proximo step (next_step_id)
+-> Atualizar execution status (completed/failed/suspended)
ArquivoDescrição
src/lib/automations/trigger-registry.tsRegistro de todos os triggers com match conditions
src/lib/automations/trigger-dispatcher.tsDespacha triggers para automações correspondentes
src/lib/automations/executor.tsExecuta uma automação (loop de steps)
src/lib/automations/step-handlers.tsHandlers para cada tipo de step
src/lib/automations/expression-resolver.tsResolve expressoes {{variable}}
src/lib/automations/variable-resolver.tsResolve variaveis de template + inline expressions
src/lib/automations/filter-evaluator.tsAvalia filtros em 3 níveis
src/lib/automations/flow-serializer.tsSerializa/deserializa flows entre editor e banco
src/lib/automations/execution-limiter.tsLimita execuções concorrentes (Upstash Redis)
src/lib/automations/smart-sending.tsFrequency cap + quiet hours
src/lib/automations/whatsapp-sender.tsEnvia mensagens/templates via Meta API
src/lib/automations/platform-events.tsEventos de cada plataforma e-commerce
src/lib/automations/toolbox-categories.tsCategorias do toolbox no editor visual
src/lib/automations/trigger-schema.tsSchema helpers para triggers
src/lib/automations/blueprints.tsTemplates pre-configurados de automações
src/lib/automations/template-presets.tsPresets de templates
src/lib/automations/insert-variable.tsHelper para inserir variaveis no editor

O Trigger Registry é a Single Source of Truth para todos os tipos de trigger. Define:

  • type: identificador único do trigger
  • label: nome exibido na UI (pt-BR)
  • match(config, triggerData): função que determina se o trigger ativa a automação
  • schema: campos disponíveis como variaveis no flow
  • sampleData: dados de exemplo para preview no editor
TriggerLabelMatch Condition
message_receivedMensagem RecebidaSe config.keyword definido, verifica se message.text.body contém keyword (case-insensitive). Senao, sempre match.
TriggerLabelMatch Condition
new_contactNovo ContatoSe config.source definido, verifica triggerData.source === source. Senao, sempre match.
tag_addedTag AdicionadaSe config.tag_name definido, compara com triggerData.tag_name (case-insensitive).
tag_removedTag RemovidaMesma lógica de tag_added.
contact_updatedContato atualizadoSe config.field definido, verifica triggerData.field === field.
conversation_assignedConversa atribuidaSe config.assigned_to definido, verifica match.
segment_enteredEntrou no SegmentoSe config.segment_id definido, verifica match.
segment_exitedSaiu do SegmentoSe config.segment_id definido, verifica match.
TriggerLabelMatch Condition
order_createdPedido CriadoSe config.platform definido, verifica match.
order_paidPedido PagoFiltra por platform e min_amount.
order_cancelledPedido CanceladoSe config.platform definido, verifica match.
order_fulfilledPedido EnviadoSe config.platform definido, verifica match.
customer_createdCliente CriadoSe config.platform definido, verifica match.
checkout_createdCheckout CriadoSempre match.
cart_abandonedCarrinho AbandonadoFiltra por platform e min_amount.
TriggerLabelMatch Condition
tracking_updatedRastreamento AtualizadoFiltra por config.status e config.carrier_name.
TriggerLabelMatch Condition
webhookWebhook ExternoSempre match.
scheduleAgendamentoSempre match.
template_status_changedStatus de template alteradoSe config.status definido, verifica match.
date_propertyData do ContatoSempre match (verificação feita pelo cron).

Cada trigger define um schema com campos acessiveis via {{trigger.campo}}. Exemplos:

  • message_received: {{trigger.message_text}}, {{trigger.from}}, {{trigger.timestamp}}, {{trigger.message_type}}
  • order_created: {{trigger.customer_name}}, {{trigger.total_amount}}, {{trigger.order_id}}, {{trigger.order_number}}, {{trigger.platform}}, {{trigger.item_names}}, {{trigger.pix_qr_code}}
  • cart_abandoned: {{trigger.customer_name}}, {{trigger.total_amount}}, {{trigger.recovery_url}}, {{trigger.item_names}}
  • tracking_updated: {{trigger.tracking_number}}, {{trigger.tracking_status}}, {{trigger.carrier_name}}, {{trigger.tracking_url}}

O dispatcher é chamado quando um evento ocorre (webhook, ação do usuário, cron) e precisa encontrar e executar automações correspondentes.

dispatchTrigger(accountId, triggerType, triggerData)

Seção intitulada “dispatchTrigger(accountId, triggerType, triggerData)”

Fluxo:

  1. Busca automações ativas com trigger_type = triggerType e is_active = true e deleted_at IS NULL
  2. Para cada automação encontrada: a. Obtem TriggerDefinition do registry b. Chama def.match(config, triggerData) - se false, skip c. Re-entry check: verifica reentry_mode da automação d. Loop detection: verifica se automação já está na _executionChain (max depth: 5) e. Concurrency check: canExecute(accountId) via Upstash Redis (max 10 por account) f. incrementCount(accountId) + executeAutomation() (fire-and-forget) g. decrementCount(accountId) no finally

dispatchEventToAutomations(accountId, metricName, eventData)

Seção intitulada “dispatchEventToAutomations(accountId, metricName, eventData)”

Similar ao dispatchTrigger, mas para automações baseadas em trigger_metric:

  1. Busca automações com trigger_metric = metricName
  2. Usa matchesEventConfig() em vez do registry (filtra por platform e min_amount)
  3. Ignora eventos com _fromAutomation = true (previne loops)
  4. Limite: max 10 automações por evento (MAX_EVENT_AUTOMATIONS)

Configuravel por automação (reentry_mode):

ModeComportamento
alwaysExecuta toda vez (padrão)
onceExecuta apenas uma vez por contato (nunca re-entra)
cooldownRe-entra após X horas (reentry_cooldown_hours, padrão 24h)

Verificação: busca em automation_executions a última execução completed para o par (automation_id, contact_id).

Previne loops infinitos onde automação A dispara automação B que dispara automação A:

  • _executionChain: array de automation IDs já executados na chain
  • MAX_CHAIN_DEPTH = 5: máximo de automações encadeadas

executeAutomation(automationId, accountId, triggerData)

Seção intitulada “executeAutomation(automationId, accountId, triggerData)”

Responsável por executar todos os steps de uma automação em sequência.

Fluxo detalhado:

  1. Busca todos os automation_steps da automação (ordenados por created_at)
  2. Busca flow_filters da automação
  3. Encontra o primeiro step (step sem incoming edges)
  4. Cria registro em automation_executions com status: "running"
  5. Cria registros em automation_execution_steps para cada step (todos pending)
  6. Loop de execução (max 100 iterações - MAX_STEPS): a. Busca step atual no map b. Avalia flow_filters (se falha, para a execução) c. Avalia step_filters (se falha, pula para next_step_id) d. Atualiza step para running e. Chama executeStep(stepConfig, context) f. Se sucesso:
    • Se suspended (wait longo): cria suspension record, para execução
    • Senao: atualiza step para completed, avanca para proximo step
    • Para condition: branch true -> next_step_id, branch false -> condition_false_step_id
    • Para switch: branch matched -> switch_routes[].next_step_id g. Se falha: atualiza step para failed, para execução
  7. Atualiza automation_executions com status final + duration_ms

Encontrar primeiro step (findFirstStep):

  • Coleta todos os IDs que são target de algum edge (next_step_id, condition_false_step_id, switch_routes)
  • O primeiro step e aquele cujo ID NAO aparece como target
  • Fallback: primeiro step pela ordem de criação

Resume uma automação suspensa (apos wait longo):

  1. Busca suspension com status: "processing"
  2. Le current_step_id, trigger_data, previous_output
  3. Continua o loop de execução a partir do step salvo
  4. Acumula duration_ms (soma com duração anterior)
  5. Pode gerar OUTRA suspension se encontrar outro wait longo
automation_executions
id UUID PK
automation_id UUID FK
account_id UUID FK
contact_id UUID FK (nullable)
status: running | completed | failed | suspended
trigger_data JSONB
steps_total INTEGER
steps_completed INTEGER
error_message TEXT
duration_ms INTEGER
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
automation_execution_steps
id UUID PK
execution_id UUID FK
step_id UUID FK
step_type TEXT
step_name TEXT
status: pending | running | completed | failed | skipped | suspended
input_data JSONB
output_data JSONB
error_message TEXT
started_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
automation_execution_suspensions
id UUID PK
execution_id UUID FK
step_id UUID FK
account_id UUID FK
automation_id UUID FK
trigger_data JSONB
previous_output JSONB
current_step_id UUID
resume_at TIMESTAMPTZ
status: pending | processing | completed | failed
error_message TEXT
resumed_at TIMESTAMPTZ

Cada tipo de step tem um handler dedicado. O executeStep() roteia para o handler correto via lookup no STEP_HANDLERS map.

  • Descrição: Envia mensagem de texto livre via WhatsApp
  • Config: { message: string, is_transactional?: boolean }
  • Fluxo:
    1. Busca contato por contact_id
    2. Smart Sending check (se não transacional)
    3. Resolve variaveis inline {{trigger.name}} no texto
    4. Substitui URLs por tracked links (UTM params)
    5. Envia via sendWhatsAppText()
    6. Cria registro em messages + atualiza conversations.last_message_at
    7. Emite evento Message Sent
    8. Registra em smart_sending_timers
  • Output: { sent, wa_message_id, conversation_id, message }
  • Descrição: Envia template WhatsApp aprovado pela Meta
  • Config: { template_id: string, variable_mapping: Record<string, string>, var_custom_*: string, is_transactional?: boolean }
  • Fluxo:
    1. Busca template por ID, verifica meta_status === "approved"
    2. Busca contato, resolve phone
    3. Smart Sending check
    4. Resolve variaveis do template (resolveVariables())
    5. Resolve botoes (copy_code, url com variaveis)
    6. Cria tracked links para URLs no body
    7. Envia via sendWhatsAppTemplate()
    8. Cria registro em messages + emite evento Template Sent
  • Output: { template_id, template_name, sent, wa_message_id, resolved_variables }
  • Descrição: Pausa o fluxo por um tempo definido
  • Config: { duration: number, unit: "seconds" | "minutes" | "hours" }
  • Fluxo:
    • Se delay <= 30 segundos: setTimeout() sincrono
    • Se delay > 30 segundos: retorna { suspended: true, resume_at } -> cron resume
  • Output: { duration, unit, waited_ms } ou { duration, unit, delay_ms, resume_at, reason }
  • Descrição: Divide o fluxo em dois caminhos (true/false)
  • Config: { field: string, operator: string, value: string }
  • Operadores: equals, not_equals, contains, not_contains, starts_with, ends_with, is_empty, is_not_empty, greater_than, less_than
  • Fluxo: Avalia triggerData[field] <operator> value, suporta expressoes {{}} no value
  • Output: { field, operator, value, fieldValue, result }, branch: "true" | "false"
  • Descrição: Roteia para múltiplos caminhos com regras
  • Config: { rules: [{ conditions: [{ field, operator, value }], output_name }] }
  • Fluxo: Avalia regras em ordem, primeira que match determina o branch
  • Output: { matched_rule, output_name }, branch: "rule_0" | "rule_1" | ... | "default"
  • Routing: Usa switch_routes array no config para mapear handle_id -> next_step_id
  • Descrição: Adiciona ou remove uma tag do contato
  • Config: { tag_name: string }
  • Fluxo: Busca tag por nome, upsert/delete em contact_tags
  • Output: { action, tag_name, contact_id }
  • Eventos: Emite Tag Added ou Tag Removed
  • Descrição: Gera resposta inteligente com Claude via VPS proxy
  • Config: { prompt?: string } (prompt adicional opcional)
  • Fluxo:
    1. Busca ai_settings e brand_context da conta
    2. Busca últimas 20 mensagens da conversa
    3. Constroi system prompt + histórico de chat
    4. Chama chatWithClaude() via VPS proxy
    5. Trunca resposta em 500 chars
    6. Envia via sendWhatsAppText()
    7. Salva mensagem com ai_generated: true
  • Output: { reply, model, sent, wa_message_id }
  • Descrição: Faz requisição HTTP para API externa
  • Config: { url: string, method: string, headers: Record<string, string>, body?: string }
  • Segurança:
    • Bloqueio de IPs internos (10.x, 172.16-31.x, 192.168.x, 127.x, 0.x)
    • Apenas protocolos HTTP/HTTPS
    • Timeout de 10 segundos
  • Fluxo: Resolve variaveis {{}} na URL e body, executa fetch
  • Output: { status, body }
  • Descrição: Atribui a conversa a um agente da equipe
  • Config: { assign_to: string } (user_id do agente)
  • Fluxo: Verifica membro na conta, busca conversa aberta, atualiza assigned_to
  • Output: { assign_to, conversation_id, contact_id }
  • Descrição: Encerra a conversa ativa do contato
  • Config: { closing_message?: string }
  • Fluxo: Busca conversa aberta, opcionalmente envia mensagem de encerramento, atualiza status: "closed"
  • Eventos: Emite Conversation Closed
  • Output: { conversation_id, status, closing_message_sent }

Resolve expressoes no formato {{namespace.field}} dentro de textos.

{{trigger.customer_name}} -> Dados do trigger
{{contact.name}} -> Nome do contato
{{contact.phone}} -> Telefone do contato
{{contact.email}} -> Email do contato
{{previous.wa_message_id}} -> Output do step anterior
{{field_name}} -> Acesso legado direto ao trigger
  • Blacklist: __proto__, constructor, prototype, toString, valueOf
  • Max depth: 10 níveis de aninhamento
  • Max substitutions: 50 substituicoes por texto
  • Se não encontrar valor: mantem a expressão original {{trigger.campo}}
NamespaceFonte de Dados
trigger.*triggerData (dados do trigger que disparou a automação)
contact.*Contato associado (name, phone, email)
previous.*Output do step anterior na chain
(sem namespace)Acesso legado direto ao triggerData

Resolve variaveis de template WhatsApp. Diferente do expression resolver, este e específico para template parameters.

resolveVariables(variableMapping, triggerData, contact, customValues, resolveContext)

Seção intitulada “resolveVariables(variableMapping, triggerData, contact, customValues, resolveContext)”

Recebe um mapeamento { "1": "customer_name", "2": "total_amount" } e retorna string[] com valores resolvidos.

VariavelResolução
customer_nametriggerData.customer_name ou contact.name
contact_namecontact.name ou triggerData.customer_name
contact_phonecontact.phone ou triggerData.customer_phone
contact_emailcontact.email ou triggerData.customer_email
total_amountFormata como moeda BRL (R$ 297,90)
recovery_urlAdiciona UTM params automaticamente
order_idtriggerData.external_order_id ou triggerData.order_id
order_numbertriggerData.order_number
item_namestriggerData.item_names
item_counttriggerData.item_count
financial_statusTraduz para portugues (Pago, Pendente, etc.)
fulfillment_statusTraduz para portugues (Entregue, Parcial, etc.)
payment_methodTraduz para portugues (PIX, Cartão, etc.)
is_pix"true" ou "false"
pix_qr_codeCódigo PIX copia e cola
pix_expiration_dateFormatado pt-BR
tracking_numberCódigo de rastreio
tracking_statusLabel traduzido
carrier_nameNome da transportadora
tracking_urlURL de rastreio
tracking_locationLocalização
estimated_deliveryData formatada pt-BR
customValor customizado definido no editor

resolveInlineExpressions(text, triggerData, contact, previousOutput, resolveContext)

Seção intitulada “resolveInlineExpressions(text, triggerData, contact, previousOutput, resolveContext)”

Resolve expressoes {{trigger.*}}, {{contact.*}}, {{previous.*}} dentro de textos livres (mensagens, URLs, etc.). Combina o expression resolver com o variable resolver para acesso a variaveis formatadas.


Avalia filtros de contato em 3 níveis do flow.

Automação
|
+-> Flow Filters (automations.flow_filters)
| Aplicados ANTES de cada step.
| Se falha: PARA a execução inteira.
|
+-> Step 1
| +-> Step Filters (automation_steps.step_filters)
| Se falha: PULA este step, vai para next_step_id.
|
+-> Step 2
| +-> Step Filters
| ...
interface FilterCondition {
id: string
field: string // "name" | "phone" | "email" | "tag" | "created_at"
operator: string // depends on field type
value: string
}
CampoTipoOperadores
nametextequals, not_equals, contains, not_contains, starts_with, ends_with, is_empty, is_not_empty
phonetext(mesmos de text)
emailtext(mesmos de text)
tagtaghas_tag, not_has_tag
created_atdategreater_than (depois de), less_than (antes de), is_empty, is_not_empty

evaluateContactFilters(contactId, accountId, filters)

Seção intitulada “evaluateContactFilters(contactId, accountId, filters)”
  1. Busca contato completo (id, name, phone, email, created_at)
  2. Busca tags do contato via contact_tags JOIN tags
  3. Avalia TODOS os filtros (AND logico - todos devem passar)
  4. Se contato não encontrado: retorna true (permite execução)

Controla o número máximo de automações executando simultaneamente por account, usando Upstash Redis.

  • MAX_CONCURRENT = 10 execuções por account
  • TTL_SECONDS = 300 (5 minutos) para auto-cleanup
  • Redis key: x17:exec:active:{accountId}
canExecute(accountId) // Verifica se count < MAX_CONCURRENT
incrementCount(accountId) // INCR + EXPIRE 300s
decrementCount(accountId) // DECR, se <= 0: DEL

Se Redis não estiver configurado (sem env vars), todas as funções retornam permissivo (não bloqueia).


Previne fadiga do contato com dois mecanismos:

  • Configurado por conta: quiet_hours_enabled, quiet_hours_start, quiet_hours_end, timezone
  • Verifica hora atual no timezone da conta
  • Suporta overnight (ex: 21:00 -> 08:00)
  • Se em quiet hours: { allowed: false, reason: "quiet_hours", nextAllowedAt: "08:00 America/Sao_Paulo" }
  • Configurado por conta: smart_sending_enabled, smart_sending_window_hours (padrão: 16h)
  • Tabela smart_sending_timers: armazena last_sent_at por (account_id, contact_id, channel)
  • Se última mensagem enviada dentro da janela: { allowed: false, reason: "smart_sending", nextAllowedAt: ISO }
  • recordMessageSent(): upsert no timer após envio bem-sucedido

Steps com is_transactional: true ignoram Smart Sending (ex: confirmação de pedido, PIX).


Abstrai o envio de mensagens via Meta WhatsApp Cloud API.

  1. Busca whatsapp_profiles conectado para a conta
  2. Descriptografa access_token_encrypted com decrypt()
  3. Cria instância MetaWhatsAppApi

Envia mensagem de texto livre. Retorna SendResult { sent, waMessageId, reason }.

sendWhatsAppTemplate(accountId, to, templateName, language, parameters, buttonComponents?)

Seção intitulada “sendWhatsAppTemplate(accountId, to, templateName, language, parameters, buttonComponents?)”

Envia template com variaveis resolvidas. Constroi payload com components[] (body parameters + button components).

  • MetaApiError: extrai error.code e error.message do response da Meta
  • Retorna { sent: false, reason, errorCode, errorDetail } em vez de lancar exceção

Converte entre a representação do editor visual (@xyflow/react Nodes/Edges) é o modelo de dados do banco (AutomationStep[]).

serializeFlow(nodes, edges, automationId) -> AutomationStep[]

Seção intitulada “serializeFlow(nodes, edges, automationId) -> AutomationStep[]”

Converte nodes/edges do ReactFlow para registros do banco:

  • Filtra node do tipo trigger (não é salvo como step)
  • Mapeia node.type -> AutomationStepType via getStepType()
  • Resolve next_step_id via edges
  • Para condition: extrai yes edge -> next_step_id, no edge -> condition_false_step_id
  • Para switch: cria array switch_routes com { handle_id, next_step_id }
  • Preserva step_filters do node data

deserializeFlow(steps, triggerType, triggerConfig?, triggerMetric?) -> { nodes, edges }

Seção intitulada “deserializeFlow(steps, triggerType, triggerConfig?, triggerMetric?) -> { nodes, edges }”

Converte registros do banco para nodes/edges do ReactFlow:

  • Cria node trigger-0 com dados do trigger
  • Mapeia AutomationStepType -> ReactFlow type via REACT_FLOW_TYPE_MAP
  • Cria edges entre steps (incluindo condition yes/no e switch routes)
  • Edge style padrão: stroke: "#4D4D4D", strokeWidth: 2, type: "custom"
  • Conecta trigger ao primeiro step automaticamente
AutomationStepTypeReactFlow Node Type
send_messageaction
send_templatesend_template
waitwait
conditioncondition
switchswitch
add_tagtag
remove_tagtag
webhookaction
ai_respondai
http_requesthttp_request
assignassign
close_conversationclose_conversation

  • Autenticado via CRON_SECRET no header Authorization
  • Busca automações com trigger_type: "schedule" e is_active: true
  • Verifica schedule_interval (minutos) contra last_run_at
  • Se nenhuma execução running ativa: executa
  • Atualiza trigger_config.last_run_at
  • Autenticado via CRON_SECRET
  • Recebe { suspension_id } no body
  • Chama resumeAutomation(suspensionId)
  • Usado pelo pg_cron para retomar automações suspensas (apos wait longo)
  • recalculate_all_segments: a cada 5 minutos, recalcula membros de segmentos dinâmicos
  • execute-scheduled-campaigns: a cada 1 minuto, executa campanhas agendadas

Define os eventos disponíveis para cada plataforma de e-commerce integrada.

PlataformaEventos
ShopifyOrder Placed, Order Paid, Order Cancelled, Order Refunded, Order Fulfilled, Checkout Started, Checkout Recovered, Shipment Created, Customer Created
YampiOrder Placed, Order Paid, Order Cancelled, Order Fulfilled, Checkout Abandoned, Shipment Delivered, Contact Created
NuvemshopOrder Placed, Order Paid, Order Cancelled, Order Fulfilled, Checkout Started, Shipment Created
HotmartOrder Placed, Order Cancelled, Order Refunded, Checkout Abandoned
KiwifyOrder Placed, Order Refunded, Checkout Abandoned
WooCommerceOrder Placed, Order Paid, Order Cancelled

Esses eventos aparecem no toolbox do editor quando a integração correspondente está ativa.


Define as categorias e itens do toolbox no editor visual de automações.

IDLabelTriggersActions
whatsappWhatsAppmessage_received, Template Delivered, Template Readsend_message, send_template
contactsContatosnew_contact, Contact Created, tag_added, Tag Removed, Conversation Opened/Closed, contact_updated, conversation_assigned, segment_entered/exitedadd_tag, remove_tag, assign, close_conversation
systemSistemawebhook, schedule, Link Clicked, template_status_changed, date_propertyhttp_request
logicLógica(nenhum)condition, switch, wait
aiIA(nenhum)ai_respond

Geradas dinâmicamente via getToolboxCategories(integrations). Para cada integração ativa, cria uma categoria com os eventos da plataforma como triggers.


ArquivoLinhaDescrição
src/lib/automations/trigger-registry.ts1-56119 triggers registrados
src/lib/automations/trigger-dispatcher.ts1-234Dispatch + re-entry + loop detection
src/lib/automations/executor.ts1-598Execute + resume automation
src/lib/automations/step-handlers.ts1-94611 step handlers
src/lib/automations/expression-resolver.ts1-54Expression {{}} resolver
src/lib/automations/variable-resolver.ts1-250Template variable resolver
src/lib/automations/filter-evaluator.ts1-1493-level filter evaluation
src/lib/automations/flow-serializer.ts1-228Editor <-> DB serialization
src/lib/automations/execution-limiter.ts1-56Redis concurrency limiter
src/lib/automations/smart-sending.ts1-105Frequency cap + quiet hours
src/lib/automations/whatsapp-sender.ts1-117Meta API send abstraction
src/lib/automations/platform-events.ts1-111E-commerce event definitions
src/lib/automations/toolbox-categories.ts1-455Editor toolbox categories
src/app/api/cron/automations/route.ts1-81Schedule cron job
src/app/api/automations/resume/route.ts1-48Resume suspended automations