Les bases de données commerciales disent ce qu’une entreprise est (taille, secteur, stack), pas ce qu’elle fait en ce moment. Or le bon moment change tout en prospection. J’ai construit un pipeline n8n qui détecte, à partir de sources publiques, les entreprises probablement en phase d’expansion.
Le problème
Une entreprise qui ouvre 15 postes en six semaines, parle d’expansion géographique sur LinkedIn et annonce un partenariat sur son blog n’est pas dans le même état qu’une entreprise immobile depuis 18 mois. Ces signaux sont publics, mais éparpillés et bruités. Et les outils de sales intelligence ne montrent jamais le raisonnement derrière leurs signaux : je voulais voir la source, le contenu et la logique.
Le vrai sujet
La question n’était pas « comment scraper des données ». C’était « comment transformer des données publiques désordonnées en signal exploitable commercialement, sans produire principalement des faux positifs ».
Ce que j’ai construit
Trois sources complémentaires :
- Welcome to the Jungle : les recrutements actifs (volume, contrats, localisations).
- LinkedIn, via Apify pour ne pas maintenir de scraper maison (même logique que dans l’article sur la veille concurrentielle multi-source). C’est la source la plus bruitée : surtout de la marque employeur.
- Pages actualités (
/blog,/actualites,/news) pour les levées, partenariats et lancements, avec la même heuristique que dans le projet Scrapy de détection de technologies. Les blogs sans contenu récent sont ignorés.
flowchart TD
A[(Supabase\nentreprises)] --> B[n8n Scheduler]
B --> C{Loop Over Items}
C --> D[HTTP Algolia\nWTTJ]
C --> E[Apify\nLinkedIn]
C --> F[HTTP\nPages actualités]
D --> G[Code\nParse Jobs]
E --> H[Code\nParse Posts]
F --> I[Code\nParse Contenu]
G --> J[Assemblage\ncontexte]
H --> J
I --> J
J --> K[AI Agent\nOpenRouter]
K --> L[Parsing\nJSON]
L --> M[(Supabase\nsignaux)]
Redis sert de sémaphore en amont de la boucle : chaque itération prend un slot (INCR), attend si la limite est atteinte, le rend après (DECR). Un nœud Redis natif suffit, pas besoin de queue complète.
WTTJ expose son index Algolia
Je partais sur du scraping HTML. En ouvrant l’onglet réseau des DevTools, j’ai vu que WTTJ charge ses offres via Algolia, avec une clé de recherche présente dans le frontend. Ce n’est pas une faille : les Search-Only API Keys d’Algolia sont faites pour être exposées et ne permettent que la lecture. Une requête POST structurée remplace donc tout le parsing HTML :
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 dans les DevTools WTTJ>
Content-Type: application/json
{"query": "", "hitsPerPage": 20, "filters": "organization.slug:doctolib"}
Ce qui aurait pris plusieurs jours en HTML a pris moins d’une heure. Un nœud Code extrait ensuite des hits le titre, le contrat, le télétravail, la ville, la date de publication et l’URL de chaque offre, plus le total (nbHits).
Un Agent IA (openai/gpt-4o-mini via OpenRouter) analyse ensuite le contexte assemblé. Deux choix comptent dans le system prompt : un persona précis (« account manager senior qui cherche des opportunités d’upsell ») plutôt que « analyse ces offres », et une réponse en JSON strict uniquement.
Ce qui a cassé
- Le slug WTTJ manquant.
organization.slugne correspond pas toujours au domaine ou au nom. Je l’ai stocké dans un champwttj_slug_computedde la tablecompanies. Sur les premières exécutions, environ 15 % des entreprises n’en avaient pas : Algolia renvoyait 0 hits, traité comme « aucun poste ouvert ». J’ai ajouté un log pour distinguer les deux cas. - Les annonces republiées. Des entreprises repostent la même offre toutes les quelques semaines :
published_atchange, le total monte, rien ne se passe. Correction : un hash sur(title, contract, location)par entreprise, ignoré s’il existe déjà avec une date récente. Letotal_jobsbrut reste suivi à part. - Les prompts trop ouverts. « Identifie les signaux d’expansion » produisait du texte bien rédigé, impossible à parser. Le JSON strict et une taxonomie explicite des sorties ont tout changé.
Le piège classique
Le modèle ne « comprend » pas l’entreprise. Il complète des patterns. Si le prompt est vague, la sortie sera plausible mais pas fiable. Contraindre le format de sortie et le persona n’est pas un détail : c’est la variable d’ajustement principale.
Limites actuelles : le hash ne couvre pas les reformulations de titre, LinkedIn dépend d’un acteur Apify, le scoring reste empirique et seul l’index wttj_jobs_production_fr est interrogé.
À retenir
Inspecter le réseau avant de scraper : la meilleure solution technique est souvent celle qui n’invente rien. Le scraping était la partie facile ; le difficile, c’est de distinguer un vrai signal d’une annonce qui traîne, sachant qu’un signal seul ne veut presque rien dire (20 offres peuvent être une croissance ou une forte rotation). Prochaine étape : un score de tendance sur l’évolution du total_jobs sur 4 semaines glissantes, puis vérifier si les entreprises détectées « en expansion » convertissent vraiment mieux.