Un flow n8n qui surveille Greenhouse pour détecter les reqs nouvellement ouvertes, retrouve les candidats passés qui avaient atteint un stade d’entretien tardif sur une req apparentée et avaient été rejetés pour une raison non disqualifiante — les « silver medalists » —, re-note chacun d’eux par rapport à la grille de la nouvelle req avec Claude, et publie une shortlist classée dans un seul canal Slack. Il ne contacte jamais personne, n’ajoute jamais un candidat à un pipeline, et ne déplace jamais un candidat dans l’ATS. Le recruteur décide de chaque prise de contact. Il transforme « on a embauché quelqu’un d’autre au printemps dernier, qui était de nouveau le finaliste malheureux ? » d’une fouille archéologique de 40 minutes en un message Slack qui arrive dans l’heure où la req s’ouvre.
Quand l’utiliser
Vous fonctionnez sur Greenhouse (ou un autre ATS doté d’une API en lecture — les nœuds d’ingestion se remplacent), et vous ouvrez assez de reqs dans des familles de postes récurrentes pour que les finalistes de l’an dernier soient la shortlist de cette année.
Vous rejetez réellement les finalistes avec des raisons de rejet structurées. Tout le modèle de sécurité du flow repose sur la capacité à distinguer « on a embauché quelqu’un d’autre » de « a échoué au contrôle des antécédents ». Si votre équipe rejette tout le monde avec une seule raison générique, corrigez cela d’abord ; le flow n’a rien sur quoi se baser pour filtrer.
Vous avez des reqs sources à désigner. Le flow ne devine pas quelles reqs passées sont « apparentées » — vous listez les job IDs Greenhouse passés par famille de postes dans un fichier de configuration. Cela rend la correspondance auditable plutôt qu’une boîte noire de similarité.
Un recruteur parcourt le digest et décide de la prise de contact. Le flow fait remonter et classe ; un humain re-screene et contacte.
Quand NE PAS l’utiliser
Prise de contact automatique dans la boucle. Le flow classe et publie sur Slack ; il n’envoie jamais d’email, n’ajoute jamais à une séquence, ne déplace jamais de stade. Raccorder un envoi de prise de contact au digest transforme une suggestion de re-contact en traitement automatisé de données candidates — et re-contacter un candidat au-delà de la période de rétention que vous lui avez communiquée est une violation du GDPR, pas un growth hack. La ligne Confirm first: par candidat dans le digest existe précisément pour qu’un recruteur vérifie le consentement et la fraîcheur avant tout message.
Aucune fenêtre de récence. Le GDPR exige que vous ne conserviez ni ne re-traitiez les données candidates au-delà de la période de rétention que vous avez communiquée au candidat — couramment 12 à 24 mois pour les candidats non retenus. Le filtre recency_months du flow écarte quiconque dépasse la fenêtre. Le régler plus long que votre période de rétention déclarée pour élargir le pool est la seule modification qui transforme ce flow en risque.
Des raisons de rejet auxquelles vous ne pouvez pas vous fier. Si « Position filled » est utilisé en douce pour « on avait des réserves », la deny-list ne peut pas vous protéger. Le flow n’est aussi sûr que la discipline des raisons de rejet qui le sous-tend.
Recrutement de petite taille ou ponctuel. Une équipe ouvrant trois reqs sans rapport par an va plus vite en consultant sa propre mémoire qu’en rédigeant une grille et une liste de reqs sources. La mise en place se rentabilise lorsqu’une famille de postes se répète.
Recherches confidentielles ou de cadres dirigeants. Posture de consentement différente, chaîne d’audit différente. Cela n’a pas sa place dans un canal Slack partagé.
Mise en place
Importez le flow. Déposez apps/web/public/artifacts/candidate-rediscovery-n8n/candidate-rediscovery-n8n.json dans votre instance n8n. Chaque nœud porte notesInFlow: true, de sorte que les notes sur le canevas expliquent chaque choix.
Raccordez les credentials. Trois : PLACEHOLDER_GREENHOUSE_CRED_ID (clé API Harvest, scope lecture uniquement — Jobs, Applications, Scorecards), PLACEHOLDER_ANTHROPIC_CRED_ID (clé API Claude), PLACEHOLDER_SLACK_CRED_ID (token bot Slack avec chat:write pour #talent-rediscovery). Le _README.md du bundle indique où se trouve chaque valeur.
Rédigez un fichier de configuration par famille de postes dans ${CONFIG_DIR}/<family>.json. Il contient les match_job_ids (les reqs sources), min_stage_reached (le filtre de stade tardif), les allow-lists et deny-lists de raisons de rejet, recency_months, fit_threshold, top_n, et la grille. Le format complet est dans _README.md. Aucune config pour une famille → le flow s’arrête avec missing_config plutôt que de noter par rapport à des valeurs par défaut.
Réglez la fenêtre de rétrospective.POLL_LOOKBACK_HOURS doit être ≥ à l’intervalle de planification (6 h par défaut), sinon une req ouverte entre deux relevés passe au travers. Les deux se règlent ensemble.
Faites un dry-run sur une famille pour laquelle vous venez d’embaucher. Les finalistes malheureux dont vous vous souvenez devraient arriver en haut du digest. Réglez min_stage_reached et les ancres de la grille par rapport à votre mémoire avant de lui faire confiance sur une nouvelle famille.
Activez le déclencheur. Passez active: true uniquement après un digest sur lequel vous agiriez réellement.
Ce que fait le flow
Douze nœuds, dans l’ordre. Les filtres déterministes de consentement et d’équité s’exécutent avant l’appel au modèle, car lâcher un LLM sur l’intégralité de l’archive des rejets, c’est la façon de re-contacter quelqu’un qui vous a demandé de ne jamais le faire.
Every 6 Hours — déclencheur planifié. Greenhouse n’a pas de webhook fiable de création de poste, donc le flow effectue des relevés.
Fetch New Open Reqs — GET /v1/jobs?status=open&created_after=… sur Greenhouse Harvest. Le tableau JSON se scinde en un item par nouvelle req.
Load Match Config — résout la famille de postes de la req, charge sa config, la hashe pour le journal d’audit. S’arrête sur missing_config.
Config Loaded? — filtre IF ; les reqs sans config s’arrêtent ici.
Fetch Rejected Pool — GET /v1/applications?status=rejected&last_activity_after=…, paginé. Un item par candidature rejetée.
Eligibility Filter — le socle à cinq filtres : correspondance de req source, stade tardif atteint, allow/deny de raison de rejet (deny l’emporte), fenêtre de récence, suppression « ne pas contacter ». Tout le reste est écarté avant qu’un modèle ne voie quoi que ce soit.
Fetch Scorecards — récupère les scorecards d’entretien antérieurs du candidat, le texte d’ancrage pour la re-correspondance.
Claude Re-Match — note le candidat passé par rapport à la grille de la nouvelle req sur Sonnet 4.6, avec consigne explicite de ne pas hériter de l’ancienne décision de rejet et de ne pas noter sur des proxys de classe protégée. Preuve requise : aucune citation textuelle de scorecard → fit 1.
Parse + Keep — applique la règle de preuve, marque « keep » quand le fit ≥ au seuil de la config.
Audit Append — une ligne JSONL pseudonyme par candidat noté (ID candidat + lien, pas de nom, pas de texte de scorecard).
Build Digest — regroupe par req, dédoublonne un candidat ayant matché via deux reqs sources (le fit le plus élevé l’emporte), classe, tronque à top_n.
Slack Digest — publie une shortlist classée par req dans #talent-rediscovery, chaque candidat accompagné d’une raison en une ligne de le faire ressortir et d’une note Confirm first:.
La réalité des coûts
Tokens API Anthropic — chaque candidat envoie le texte de la scorecard + la grille (~4-5k tokens en entrée) et renvoie ~300 tokens en sortie. Au tarif catalogue de Sonnet 4.6, cela tombe autour de $0.015-0.03 par candidat noté, donc une famille remontant 200 silver medalists éligibles coûte environ $3-6 par req ouverte (calculé à partir des comptes de tokens, non mesuré sur vos données).
Appels Greenhouse Harvest — en lecture seule : un relevé de jobs, un tirage paginé des applications, un fetch de scorecards par candidat éligible. Cela reste dans la limite de débit par clé documentée de Harvest pour toute taille de famille réaliste.
Coût n8n — l’auto-hébergement est gratuit en conteneur. Le plan Starter de n8n Cloud couvre le volume de relevés ; seul un débit de reqs très élevé nécessite Pro.
Temps recruteur — le gain. Reconstituer à la main une liste de silver medalists à travers les reqs passées prend la majeure partie d’une heure par req ; le digest arrive classé, avec les drapeaux de consentement et les prompts de re-screen pré-positionnés, dans les minutes qui suivent l’ouverture de la req.
L’économie derrière le gain. Les benchmarks publiés en recrutement situent le coût par embauche au-dessus de $4,500 et les économies d’une embauche redécouverte à environ $2,000-3,000, avec un time-to-fill sur les embauches par redécouverte qui baisse de 20 à 30 jours. Les équipes démarrent généralement à un taux de redécouverte de 5 à 15 % et visent 35 à 50 % en un an ; le benchmark du taux d’embauche de silver medalists se situe autour de 8 à 15 %. Le flow existe pour faire de l’atteinte de ces chiffres une option par défaut, pas un projet trimestriel.
Métrique de succès
Suivez trois chiffres par famille de postes et par trimestre :
Taux shortlist-vers-screen — part des candidats du digest qu’un recruteur emmène vers un re-screen. En dessous de ~20 %, c’est que la grille ou min_stage_reached est trop lâche ; resserrez les ancres avant d’élargir le pool.
Taux d’embauche par redécouverte — part des embauches de la famille issues du digest. Le benchmark de 8 à 15 % est l’objectif ; en dessous de 5 % après deux trimestres, c’est que la liste de reqs sources ou la fenêtre de récence est trop étroite.
Délai entre ouverture de la req et première slate qualifiée — la métrique côté expérience candidat et hiring manager. Le digest devrait faire passer cela de plusieurs jours au jour même.
vs alternatives
vs la redécouverte de Gem ou hireEZ — ce sont des produits de CRM de talents managés avec leurs propres campagnes de réengagement et un graphe de candidats ; choisissez-les si vous voulez la plateforme et que le budget le permet. Choisissez le flow si vous voulez les règles de correspondance, la deny-list et le journal d’audit versionnés dans votre propre repo, cadrés sur des reqs sources que vous choisissez, avec le digest qui arrive dans votre stack.
vs la recherche « prospect pool » native de Greenhouse — la recherche native trouve les candidats par mot-clé et par stade mais ne les re-note pas par rapport à la grille d’une nouvelle req avec preuve citée, et le classement par pertinence est une boîte noire. Choisissez le flow lorsque les lignes reason_to_resurface et Confirm first: par candidat sont ce qui fait agir le recruteur.
vs un recruteur qui mine manuellement l’ATS — même qualité un bon jour, mais le recruteur oublie la fenêtre de récence, saute la deny-list sous la pression des délais, et ne le fait que pour les reqs dont il se souvient. Le flow le fait pour chaque req récurrente, à chaque fois, avec les filtres de consentement non optionnels.
Points de vigilance
Re-contact au-delà de la rétention.Garde-fou : le filtre recency_months écarte quiconque dépasse la fenêtre de rétention communiquée avant la notation, et le journal d’audit enregistre la fenêtre utilisée. Réglez-le sur votre période de rétention déclarée ou plus court — jamais plus long pour agrandir le pool.
Candidats disqualifiés qui ressurgissent.Garde-fou : la deny-list de raisons de rejet s’exécute avant le modèle et deny l’emporte sur allow. Les contrôles d’antécédents/de références échoués, les réserves de conduite, l’absence d’autorisation de travail et les raisons explicites « ne pas contacter » ne peuvent jamais atteindre le digest. La discipline dépend de raisons de rejet honnêtes en amont.
Report de biais d’anciennes décisions.Garde-fou : le modèle a pour consigne de ne pas hériter du verdict de rejet antérieur — un candidat écarté parce que quelqu’un d’autre a été choisi peut être un 5 pour une nouvelle req — et de ne pas noter sur le nom, l’école comme signal isolé, l’âge, le genre ou les périodes d’inactivité professionnelle. Le config_sha dans le journal d’audit rend reproductibles les règles de correspondance utilisées à une date donnée lors d’une revue de biais-de-screening-IA.
État du candidat obsolète.Garde-fou : la ligne Confirm first: par candidat dans le digest force le recruteur à vérifier que la personne est toujours dans la région, toujours intéressée et toujours en adéquation avant la prise de contact ; le flow affirme une correspondance, pas un fait courant. Les candidats actifs ailleurs relèvent de la vérification du recruteur dans Greenhouse, signalée dans les limites connues du bundle.
Scorecards maigres notées bas.Garde-fou : le texte de la scorecard est le seul ancrage, donc un candidat rejeté avant des entretiens substantiels est noté bas par conception. Relevez min_stage_reached plutôt que d’alimenter le modèle avec des CV qu’il ne peut pas voir.
Stack
Le bundle d’artefact se trouve dans apps/web/public/artifacts/candidate-rediscovery-n8n/ et contient :
# Candidate rediscovery (silver medalists) — n8n flow
This flow polls Greenhouse for newly-opened reqs, finds past candidates who reached a late stage on a related req and were rejected for a non-disqualifying reason ("silver medalists"), re-scores each against the new req's rubric with Claude (Sonnet 4.6 by default), and posts a ranked shortlist to Slack. It never contacts a candidate, never adds anyone to a pipeline, and never moves a candidate in Greenhouse. The recruiter decides every outreach.
This README covers import, credentials, the per-job-family config format, the consent and fairness gates, and the dry-run procedure.
## Import
1. Open n8n → Workflows → Import from file → pick `candidate-rediscovery-n8n.json`.
2. Set the workflow timezone (top of the canvas) to your team's working timezone for sane audit-log timestamps. The default is UTC.
3. Do not enable the workflow yet. Configure credentials and at least one job-family config, complete the dry-run, then flip to enabled.
## Credentials (three required)
### `PLACEHOLDER_GREENHOUSE_CRED_ID` — Greenhouse Harvest API key
- Greenhouse admin → Configure → Dev Center → API Credential Management → Create New API Key → type "Harvest". Grant only the read permissions the flow uses: `GET` on Jobs, Applications, and Scorecards. The flow never writes to Greenhouse.
- In n8n, create an HTTP Basic Auth credential. Username = the API token. Password = empty. (Harvest authenticates as base64 of `token:` with a trailing colon — n8n's Basic Auth credential does this for you.)
- Bind the credential to the three Greenhouse nodes: `Fetch New Open Reqs`, `Fetch Rejected Pool`, `Fetch Scorecards`.
### `PLACEHOLDER_ANTHROPIC_CRED_ID` — Anthropic API key
- console.anthropic.com → API Keys → Create Key. Restrict by IP if your n8n is behind a fixed egress.
- In n8n, create a credential of type "Anthropic API". Paste the key.
- Bind to the `Claude Re-Match` node. The model is set to `claude-sonnet-4-6` in the request body — change it there if you want to test other models.
### `PLACEHOLDER_SLACK_CRED_ID` — Slack bot token
- Create (or reuse) a Slack app with the `chat:write` scope. Install to the workspace. Invite the bot into `#talent-rediscovery`.
- In n8n, create a Slack credential with the bot token (`xoxb-…`).
- Bind to the `Slack Digest` node.
### Environment variables
- `CONFIG_DIR` — directory holding the per-job-family config files. Default `/data/rediscovery`.
- `AUDIT_DIR` — directory for the JSONL audit log. Default `/data/audit`.
- `POLL_LOOKBACK_HOURS` — how far back `Fetch New Open Reqs` looks for newly-opened reqs. Must be **≥** the schedule interval (default 6) or a req opened between polls will be missed. Default 6.
## Config file format (one per job family)
The flow expects one config file per job family at `${CONFIG_DIR}/<family>.json`. The family is resolved from the new req's `job_family` custom field, or — if that is absent — the slugified name of the req's first department. Missing config → the flow halts for that req with `missing_config` and leaves the req for manual sourcing.
The config is the only place the matching rules live. Copy this, replace every value, and save as `<family>.json`:
```json
{
"job_family": "backend-engineer",
"version": "2026-06-15",
"match_job_ids": [4012, 3987, 3654],
"recency_months": 18,
"min_stage_reached": ["Onsite", "Final Interview", "Reference Check", "Offer"],
"rejection_reasons_allow": [
"Position filled — strong candidate",
"Hired another candidate",
"Kept warm for future role",
"Timing — not ready to move"
],
"rejection_reasons_deny": [
"Failed background check",
"Not legally authorized to work",
"Conduct / values concern",
"Failed reference check",
"Withdrew — compensation mismatch",
"Do not contact"
],
"do_not_contact_tags": ["do-not-contact", "gdpr-erased", "opted-out"],
"fit_threshold": 4,
"top_n": 10,
"rubric": {
"role": "Senior Backend Engineer (Distributed Systems)",
"dimensions": {
"fit": {
"must_have": [
"Production Go or Rust (3y+)",
"Owned a distributed-system migration"
],
"anchors": {
"5": "Late-stage scorecards show owned, measurable distributed-system outcomes that map to this req's must-haves",
"4": "Strong scorecards on the core skill; one must-have unconfirmed",
"3": "Adjacent skills; would need a fresh screen on the core must-have",
"2": "Partial overlap; likely a stretch for this req",
"1": "No scorecard evidence the candidate matches this req"
}
}
}
}
}
```
- `match_job_ids` are the **feeder reqs** — the past Greenhouse job IDs whose late-stage rejects count as silver medalists for this family. Find them in the URL of each job in Greenhouse. This is what scopes "related req"; the flow does not guess relatedness.
- `min_stage_reached` is the late-stage gate. A candidate rejected at "Application Review" or "Phone Screen" is not a silver medalist — they never got a real read. Use your own stage names exactly as they appear in Greenhouse.
- `rejection_reasons_deny` is the safety floor and **deny wins over allow**. Any disqualifying reason — failed background/reference check, conduct, no work authorization, an explicit do-not-contact — must be listed here so the candidate is never re-surfaced.
- The config is hashed (SHA-256, first 16 hex chars) per run and the hash is written to the audit log and the Slack footer, so the exact rules used on a given date are reproducible.
## Consent and fairness gates (do not weaken to widen the pool)
Two layers protect the candidate, both **before** the LLM call:
1. **`Eligibility Filter`** drops any application that is not a feeder-req match, did not reach a late stage, carries a disqualifying or non-allow-listed rejection reason, falls outside the recency window, or whose candidate carries a do-not-contact / erasure / opt-out tag.
2. **`Claude Re-Match`** is instructed not to inherit the prior reject decision and not to score on protected-class proxies (name, school as a standalone signal, age, gender, employment gaps), and to cite verbatim scorecard evidence — no citation forces fit to 1.
The recency window exists because GDPR requires you not to hold or re-process candidate data beyond the retention period you told the candidate about — commonly 12–24 months for unsuccessful applicants. Set `recency_months` to your stated retention period or shorter; never longer. Candidates past the window are dropped, not re-contacted.
If you find yourself wanting to delete a deny-list reason or stretch the recency window to grow the shortlist, that is exactly the decision a recruiter — not the flow — should make case by case, in Greenhouse, with the candidate's consent status in view.
## Dry-run procedure
1. Author one config file for a family where you recently filled a role and remember the runner-up candidates.
2. Temporarily point `match_job_ids` at the feeder reqs and set the new-req trigger to fire manually: in n8n, click "Execute workflow" with `Fetch New Open Reqs` returning the already-open target req (or pin a sample job item).
3. Read the Slack digest. The runner-ups you remember should appear near the top. If a known strong silver medalist is missing, check, in order: were they within the recency window, did they reach a `min_stage_reached` stage, was their rejection reason allow-listed, do they carry a suppression tag.
4. If obvious mis-fits rank high, the rubric anchors are too loose or the scorecards are thin — look at the `evidence` line in the digest. Empty or paraphrased evidence means the model had little to work with (the candidate was rejected before substantive interviews); raise `min_stage_reached`.
5. Only switch the workflow `active: true` after a digest you would actually act on.
## First-run sanity check
After enabling, watch the first real digest:
1. Confirm the `Confirm first:` line on each candidate is specific (e.g. "still in-region; was a 2024 reject so re-screen on the new framework"). Generic lines mean the model is guessing — check it is on Sonnet 4.6.
2. Confirm the `config <sha>` in the Slack footer matches the file you authored. A mismatch means the wrong family file loaded.
3. Confirm `${AUDIT_DIR}/rediscovery-<YYYY-MM>.jsonl` exists and has one line per scored candidate. No file means you are operating without the audit trail that a GDPR / EEOC inquiry about automated re-contact would require.
## Known limits
- **Active-elsewhere check is the recruiter's, not the flow's.** The pool query returns rejected applications only, so it cannot tell whether a candidate is currently active on another open req. The recruiter sees that in Greenhouse before reaching out; the flow does not auto-suppress active candidates.
- **A candidate who matched via two feeder reqs is scored twice**, then de-duplicated in `Build Digest` (the higher fit wins). The duplicate scoring is a small, bounded token cost, not a correctness problem.
- **Scorecard text is the only grounding.** Greenhouse does not return parsed resume text via Harvest, so a candidate rejected before any substantive interview has thin scorecards and will score low even if their resume is a strong match. That is intended: re-surface people you actually evaluated, not your whole archive.
- **No dedupe table across runs.** If the same req stays open across two polls it will not re-fire (the `created_after` filter only catches newly-opened reqs), but re-opening a req would re-digest it. The audit log makes repeats visible; add a seen-reqs check in front of `Load Match Config` if your audit posture needs hard idempotency.