Sales databases tell you what a company is (size, industry, stack), not what it’s doing right now. Yet in prospecting, timing changes everything. I built an n8n pipeline that detects, from public sources, companies that are likely in an expansion phase.
The problem
A company that opens 15 positions in six weeks, talks about geographic expansion on LinkedIn and announces a partnership on its blog is not in the same state as a company that hasn’t moved in 18 months. These signals are public, but scattered and noisy. And sales intelligence tools never show the reasoning behind their signals: I wanted to see the source, the content and the logic.
The real question
The question wasn’t “how to scrape data”. It was “how to turn messy public data into a commercially actionable signal, without mostly producing false positives”.
What I built
Three complementary sources:
- Welcome to the Jungle: active hiring (volume, contracts, locations).
- LinkedIn, via Apify to avoid maintaining a homegrown scraper (same logic as in the article on multi-source competitive monitoring). It’s the noisiest source: mostly employer branding.
- News pages (
/blog,/actualites,/news) for funding rounds, partnerships and launches, with the same heuristic as in the Scrapy technology detection project. Blogs without recent content are skipped.
flowchart TD
A[(Supabase\ncompanies)] --> B[n8n Scheduler]
B --> C{Loop Over Items}
C --> D[HTTP Algolia\nWTTJ]
C --> E[Apify\nLinkedIn]
C --> F[HTTP\nNews pages]
D --> G[Code\nParse Jobs]
E --> H[Code\nParse Posts]
F --> I[Code\nParse Content]
G --> J[Context\nassembly]
H --> J
I --> J
J --> K[AI Agent\nOpenRouter]
K --> L[JSON\nParsing]
L --> M[(Supabase\nsignals)]
Redis acts as a semaphore upstream of the loop: each iteration takes a slot (INCR), waits if the limit is reached, and releases it afterwards (DECR). A native Redis node is enough, no need for a full queue.
WTTJ exposes its Algolia index
I was heading for HTML scraping. Opening the network tab in DevTools, I saw that WTTJ loads its job listings through Algolia, with a search key present in the frontend. It’s not a vulnerability: Algolia’s Search-Only API Keys are designed to be exposed and only allow reads. A structured POST request therefore replaces all the HTML parsing:
POST https://<APP_ID>-dsn.algolia.net/1/indexes/wttj_jobs_production_fr/query
X-Algolia-Application-Id: <APP_ID>
X-Algolia-API-Key: <search-only key visible in WTTJ DevTools>
Content-Type: application/json
{"query": "", "hitsPerPage": 20, "filters": "organization.slug:doctolib"}
What would have taken several days in HTML took less than an hour. A Code node then extracts from the hits each job’s title, contract, remote policy, city, publication date and URL, plus the total (nbHits).
An AI Agent (openai/gpt-4o-mini via OpenRouter) then analyzes the assembled context. Two choices matter in the system prompt: a precise persona (“senior account manager looking for upsell opportunities”) rather than “analyze these job listings”, and a strict JSON-only response.
What broke
- The missing WTTJ slug.
organization.slugdoesn’t always match the domain or the name. I stored it in awttj_slug_computedfield in thecompaniestable. On the first runs, about 15% of companies had none: Algolia returned 0 hits, treated as “no open positions”. I added a log to tell the two cases apart. - Reposted listings. Some companies repost the same job every few weeks:
published_atchanges, the total goes up, nothing actually happens. Fix: a hash on(title, contract, location)per company, skipped if it already exists with a recent date. The rawtotal_jobsis still tracked separately. - Prompts that were too open. “Identify expansion signals” produced well-written text that was impossible to parse. Strict JSON and an explicit output taxonomy changed everything.
The classic trap
The model doesn’t “understand” the company. It completes patterns. If the prompt is vague, the output will be plausible but not reliable. Constraining the output format and the persona isn’t a detail: it’s the main tuning lever.
Current limits: the hash doesn’t catch reworded titles, LinkedIn depends on an Apify actor, scoring is still empirical, and only the wttj_jobs_production_fr index is queried.
Takeaways
Inspect the network before scraping: the best technical solution is often the one that invents nothing. Scraping was the easy part; the hard part is telling a real signal from a stale listing, knowing that a single signal means almost nothing (20 listings can mean growth or heavy turnover). Next step: a trend score based on how total_jobs evolves over a rolling 4 weeks, then checking whether companies flagged “in expansion” actually convert better.