La mayor parte de la inteligencia competitiva dentro de equipos de ventas B2B llega de la forma equivocada: un rep pierde un deal, postea en #lost-deals que el prospect mencionó un nuevo tier de pricing del competidor, y el resto del equipo se entera tres semanas después. El costo del descubrimiento tardío se acumula — cada deal que cierra en esa ventana entra a la conversación poco preparado. Este flujo es el arreglo barato y aburrido. Un cron diario crawlea una lista de páginas de competidores que de hecho te importan, normaliza el HTML para descartar ruido de deploy, le pide a Claude que resuma qué cambió materialmente (y que devuelva NO_CHANGE cuando el diff es cosmético), y postea un único digest semanal a Slack para que el canal se mantenga lo suficientemente denso en señal como para que los reps todavía lo abran después de un mes.
El bundle del artefacto en apps/web/public/artifacts/competitive-intel-tracker-n8n/ contiene el workflow de n8n importable (competitive-intel-tracker-n8n.json, 20 nodos a través de tres triggers) y _README.md con setup de credenciales, las dos tablas de Postgres que necesitas crear, y una verificación de primera corrida de seis pasos que ejercita tanto la rama de skip-por-no-materialidad como el slash command on-demand de Slack.
Cuándo usarlo
Tienes entre cinco y quince competidores contra los que te posicionas activamente, puedes nombrar de tres a cinco páginas públicas por competidor que cambian de formas que importan (pricing, posicionamiento de producto, señal de hiring que sugiera estrategia), y tienes al menos un canal de Slack que el equipo de ventas genuinamente abre. Estás dispuesto a mantener una lista de URLs trackeadas a medida que los competidores reestructuran sus sitios. Tienes una base de datos Postgres (u otro almacén al que puedas adaptar las queries) y una instancia de n8n alcanzable desde el internet público si quieres que el slash command on-demand funcione.
Esta también es la forma correcta si previamente intentaste un artilugio RSS de “alerta de Slack en cada post de blog del competidor” y el equipo lo muteó dentro de una semana — el filtro de materialidad y la cadencia semanal aquí son respuestas directas a ese modo de falla.
Cuándo NO usarlo
No montes esto si tu set competitivo está dominado por agregadores de reviews JS-heavy como G2, Capterra o TrustRadius. Su HTML público es una cáscara — el contenido real de las reviews se renderiza client-side o detrás de autenticación, y crawlearlos respetuosamente te devolverá casi nada. Paga por un vendor que los maneje (Crayon, Klue, Kompyte) o sáltate esas fuentes por completo.
No uses esto si tu equipo necesita la intel en tiempo real — por ejemplo, un ciclo de deal que rota dentro de una semana y cuyas calls de discovery dependen del cambio de pricing del competidor de ayer. La cadencia aquí es fetch diario, digest semanal. Si necesitas latencia bajo una hora, estás comprando un producto distinto (alertas de Klue) o construyendo un workflow distinto (webhooks de cambio por página alimentados a DMs de Slack del rep, no un digest).
No uses esto contra superficies privadas del competidor (trials gateados, portales de cliente pagos, cualquier cosa detrás de login). Crawlear esos está en una clase ética y legal distinta a chequear páginas de marketing públicas, y este flujo no es el sustrato correcto para ello.
No uses esto para menos de tres competidores. El costo de setup (veinte a treinta filas de páginas trackeadas, schema, credenciales, tuning de materialidad) no se paga si estás mirando uno o dos — un Google Alert y un recordatorio de calendario es la respuesta correcta a esa escala.
Setup
Lee apps/web/public/artifacts/competitive-intel-tracker-n8n/_README.md de punta a punta antes de importar. La versión corta: importa competitive-intel-tracker-n8n.json vía Import from File de n8n, crea las dos tablas de Postgres (competitor_tracked_pages y competitor_change_log) con el DDL del README, conecta cuatro credenciales (PLACEHOLDER_POSTGRES_CRED_ID, PLACEHOLDER_ANTHROPIC_CRED_ID, PLACEHOLDER_SLACK_CRED_ID, más la URL opcional de webhook del slash command de Slack), define la timezone del workflow explícitamente en Settings, siembra la tabla de tracked-pages con veinte a treinta filas, y recorre la verificación de primera corrida de seis pasos antes de activar. La verificación deliberadamente ejercita la ruta sin snapshot previo, la ruta cheap-no-change, la ruta de diff forzado, la ruta de skip por no-materialidad, la ruta del digest, y el webhook on-demand — seis ramas, seis inputs pequeños.
Qué hace realmente el flujo
El crawler es un loop splitInBatches con batchSize: 1 para que la falla de una sola página no aborte la corrida. Cada iteración duerme cuatro segundos antes del HTTP fetch — eso reparte treinta páginas en dos minutos, lo que te mantiene bien por debajo de cualquier rate limit razonable por host y se lee como un bot educado en los logs del servidor. El nodo httpRequest define neverError: true porque un 403 de defensas anti-bot debería registrarse y saltarse, no crashear el workflow.
El gate de materialidad es un AND de cuatro condiciones: el fetch tuvo éxito, el hash difiere del snapshot previo, existe un snapshot previo, y el delta de longitud excede 0,5%. El término de delta de longitud es el pre-filtro barato que ahorra llamadas a Claude — ediciones de un solo carácter o solo de whitespace nunca llegan al modelo. El término “tenía-snapshot-previo” es lo que hace barata la primerísima corrida: una página trackeada nuevita captura su hash baseline y se salta el diff por completo.
La llamada a Claude envía ambos snapshots truncados a 6000 caracteres cada uno (aproximadamente 1500 tokens cada uno, más system prompt y overhead → alrededor de 3500 tokens de entrada por página material). El system prompt fuerza una elección binaria: devolver NO_CHANGE si el diff es cosmético, solo de navegación, solo de footer, o no identificable, o devolver exactamente dos oraciones — qué cambió y por qué a un vendedor le debería importar. El nodo Parse trata NO_CHANGE como un sentinel y voltea is_material = false para que la fila igual quede logueada para auditoría pero nunca llegue al digest.
El agregador de digest del lunes a las 14:30 corre una sola query SQL que agrupa los cambios materiales de los últimos siete días por competidor, y luego renderiza un mensaje de Slack Block Kit por competidor — no un mega-post. Los reps de ventas mutean digests largos sin cortes; los mensajes por competidor son scaneables y threadeables. Las semanas silenciosas (sin cambios materiales en ningún lado) no postean nada. El webhook on-demand es un tercer trigger, completamente independiente: consume un POST de slash command de Slack, corre una query de match LIKE contra el change log de los últimos 90 días, y responde con hasta diez bloques formateados de forma efímera al usuario que solicitó.
Realidad de costos
Por corrida de crawl, con 30 páginas trackeadas y un típico 3-5 de ellas cambiando materialmente: aproximadamente 11.000 tokens de entrada y 1.000 tokens de salida contra claude-sonnet-4-6, lo que aterriza en cerca de $0,05 por corrida. Diariamente por 30 días: ~$1,50/mes en gasto de Claude. n8n self-hosted: $0 incremental; n8n Cloud Starter: $20/mes standalone o $0 si ya lo corres para otros flujos. Postgres: unos pocos megabytes de almacenamiento si guardas el change log indefinidamente (la columna last_content_text es la pesada — 30 filas × ~50KB ≈ 1,5MB total, creciendo lento).
Wall-clock por corrida: ~2,5 minutos (30 páginas × 4s de throttle + latencia de Claude para las materiales). Digest de Slack: bajo 5 segundos. Webhook on-demand: bajo 2 segundos para la respuesta.
Tiempo de operador: 30-60 minutos una vez por trimestre para refrescar la lista de tracked-pages cuando los competidores reestructuran sus sitios, más ~5 minutos la primera vez que alguien reporte un falso positivo (“el digest dijo que el pricing cambió pero no fue así”) para tunear el umbral de materialidad o agregar un patrón de máscara de ruido.
Cómo se ve el éxito
Métrica concreta a vigilar las primeras ocho semanas: open-rate del digest o equivalente a read-receipt en Slack (puedes proxearlo por conteo de reacciones o sondeando manualmente a los reps). Si menos del 30% del canal lee el digest, la relación señal-a-ruido es muy baja — ajusta el umbral de materialidad (sube el gate de delta de longitud de 0,5% a 1%), tira los page types de menor señal (las páginas de hiring de competidores con una página permanente de open-jobs que rota semanalmente son usualmente ruido), o fusiona competidores de baja frecuencia en una sección de digest “long tail”. Si más del 60% lo lee consistentemente, construiste lo correcto y el siguiente movimiento es agregar una ruta on-demand para el caso de uso de discovery-call (ya cableado — solo publicita el slash command).
Una segunda métrica: número de veces en un trimestre que un rep cita el digest en un thread #won-deals o #lost-deals. Cinco citas por trimestre desde un equipo de 20 reps es una buena señal; cero citas después de dos meses significa que o el digest no se lee o el contenido es no accionable.
Versus las alternativas
Klue o Crayon ($30k-$80k/año por el tier SMB de cualquiera, último chequeo Q1 2026) maneja las fuentes JS-heavy de agregadores de reviews que no puedes crawlear vos mismo, despacha una experiencia de consumidor pulida para el equipo de ventas (battlecards, temas de win/loss, hub de intel), e incluye una capa de curación humana que captura el matiz que Claude se pierde. Si tu intel competitiva es lo bastante central a un ciclo de deal como para que tengas a una persona de inteligencia competitiva full-time, compra Klue o Crayon. Este flujo es la respuesta correcta cuando estás corriendo una org de 20 reps sin un hire dedicado de CI y necesitas dejar de descubrir cambios de pricing del competidor desde tus propios threads de lost-deals — te lleva al 70% del valor al 1% del costo.
Visualping o Distill.io (bajo $10/mes) hacen bien la capa de detección de cambio de página, pero se detienen en “esta página cambió” y vuelcan el diff en tu inbox. El trabajo interesante — convertir un diff en “esto es lo que tu equipo de ventas necesita decir distinto” — es exactamente lo que Claude hace aquí. Podrías pegar Visualping a n8n y bypassear la mitad de crawler/hasher de este flujo si quisieras outsourcear la preocupación de polite-crawler; el filtro de materialidad y la etapa de diff con Claude son las partes que de verdad importan.
Un único feed de Google Alerts es lo que la mayoría de los equipos default y lo que la mayoría de los equipos calladamente dejan de leer después de un mes. Google Alerts dispara con menciones de prensa, no con cambios de página; se pierde por completo las ediciones de página de pricing (la página no obtiene una nueva entrada de índice de noticias); y el volumen está dominado por ruido de press release sindicado. Usa Alerts como complemento de este flujo para señal de prensa, no como reemplazo del sustrato de monitoreo de páginas.
Un crawler bespoke en Python sobre un cron job en tu data warehouse es lo que cada staff engineer quiere construir. Lo van a tener funcionando en un sprint, la capa de diff funcionando en un sprint después, el formato de Slack funcionando en un sprint después, y entonces nadie va a ser dueño de él cuando el ingeniero cambie de equipo. La razón para usar n8n acá es que hace el workflow visible (el grafo es la documentación), editable por un no-ingeniero (la persona de marketing ops puede agregar una página trackeada sin un PR), y lo bastante aburrido como para sobrevivir a la persona que lo construyó.
Watch-outs
Bloqueos anti-bot devuelven 403/503 y tu hash silenciosamente se queda obsoleto. Guard: el nodo Fetch Page HTML define neverError: true y la condición fetch_ok del gate de materialidad (status 200-399 AND body length > 200 bytes) enruta los fetches fallidos a la rama false — quedan logueados pero nunca llegan a Claude ni al digest. Agrega una query semanal contra competitor_change_log para páginas cuyo last_seen_at sea mayor a 7 días y trata eso como el reporte de “tracked pages obsoletas”.
Claude alucina un cambio cuando el diff normalizado está sucio (por ejemplo, un rename de clase CSS tocó cada <div> y el texto strippeado no se recuperó del todo). Guard: la escape hatch del system prompt es el string literal NO_CHANGE, y el parser trata cualquier cosa que matchee ^NO_CHANGE\b (case-insensitive) como no material. Cuando veas una entrada de digest obviamente errónea, el fix es agregar un patrón de máscara de ruido en el nodo Code Normalize + Hash, no bajar la temperatura del modelo.
El canal de Slack se mutea dentro de cuatro semanas de salir vivo si incluso el 20% de las entradas del digest son no materiales. Guard: cadencia semanal en lugar de diaria (el cron de digest bundleado es 30 14 * * 1, lunes 14:30 únicamente), el piso de delta de longitud de materialidad en 0,5%, el sentinel NO_CHANGE de Claude, y el gate IF de semanas-silenciosas-quedan-silenciosas que suprime el digest por completo cuando ningún competidor tiene cambios materiales. Si los reps igual lo mutean, el siguiente dial a girar es tirar los page_type de menor señal de la lista de tracked-pages — usualmente páginas de hiring.
Nombres largos de competidores o grandes volúmenes de cambio sobrepasan el límite de 50 bloques por mensaje de Slack. Guard: un mensaje por competidor (no un mega-post), así el cap es por competidor y no por semana. Si un solo competidor genuinamente tiene más de ~15 cambios materiales en una semana, eso en sí mismo es una señal de que el umbral de materialidad necesita subir para ese competidor específicamente.
El slash command on-demand filtra inteligencia competitiva a cualquiera en el workspace porque los slash commands de Slack no enforzan membresía de canal. Guard: el respondToWebhook devuelve response_type: "ephemeral" para que solo el usuario que solicitó vea el resultado, y la query está acotada al change log (no se devuelve texto crudo de página). Si necesitas control de acceso más estricto, gateá el slash command sobre un user-group ID de Slack en el nodo Code Parse Slash Command antes de correr la query SQL.
Stack
n8n — tres triggers (cron de fetch diario, cron de digest semanal, webhook on-demand), HTTP fetcher, normalizer, gate de materialidad, persistencia
Postgres — competitor_tracked_pages (la lista source-of-truth, 20-30 filas) y competitor_change_log (audit trail de cada cambio detectado, material o no)
Claude Sonnet 4.6 — la etapa de diff-y-resumen, con el sentinel NO_CHANGE como escape hatch
Slack — el canal de distribución del digest y la superficie del 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.