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: 2em todas as chains de IA - OpenRouter retry:
retryOnFail: true, maxTries: 3, waitBetweenTries: 5000em 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: pending → processing (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 emHISTORICO.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¶
-
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.
-
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.
-
Sem contato = dead end: Leads sem email/phone têm atividade fechada sem nova. Evita loops infinitos.
-
Unsubscribe validation no WF3: Checagem de
aceita_comunicacaoANTES de criar próxima atividade. Granular por canal (Só Email, Só WhatsApp, Ambos, Nenhum). -
SES raw body: Node nativo do n8n double-encodes
@. Solução: HTTP Request com encoding manual. -
Make callback com filtro de tag: Só
tag="nutricao"dispara callback pro WF3, protegendo outros fluxos. -
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.
-
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.
-
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.