Pular para conteúdo

Conteúdo Infinito — Arquitetura

Atualizado: 2026-07-08 · por: sessão manual · aprovação em princípio da segmentação de clusters · rev: 5

Fluxo detalhado de cada workflow, fila mode e decisões de design.


Visão geral do pipeline

Pipedrive (leads) ──► WF0 (cria atividades) ──► WF1 (IA + fila) ──► WF2 (disparo)
                                                                         │
                                                          ┌──────────────┤
                                                          ▼              ▼
                                                     Amazon SES    Make.com (WhatsApp)
                                                     (email)       (Vendas1-6 → Chatwoot)
                                                          │              │
                                                          └──────┬───────┘
                                                                 ▼
                                                          WF3 (cadência)
                                                          ──► próxima atividade

WF0 — A Ignição (19v9V6NEIsJgpS3j)

Trigger: Manual (Enrico ou Rafael executam quando precisa alimentar o pipeline)

Fluxo: 1. Busca pessoas do Pipedrive via filtro (leads ativos com canal de comunicação) 2. Code node "Processar e Calcular Datas": para cada pessoa, cria 2 atividades (email + whatsapp) - Filtra pessoas sem email E sem phone (não cria atividade pra quem não tem contato) - Distribui due_dates: email batch ~24/dia, whatsapp batch ~8/dia - Flags has_email e has_phone preservados nos dados 3. Cria atividades no Pipedrive com subject padrão de nutrição

Decisão de design: Batches separados por canal evitam sobrecarga. Email tem throughput maior (24/dia) porque SES não tem gargalo; WhatsApp é 8/dia porque Make.com processa sequencialmente.


WF1 — Motor de IA (3ZQCpuNIDjpnFlKH)

Trigger: Schedule */5 8-17 * * 1-5 (cada 5 min, 8h-17h, seg-sex) Versão atual: v2 (fila mode), 48 nós

Fila mode (implementado em maio 2026)

O WF1 usa uma fila PostgreSQL (nutricao_fila) para controle de processamento:

Schedule Trigger
  → Verifica Pausa (staticData.pause_until)
     → Está Pausado? → sim: para
     → não pausado:
        → Check Queue Count (pending na fila)
           → Queue Vazia?
              → SIM: Busca Atividades Pipedrive (limit=100) → Insert Fila (ON CONFLICT DO NOTHING)
              → NÃO: skip Pipedrive
           → Pick Batch da Fila (3 items, FOR UPDATE SKIP LOCKED)
              → Tem Item?
                 → SIM: Prepara Item → Loop (SplitInBatches batch=1)
                    → Get Person → Verifica Filtros → pipeline IA → WF2
                 → NÃO: Pausa Até Amanhã (staticData.pause_until = amanhã 8h)

Auto-pausa: Quando Pipedrive retorna vazio (data: null), o WF1 seta pause_until no staticData. O Schedule continua disparando, mas "Verifica Pausa" short-circuits (execução barata, sem chamadas externas). Resume sozinho às 8h do dia seguinte.

Proteção Pipedrive retorna vazio: Nó "Pipedrive Retornou?" (IF) inserido entre busca e separação. Se data === null → "Pausa Até Amanhã" direto, sem tentar separar items inexistentes.

Pipeline de IA (dentro do loop)

Get Person (Pipedrive)
  → Verifica Filtros (Code):
     - canal email: precisa ter email
     - canal whatsapp: precisa ter phone
     - aceita_comunicacao != 397 (Não aceita)
     - pool de conteúdos não esgotado
  → Descarta? (IF)
     → SIM: Marca Descartada → Mark Done fila
     → NÃO:
        → IA Etapa 1 — Escolhe Conteúdo (OpenRouter):
           - Classifica tipo do lead (incorporador, construtor, arquiteto, etc.)
           - Consulta pool_conteudos (Postgres)
           - Escolhe conteúdo mais relevante
        → IA Etapa 2 — Redige Mensagem (OpenRouter):
           - Personaliza mensagem usando dados do lead + conteúdo escolhido
           - Limite: 2 frases, 35 palavras (prompt v2)
           - NUNCA se apresenta com nome próprio
        → Despacha para WF2 (webhook)
        → Mark Done fila

Proteções implementadas no WF1

  • Parser resiliente: onError: continueRegularOutput + retryOnFail: true, maxTries: 2 em todas as chains de IA
  • OpenRouter retry: retryOnFail: true, maxTries: 3, waitBetweenTries: 5000 em todos os 4 nós OpenRouter
  • Paired-item tracking: Code nodes usam pairedItem: {item: i} no .map(), downstream usa $('Node').item (não .first())
  • Pool esgotado: Se todos os conteúdos do pool já foram enviados para aquele lead, descarta sem enviar

WF2 — Fila de Disparo (hyzhklpo9J4cbyIJ)

Trigger: Webhook (chamado pelo WF1)

Fluxo:

Webhook (recebe dados do WF1)
  → IF Canal Email?
     → SIM:
        → IF Tem Email?
           → SIM: Envia via Amazon SES (HTTP Request, raw body, encodeURIComponent manual)
           → NÃO: Fecha Atividade Sem Contato (marca done no Pipedrive, NÃO chama WF3)
     → NÃO (WhatsApp):
        → IF Tem Phone?
           → SIM: POST para Make.com webhook (Disparador Vendas)
           → NÃO: Fecha Atividade Sem Contato

Decisão de design — "Sem Contato": Leads que chegam no WF2 com canal=email mas sem email (ou canal=whatsapp sem phone) têm a atividade fechada SEM criar próxima. Isso evita loops infinitos de atividades que nunca serão disparadas. O WF3 NÃO é chamado nesse caso.

Amazon SES no n8n: NUNCA usar node nativo awsSes — double-encodes @. Usar HTTP Request com contentType: "raw" + rawContentType: "application/x-www-form-urlencoded" + encodeURIComponent manual para cada campo.


Make.com — Disparador Vendas 1-6

Cenários: 6 cenários idênticos processando em paralelo (IDs: 3971432, 3960338, 3960351, 3971434, 3971438, 3960363)

Fluxo de cada cenário:

Webhook (módulo 43) — recebe payload do WF2
  → Parse JSON (módulo 42) — extrai messages
  → Busca/Cria conversa no Chatwoot
  → Envia mensagem WhatsApp via Chatwoot API
  → UpdateActivity Pipedrive (marca done)
  → Callback WF3 (módulo 99) — se tag="nutricao", POST para nutricao-cadencia

Body do callback (módulo 99):

{"activity_id": "{{43.idatividade}}", "person_id": "{{42.id}}", "canal": "whatsapp"}

  • {{43.*}} = dados do webhook trigger
  • {{42.*}} = dados do parser JSON (pessoa)
  • Filtro: só executa se tag === "nutricao" (não afeta outros fluxos como cadencia-automatica)

Pegadinha Make: HTTP module tem DOIS campos no mapper (body e data). A UI renderiza data, a API lê body. Ao modificar via API, SEMPRE setar AMBOS.


WF3 — Gestão de Cadência (ny4gXcNU3dReDCba)

Trigger: Webhook /nutricao-cadencia (chamado pelo Make callback ou diretamente pelo WF2 para email)

Fluxo:

Webhook (activity_id, person_id, canal)
  → Marca Atividade Done (Pipedrive, idempotente)
  → Get Person (Pipedrive)
  → Valida Aceita Comunicação (Code):
     - 397 = Não aceita → bloqueia
     - 394 = Só Email + canal=whatsapp → bloqueia
     - 395 = Só WhatsApp + canal=email → bloqueia
     - 396 ou null = aceita → continua
  → IF Pode Continuar?
     → SIM:
        → Calcula Próxima Data (Code):
           - Lê frequência do lead (mensal/bimestral/semestral)
           - Soma dias úteis
        → Checa Feriado (Redis)
        → É Feriado? → ajusta para próximo dia útil
        → Verifica Atividade Aberta (HTTP GET, guard de idempotência — adicionado 2026-07-03/04):
           - `GET /v1/persons/{person_id}/activities?done=0&limit=100` (credencial Bepex Enrico, leitura)
        → Avalia Atividade Aberta (Code):
           - Filtra a lista retornada por `type === activity_type && done === false`
           - Assertion: se a resposta não tiver o formato `{success, data}` esperado, marca `guard_error: true` mas segue fail-open (não bloqueia a cadência por um erro do próprio guard)
        → Já Existe Atividade Aberta? (IF)
           → SIM: Guard - Pula Criação (Já Existe) — dead end, não cria nada
           → NÃO: Cria Próxima Atividade (Pipedrive)
     → NÃO: dead end (sem nova atividade)

Decisão de design — guard de idempotência (2026-07-03/04): "Cria Próxima Atividade" criava incondicionalmente antes deste fix, permitindo duplicatas quando o mesmo webhook era chamado mais de uma vez (WF2, Make Vendas1-6 + retries, WF1 pool esgotado). O guard consulta as atividades abertas da própria pessoa no Pipedrive e só cria se não houver nenhuma do mesmo canal. Vale para qualquer caminho que chegue no webhook, sem bifurcação por origem. Detalhes do bug descoberto durante o teste (o endpoint /v1/activities não filtra por person_id) e a decisão fail-open no erro do guard: ver HISTORICO.md (entrada 2026-07-03/04) e REFERENCIA_TECNICA.md.


WF4 — Opt-out Email (paffbqyOJHAafePk)

Trigger: Webhook GET /unsubscribe?pessoa_id=X (link de unsubscribe nos emails)

Recebe pedido de opt-out de email e atualiza aceita_comunicacao no Pipedrive de forma nuançada por canal — o clique cancela email, não necessariamente WhatsApp:

Estado atual Nó "Já Optou Out?" Novo valor Por quê
vazio (nunca definiu) ou 396 (Email e WhatsApp) segue 395 (Só WhatsApp) tira o email, mantém o canal que ainda não foi recusado
394 (Só Email) segue 397 (Não aceita comunicação) email era a única via aberta; sem ela, não resta canal
395 (Só WhatsApp) segue (mas sem efeito real) sem mudança já não recebe email, nada a fazer
397 (já não aceita nada) bloqueia (branch "Respond Já Optou Out") idempotência — não reprocessa quem já está 100% fora

Próxima vez que WF3 rodar para um lead em 397, será bloqueado pela validação de aceita_comunicacao. Um lead em 395 (Só WhatsApp) continua recebendo nutrição por WhatsApp normalmente.

Bug histórico (corrigido 2026-07-03): o nó IF checava um campo (nutricao_unsubscribed) que nunca existiu no schema — nenhum opt-out atualizava o Pipedrive desde a ativação (2026-04-07). Cronologia completa, causa raiz e remediação em HISTORICO.md (entrada 2026-07-03).


WF-Resumo-IA (BL3me3JQiVmSbzn3)

Trigger: Manual

Gera resumos de conteúdo via IA para alimentar a tabela pool_conteudos. Usado quando novos conteúdos (artigos do blog, materiais) precisam ser adicionados ao pool de nutrição.


Tabela nutricao_fila (PostgreSQL)

CREATE TABLE nutricao_fila (
  id SERIAL PRIMARY KEY,
  activity_id INTEGER UNIQUE NOT NULL,
  person_id INTEGER NOT NULL,
  activity_data JSONB,
  activity_type TEXT,
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'done', 'error')),
  created_at TIMESTAMP DEFAULT NOW(),
  processed_at TIMESTAMP,
  error_message TEXT
);
CREATE INDEX idx_nutricao_fila_pending ON nutricao_fila (status, id) WHERE status = 'pending';

Ciclo de vida: pendingprocessing (pick atômico com FOR UPDATE SKIP LOCKED) → done/error

Operação: Items processados ficam como done. Não há limpeza automática — acumulam. Se necessário, limpar periodicamente com DELETE FROM nutricao_fila WHERE status = 'done' AND processed_at < NOW() - INTERVAL '30 days'.


Segmentação de clusters e cadência (proposta 2026-07-03 — NÃO implementada ainda)

Ver tabelas completas (IDs de filtro, contagens, campos) em REFERENCIA_TECNICA.md. Cronologia da decisão em HISTORICO.md.

Motivação

Reunião de 2026-07-02 (Enrico Bertanha + Gustavo Sander) pediu clusterizar a base do Pipedrive pra definir cadência de nutrição (conteúdo) e reativação (comercial) por canal, e dimensionar quantos números de WhatsApp o Conteúdo Infinito precisa. O Pipedrive já tem campos RFM Segment/RFM Score nativos, mas eles cobrem só 805 de 59.617 pessoas (2,3% da base) e não são recalculados desde a criação em 2024-08-21 — decisão do Rafael foi abandonar RFM por enquanto e segmentar com campos nativos de contagem de negócios, que qualquer pessoa consegue auditar.

Os 6 clusters (campos nativos, sem RFM)

Baseados em won_deals_count, open_deals_count, lost_deals_count, last_activity_date (corte de 2 anos), excluindo Junk e opt-out. Cada cluster vira um filtro real e clicável no Pipedrive — não um número calculado à parte. IDs e links em REFERENCIA_TECNICA.md.

Cluster Critério
1 — Frio won_deals_count = 0, ativo -2a (funde os antigos "nunca abriu" + "já orçou nunca fechou" — mesma coisa na prática, won=0)
2 — Cliente ativo won_deals_count = 1, sem outro negócio aberto/perdido, ativo -2a
3 — Cliente recorrente won_deals_count = 1 MAS open_deals_count > 0 OU lost_deals_count > 0 (orçou mais de 1x, só converteu 1x), ativo -2a
4 — Recorrente ativo won_deals_count >= 2, ativo -2a
5 — Cliente adormecido won_deals_count = 1, sem atividade há +2a
6 — Recorrente adormecido won_deals_count >= 2, sem atividade há +2a

Modelo de cadência — um calendário por canal, tipo alternado

Erro corrigido pelo Rafael: a primeira versão definia frequências independentes de nutrição e reativação no mesmo canal (ex.: WhatsApp nutrição a cada 45 dias E reativação a cada 21 dias). Dois relógios independentes colidem (mesma pessoa recebe as duas mensagens no mesmo dia quando os ciclos coincidem) — imprevisível e difícil de explicar.

Modelo final: cada canal (email, WhatsApp) tem uma única frequência de contato por cluster. O tipo da mensagem simplesmente alterna a cada toque: nutrição, reativação, nutrição, reativação... Exemplo — Cluster 4, WhatsApp a cada 18 dias: dia 0 = nutrição, dia 18 = reativação, dia 36 = nutrição, dia 54 = reativação. Resultado: cada tipo específico ocorre a cada 2× a frequência de contato, sem colisão possível, e o "número de negócio" fica sendo só um: a frequência do canal.

Cluster 1 (Frio) é exceção: não tem reativação (nunca fechou negócio, não há relação comercial prévia pra reativar) — só nutrição, sem alternância.

Dimensionamento de WhatsApp

Cadência final soma ~256 msgs/dia (cluster Frio sozinho = ~172/dia, 89% da base — carrega o toque mais espaçado). Recomendação: 10 números (30 msgs/dia cada = 300/dia, ~15% de folga). Hoje o Conteúdo Infinito opera com 6 números (Vendas1-6) — faltam 4.

Status e pendências de implementação

Aprovada em princípio pelo Enrico (2026-07-08). Ainda NÃO está implementada no WF3 — o WF3 hoje continua lendo frequencia_email/frequencia_whatsapp (mensal/bimestral/semestral, sem distinção nutrição/reativação — ver campos em REFERENCIA_TECNICA.md). Pra implementar o modelo de calendário alternado, o WF3 vai precisar de: 1. Um campo por pessoa indicando o cluster atual (ou recalcular via filtro a cada ciclo). 2. Um campo/estado indicando o tipo do último toque enviado (nutrição ou reativação), pra saber qual mandar no próximo ciclo. 3. Nova tabela de frequência por cluster (ver REFERENCIA_TECNICA.md) substituindo o mapeamento atual mensal/bimestral/semestral.

Próximo passo: Rafael vai abrir uma sessão dedicada só pra planejar essa infraestrutura (não é continuação automática desta sessão) — ver HISTORICO.md, entrada 2026-07-08.


Decisões de design importantes

  1. Fila mode vs. Loop direto: Mudamos de Loop Over Items (v1) para fila PostgreSQL (v2) em maio 2026 para evitar race conditions, sobrecarga de IA, e duplicatas. O WF1 busca tudo do Pipedrive de uma vez mas processa 3 por ciclo.

  2. Prompt v2 (mensagens curtas): Limite de 2 frases / 35 palavras. Sem nome próprio do remetente (bug "Thiago" — IA inventava nomes). 4 tipos de pergunta com objetivo de vendas claro.

  3. Sem contato = dead end: Leads sem email/phone têm atividade fechada sem nova. Evita loops infinitos.

  4. Unsubscribe validation no WF3: Checagem de aceita_comunicacao ANTES de criar próxima atividade. Granular por canal (Só Email, Só WhatsApp, Ambos, Nenhum).

  5. SES raw body: Node nativo do n8n double-encodes @. Solução: HTTP Request com encoding manual.

  6. Make callback com filtro de tag:tag="nutricao" dispara callback pro WF3, protegendo outros fluxos.

  7. RFM abandonado em favor de clusters nativos (2026-07-03): RFM cobria só 2,3% da base e não era recalculado desde 2024-08. Substituído por 6 clusters baseados em contagem de negócios (won/open/lost) + última atividade, cada um com filtro real e clicável no Pipedrive. Ver seção "Segmentação de clusters" acima — proposta ainda não implementada no WF3.

  8. Cadência: um calendário por canal, tipo alternado, não dois relógios independentes. Nutrição e reativação alternam no mesmo calendário (nutrição, reativação, nutrição...) em vez de rodar em frequências separadas que podem colidir no mesmo dia. Ver seção acima.

  9. Guard de idempotência no WF3 é fail-open, não fail-closed (2026-07-03/04). Se a checagem de "já existe atividade aberta" falhar (erro de API, resposta malformada), o WF3 segue e cria a atividade mesmo assim, em vez de bloquear silenciosamente. Motivo: a lição do bug do WF4 (fail-closed silencioso derruba lead da cadência sem ninguém perceber por meses) pesa mais que o risco raro de uma duplicata ocasional, que é visível e fácil de limpar. O erro fica marcado (guard_error: true) para auditoria, não escondido.