A maior parte da inteligência competitiva em times de vendas B2B chega da forma errada: um rep perde um deal, posta em #lost-deals que o prospect mencionou um novo tier de pricing de um concorrente, e o resto do time descobre três semanas depois. O custo da descoberta tardia se acumula — todo deal fechando nessa janela entra na conversa despreparado. Esse flow é a correção barata e chata. Um cron diário rastreia uma lista de páginas de concorrentes com as quais você de fato se importa, normaliza o HTML para descartar ruído de deploy, pede ao Claude para resumir o que mudou materialmente (e devolver NO_CHANGE quando o diff é cosmético), e posta um único digest semanal no Slack para que o canal continue denso de sinal o suficiente para que os reps ainda abram depois de um mês.
O bundle do artefato em apps/web/public/artifacts/competitive-intel-tracker-n8n/ contém o workflow importável de n8n (competitive-intel-tracker-n8n.json, 20 nós em três triggers) e o _README.md com setup de credenciais, as duas tabelas Postgres que você precisa criar, e uma verificação de primeira execução em seis passos que exercita tanto o branch de materiality-skip quanto o slash command on-demand do Slack.
Quando usar isto
Você tem entre cinco e quinze concorrentes contra os quais se posiciona ativamente, consegue nomear de três a cinco páginas públicas por concorrente que mudam de formas que importam (pricing, posicionamento de produto, sinal de hiring que sugere estratégia), e tem ao menos um canal no Slack que o time de vendas genuinamente abre. Você está disposto a manter uma lista de URLs acompanhadas conforme concorrentes reestruturam seus sites. Você tem um banco Postgres (ou outro store ao qual adaptar as queries) e uma instância n8n alcançável pela internet pública se quiser que o slash command on-demand funcione.
Essa é também a forma certa se você já tentou uma geringonça do tipo “alerta no Slack a cada blog post de concorrente” via RSS e o time silenciou o canal em uma semana — o filtro de materialidade e a cadência semanal aqui são respostas diretas a esse modo de falha.
Quando NÃO usar isto
Não monte isso se seu conjunto competitivo é dominado por agregadores de review pesados em JS como G2, Capterra ou TrustRadius. O HTML público deles é uma casca — o conteúdo real das reviews é renderizado no cliente ou atrás de autenticação, e crawl respeitoso te devolve quase nada. Pague por um vendor que cuida deles (Crayon, Klue, Kompyte) ou pule essas fontes inteiramente.
Não use isso se seu time precisa da intel em tempo real — por exemplo, um ciclo de deal que vira em uma semana e cujas discovery calls dependem da mudança de pricing do concorrente de ontem. A cadência aqui é fetch diário, digest semanal. Se você precisa de latência abaixo de uma hora, está comprando outro produto (alertas do Klue) ou construindo outro workflow (webhooks de mudança por página alimentando DMs no Slack para os reps, não um digest).
Não use isso contra superfícies privadas do concorrente (trials gated, portais pagos de cliente, qualquer coisa atrás de login). Crawlear isso está em outra classe ética e jurídica do que checar páginas públicas de marketing, e esse flow não é o substrato certo para isso.
Não use isso para menos de três concorrentes. O custo de setup (vinte a trinta linhas de páginas acompanhadas, schema, credenciais, tuning de materialidade) não paga se você está vigiando um ou dois — um Google Alert e um lembrete no calendário é a resposta certa nessa escala.
Setup
Leia apps/web/public/artifacts/competitive-intel-tracker-n8n/_README.md de ponta a ponta antes de importar. Versão curta: importe competitive-intel-tracker-n8n.json via Import from File do n8n, crie as duas tabelas Postgres (competitor_tracked_pages e competitor_change_log) com o DDL do README, conecte quatro credenciais (PLACEHOLDER_POSTGRES_CRED_ID, PLACEHOLDER_ANTHROPIC_CRED_ID, PLACEHOLDER_SLACK_CRED_ID, mais a URL opcional de webhook do slash command do Slack), defina o timezone do workflow explicitamente em Settings, popule a tabela de páginas acompanhadas com vinte a trinta linhas, e percorra a verificação de primeira execução em seis passos antes de ativar. A verificação intencionalmente exercita o caminho sem snapshot prévio, o caminho barato de no-change, o caminho de diff forçado, o caminho de materiality-skip, o caminho de digest, e o webhook on-demand — seis branches, seis inputs pequenos.
O que o flow faz de fato
O crawler é um loop splitInBatches com batchSize: 1 para que uma falha em uma única página não aborte a execução. Cada iteração dorme quatro segundos antes do HTTP fetch — isso espalha trinta páginas por dois minutos, o que te mantém bem abaixo de qualquer rate limit razoável por host e aparece como um bot polido nos logs de servidor. O nó httpRequest seta neverError: true porque um 403 das defesas anti-bot deve ser registrado e pulado, não fazer o workflow crashar.
O gate de materialidade é um AND de quatro condições: fetch teve sucesso, hash difere do snapshot anterior, existe algum snapshot anterior, e o delta de length passa de 0,5%. O termo de length-delta é o pré-filtro barato que poupa chamadas ao Claude — edições de um único caractere ou só de whitespace nunca chegam ao modelo. O termo “had-prior-snapshot” é o que torna a primeira execução barata: uma página recém-adicionada captura seu hash baseline e pula o diff inteiramente.
A chamada ao Claude envia os dois snapshots truncados para 6000 caracteres cada (cerca de 1500 tokens cada, mais system prompt e overhead → em torno de 3500 input tokens por página material). O system prompt força uma escolha binária: devolver NO_CHANGE se o diff é cosmético, só de navegação, só de rodapé ou não identificável, ou devolver exatamente duas frases — o que mudou e por que um vendedor deveria se importar. O nó Parse trata NO_CHANGE como sentinela e vira is_material = false para que a linha ainda seja logada para auditoria mas nunca chegue ao digest.
O agregador do digest de segunda 14:30 roda uma query SQL que agrupa as mudanças materiais dos últimos sete dias por concorrente, e renderiza uma mensagem em Slack Block Kit por concorrente — não um mega-post único. Reps de vendas silenciam digests longos e sem quebra; mensagens por concorrente são escaneáveis e threadable. Semanas silenciosas (sem nenhuma mudança material) não postam nada. O webhook on-demand é um terceiro trigger, completamente independente: consome um POST de slash command do Slack, roda uma query com match LIKE contra o change log dos últimos 90 dias, e responde com até dez blocos formatados de forma ephemeral ao usuário que pediu.
A realidade do custo
Por execução de crawl, com 30 páginas acompanhadas e tipicamente 3-5 delas mudando materialmente: cerca de 11.000 input tokens e 1.000 output tokens contra claude-sonnet-4-6, o que dá em torno de US$ 0,05 por execução. Diário por 30 dias: ~US$ 1,50/mês de gasto em Claude. n8n self-hosted: US$ 0 incremental; n8n Cloud Starter: US$ 20/mês isolado ou US$ 0 se você já roda para outros flows. Postgres: alguns megabytes de storage se você mantiver o change log indefinidamente (a coluna last_content_text é a pesada — 30 linhas × ~50KB ≈ 1,5MB no total, crescendo devagar).
Wall-clock por execução: ~2,5 minutos (30 páginas × throttle de 4s + latência do Claude para as materiais). Digest do Slack: abaixo de 5 segundos. Webhook on-demand: abaixo de 2 segundos para a resposta.
Tempo de operador: 30-60 minutos uma vez por trimestre para refrescar a lista de páginas acompanhadas quando concorrentes reestruturam seus sites, mais ~5 minutos da primeira vez que alguém reporta um falso positivo (“o digest disse que o pricing mudou mas não mudou”) para ajustar o threshold de materialidade ou adicionar um padrão de noise-mask.
Como é o sucesso
Métrica concreta para observar nas primeiras oito semanas: taxa de abertura do digest ou equivalente a read-receipt no Slack (você pode proxiar isso por contagem de reactions ou pesquisando os reps manualmente). Se menos de 30% do canal lê o digest, a razão sinal-ruído está baixa demais — aperte o threshold de materialidade (suba o gate de length-delta de 0,5% para 1%), tire os tipos de página de menor sinal (páginas de hiring de concorrentes com uma página de vagas abertas permanente que muda toda semana costumam ser ruído), ou agrupe concorrentes de baixa frequência em uma seção “long tail” do digest. Se mais de 60% lê consistentemente, você construiu a coisa certa e o próximo movimento é adicionar um caminho on-demand para o uso em discovery call (já conectado — basta divulgar o slash command).
Segunda métrica: número de vezes em um trimestre que um rep cita o digest em uma thread de #won-deals ou #lost-deals. Cinco citações por trimestre num time de 20 reps é bom sinal; zero citações depois de dois meses significa que ou o digest está sem ser lido ou o conteúdo não é acionável.
Versus as alternativas
Klue ou Crayon (US$ 30k-US$ 80k/ano no tier SMB de cada um, conferido pela última vez no Q1 de 2026) cuida das fontes pesadas em JS de agregador de reviews que você não consegue crawlear, entrega uma experiência polida ao time de vendas (battlecards, temas de win/loss, hub de intel), e inclui uma camada de curadoria humana que pega nuance que o Claude perde. Se sua intel competitiva é central o bastante para um ciclo de deal a ponto de você ter uma pessoa dedicada full-time a competitive intel, compre Klue ou Crayon. Esse flow é a resposta certa quando você está tocando uma org de 20 reps sem uma contratação dedicada de CI e precisa parar de descobrir mudanças de pricing de concorrentes a partir de threads de deals perdidos — entrega 70% do valor a 1% do custo.
Visualping ou Distill.io (abaixo de US$ 10/mês) faz bem a camada de detecção de mudança de página, mas para em “essa página mudou” e despeja o diff no seu inbox. O trabalho interessante — transformar um diff em “aqui está o que seu time de vendas precisa dizer diferente” — é exatamente o que o Claude faz aqui. Você poderia plugar o Visualping no n8n e pular a metade de crawler/hasher desse flow se quisesse terceirizar a preocupação com crawler polido; o filtro de materialidade e o estágio de diff do Claude são as partes que de fato importam.
Um único feed de Google Alerts é o que a maioria dos times faz como default e o que a maioria dos times silenciosamente para de ler depois de um mês. Google Alerts dispara em menções de imprensa, não em mudanças de página; ele perde edições de página de pricing inteiramente (a página não ganha uma entrada nova no índice de notícias); e o volume é dominado por ruído de press release sindicalizado. Use Alerts como complemento desse flow para sinal de imprensa, não como substituto do substrato de monitoramento de página.
Um crawler Python sob medida num cron job no seu data warehouse é o que todo staff engineer quer construir. Vai botar o crawler de pé em uma sprint, a camada de diff em uma sprint depois, a formatação do Slack em uma sprint depois, e aí ninguém vai dono quando o engineer mudar de time. A razão para usar n8n aqui é que torna o workflow visível (o grafo é a documentação), editável por uma pessoa não-engineer (a pessoa de marketing ops adiciona uma página acompanhada sem PR), e chato o suficiente para sobreviver à pessoa que construiu.
Pontos de atenção
Bloqueios anti-bot devolvem 403/503 e seu hash silenciosamente fica desatualizado. Guarda: o nó Fetch Page HTML seta neverError: true e a condição fetch_ok do gate de materialidade (status 200-399 E body length > 200 bytes) roteia fetches falhos para o branch falso — são logados mas nunca chegam ao Claude ou ao digest. Adicione uma query semanal contra competitor_change_log para páginas cujo last_seen_at é mais antigo que 7 dias e trate isso como o relatório de “páginas acompanhadas desatualizadas”.
Claude alucina uma mudança quando o diff normalizado está bagunçado (ex.: um rename de classe CSS tocou todo <div> e o texto despido não se recuperou bem). Guarda: a saída de escape do system prompt é a string literal NO_CHANGE, e o parser trata qualquer coisa que bate em ^NO_CHANGE\b (case-insensitive) como não-material. Quando você vê uma entrada de digest obviamente errada, a correção é adicionar um padrão de noise-mask no nó Code Normalize + Hash, não baixar a temperatura do modelo.
O canal do Slack é silenciado em quatro semanas após o go-live se mesmo 20% das entradas de digest forem não-materiais. Guarda: cadência semanal em vez de diária (o cron de digest empacotado é 30 14 * * 1, segunda 14:30 apenas), o piso de length-delta de materialidade em 0,5%, a sentinela NO_CHANGE do Claude, e o gate IF de semanas-silenciosas-ficam-silenciosas que suprime o digest inteiramente quando nenhum concorrente tem mudanças materiais. Se os reps ainda silenciarem, o próximo botão a girar é tirar os valores de page_type de menor sinal da lista de páginas acompanhadas — geralmente páginas de hiring.
Nomes longos de concorrente ou volumes grandes de mudança estouram o limite de 50 blocos por mensagem do Slack. Guarda: uma mensagem por concorrente (não um mega-post), então o teto é por concorrente, não por semana. Se um único concorrente genuinamente tem mais de ~15 mudanças materiais em uma semana, isso em si é um sinal de que o threshold de materialidade precisa subir para aquele concorrente especificamente.
O slash command on-demand vaza intel competitiva para qualquer um no workspace porque slash commands do Slack não impõem a membership do canal. Guarda: o respondToWebhook devolve response_type: "ephemeral" para que só o usuário que pediu veja o resultado, e a query é escopada ao change log (sem texto bruto da página devolvido). Se você precisa de controle de acesso mais estrito, gate o slash command em um ID de user-group do Slack no nó Code Parse Slash Command antes de rodar a query SQL.
Stack
n8n — três triggers (cron de fetch diário, cron de digest semanal, webhook on-demand), HTTP fetcher, normalizador, gate de materialidade, persistência
Postgres — competitor_tracked_pages (a lista source of truth, 20-30 linhas) e competitor_change_log (trilha de auditoria de toda mudança detectada, material ou não)
Claude Sonnet 4.6 — o estágio de diff-and-summarize, com a sentinela NO_CHANGE como saída de escape
Slack — o canal de distribuição do digest e a superfície do slash command on-demand
# Competitive intel tracker — n8n bundle
## What this flow does
A daily cron pulls a list of tracked competitor pages from Postgres, fetches each one with a real user-agent and a 4-second throttle, normalizes the HTML by stripping volatile noise (script blocks, build IDs, server-rendered timestamps, current-year strings), hashes the result, and compares it to the previously stored hash. Pages whose hash and length-delta both clear a materiality threshold get diffed by Claude Sonnet against the prior snapshot; the model is instructed to return the literal string `NO_CHANGE` when the diff is cosmetic. Material summaries land in a `competitor_change_log` table. A second cron fires Mondays at 14:30 and aggregates the last seven days of material changes into one Slack Block Kit message per competitor — silent weeks stay silent. A third trigger (a Slack slash command webhook) lets sales reps query the same change log on demand for a single competitor over the last 90 days.
## Import
1. In n8n, open the workflow list and click **Import from File** in the top-right kebab menu.
2. Select `competitive-intel-tracker-n8n.json`.
3. Confirm the workflow opens with 20 nodes across three triggers (the daily crawler, the weekly digest, and the on-demand webhook). The graph should read left-to-right with the digest below the crawler and the webhook below that.
4. Open **Settings** on the workflow and confirm `executionOrder: v1` and a sensible `timezone` (the bundle ships `Europe/London` — change it to your team's working timezone before activating; Cron expressions are interpreted in this zone).
5. Do **not** activate yet. Wire credentials and create the database tables first (next two sections).
## Credentials
The flow references four credential placeholders by name. Each placeholder must be replaced with a real n8n credential of the matching type before the workflow will execute.
### `PLACEHOLDER_POSTGRES_CRED_ID` — Postgres (read/write)
Used by five nodes (`Pull Tracked Pages`, `Persist Change + Update Snapshot`, `Touch Snapshot (No Material Change)`, `Aggregate Last 7 Days Of Material Changes`, `Fetch On-Demand History`). Create an n8n **Postgres** credential pointing at the database that holds your tracked pages and change log. The bundle assumes two tables — create them with:
```sql
CREATE TABLE competitor_tracked_pages (
page_id bigserial PRIMARY KEY,
competitor_name text NOT NULL,
page_type text NOT NULL, -- 'pricing' | 'blog' | 'hiring' | 'reviews' | 'docs'
url text NOT NULL UNIQUE,
active boolean NOT NULL DEFAULT true,
last_content_hash text,
last_content_text text,
last_seen_at timestamptz
);
CREATE TABLE competitor_change_log (
id bigserial PRIMARY KEY,
page_id bigint REFERENCES competitor_tracked_pages(page_id) ON DELETE CASCADE,
competitor_name text NOT NULL,
page_type text NOT NULL,
url text NOT NULL,
content_hash text NOT NULL,
summary text NOT NULL,
is_material boolean NOT NULL,
detected_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON competitor_change_log (competitor_name, detected_at DESC);
CREATE INDEX ON competitor_change_log (detected_at DESC) WHERE is_material;
```
Seed `competitor_tracked_pages` with twenty to thirty rows before the first run. The recommended starter set per competitor: pricing page, two recent blog posts, careers/jobs index, docs landing page. Skip JS-heavy review sites (G2, Capterra, TrustRadius) unless you have a rendering service — the raw HTML they ship is mostly empty.
### `PLACEHOLDER_ANTHROPIC_CRED_ID` — Anthropic API key
Used by `Claude — Diff + Summarize`. Create an n8n **Header Auth** credential with header name `x-api-key` and value set to your Anthropic API key (find it at console.anthropic.com → API Keys). The flow uses `claude-sonnet-4-6` — change the model in the JSON if your account routes elsewhere. Token budget per run: roughly `(pages × ~3000 input tokens) + (material pages × ~200 output tokens)` — see the cost-reality section in the page body for absolute numbers.
### `PLACEHOLDER_SLACK_CRED_ID` — Slack bot token
Used by `Slack — Post Weekly Digest`. Create a Slack app at api.slack.com/apps, add the bot scopes `chat:write` and `chat:write.public` (the latter so the bot can post to channels it has not been explicitly invited to), install the app, and copy the **Bot User OAuth Token** (starts with `xoxb-`). Create an n8n **Header Auth** credential with header name `Authorization` and value `Bearer xoxb-...`. Update the channel name in the `Slack — Post Weekly Digest` node from `#competitive-intel` to whatever channel your sales team actually reads.
### Slash command (optional, no credential — webhook URL only)
The `On-Demand Webhook` node exposes a path at `/webhook/intel-on-demand`. To wire a Slack slash command to it: in your Slack app config, add a slash command (e.g. `/whatsnew`), set the request URL to your n8n public URL plus that path, and grant the `commands` scope. No n8n credential is needed because Slack POSTs to the webhook directly. If your n8n is not internet-reachable, either expose it via a tunnel or skip this trigger and run the on-demand query manually from the n8n editor.
## First-run verification
Run these in order. Each step proves a different branch of the flow.
1. **Insert one tracked page that you know changes daily** (a competitor's blog index works well). Verify with `SELECT * FROM competitor_tracked_pages;` that the row exists with `last_content_hash IS NULL`.
2. **Manually execute the `Daily Cron — 5am UTC` trigger** from the n8n editor. The first run should: fetch the page, compute a hash, *fail* the `Material Change?` IF (because there is no prior snapshot to compare — the `had-prior-snapshot` condition is false), and route to `Touch Snapshot (No Material Change)` which writes the initial hash. Confirm `competitor_tracked_pages.last_content_hash` is now populated and `competitor_change_log` is still empty.
3. **Manually execute the trigger a second time, immediately.** The hash should match (page didn't change in two minutes), the IF fails, no Claude call. This proves the cheap path.
4. **Edit the row to force a diff.** Run `UPDATE competitor_tracked_pages SET last_content_text = 'lorem ipsum placeholder', last_content_hash = 'force-diff' WHERE page_id = <id>;` and re-execute the trigger. The IF should now pass, Claude should be called, and you should see a row appear in `competitor_change_log`. Open the row and read the summary — it should describe the page in two sentences. If it returned `NO_CHANGE` despite the forced diff, lower the materiality threshold or check the truncation in the prompt.
5. **Test the no-op materiality filter.** Insert a row pointing at a page that has trivial dynamic content (e.g. a homepage with rotating testimonials). After the first snapshot is captured, re-run the cron. The hash will likely differ but the length delta should be small — confirm it routes to the false branch and does not spend a Claude call.
6. **Test the weekly digest.** Manually execute `Weekly Digest Cron — Mon 14:30`. If `competitor_change_log` has at least one `is_material = true` row from the last 7 days, you should see a Slack message land in the configured channel. If the table is empty for the window, no message fires — that is correct behavior, not a bug.
7. **Test the on-demand webhook.** From a terminal, `curl -X POST https://<your-n8n>/webhook/intel-on-demand -d 'text=acme'` (or trigger your wired Slack slash command). Expect a JSON response with up to 10 of the most recent material changes for any competitor whose name contains `acme`. With an empty change log, expect the "No material changes recorded" fallback.
8. **Activate the workflow** only after all six branches above behaved as described.