Job-Search Pipeline
The funnel, end to end:
sources → normalize + dedup → knockout gate → weighted score
→ human curation → master tracker → inbox feedback sync → daily digest
The mechanical steps run as tested code; the judgment steps (reading full job
descriptions, deciding true fit, writing tracker rows) stay with the agent in
an interactive session. Reference implementation:
auto-job-seek.
1. Sources — pluggable, never scraped in-house
Define one JobSource interface and add implementations behind it; extend by
adding a source, not by editing callers.
- Managed scraper (recommended for LinkedIn): an Apify actor. The input
contract is a JSON file —
keyword[], locations[], publishedAt
(r86400 = last 24h), maxItems — and the output is a dataset of raw
postings. Cost scales with maxItems (cents per run). Do not scrape
LinkedIn directly (ToS).
- Free official APIs: many government job boards expose public JSON
search endpoints with no key (e.g. Singapore's MyCareersFuture:
POST api/v2/search). Zero cost, high signal, small daily volume — a
supplement, not a pillar.
- JobSpy sidecar for Indeed: run
python-jobspy under a pinned
interpreter via uv run --python 3.12 --with python-jobspy when the main
environment's Python is too new for its dependency pins; write CSV, ingest
through the normal pipeline. Never force-install it into the main env.
- Persist every raw scrape (
output/raw.json) and every gate survivor
with its full description (candidates_full.json). Curation needs full JD
text, and raw files make any run re-screenable without re-paying.
Keyword hygiene (hard-won)
maxItems is usually a global cap: one broad keyword can eat the whole
budget and starve the rest to zero results. Audit the per-keyword result
distribution after config changes; prune noise magnets (e.g. a term that
returns mostly engineering titles), keep the cap generous.
- Never search for and penalize the same track. If a keyword is worth
scraping, the scorer must not down-rank its results — resolve the
contradiction in one direction.
- Prefer ranking boosts over tighter hard gates when steering the mix
(e.g. boost explicit graduate/trainee titles rather than lowering the
max-experience cutoff and losing good borderline roles).
2. Knockout gate — hard disqualifiers first
Separate non-negotiables (instant disqualify) from soft signals (rank). The
gate checks, in text of the posting:
- Work authorization — citizens-only / no-sponsorship / clearance
phrases, when the candidate needs sponsorship.
- Experience floor — the JD's minimum required years exceeds the
candidate's ceiling ("minimum 3 years" means ≥3; a new grad fails it).
- Degree — PhD/postdoc required and the candidate is below.
Everything else is a score, not a gate: skill overlap, title relevance,
domain fit, seniority alignment, location, plus bounded focus adjustments
(±N points for priority/deprioritized title keywords, clamped, with a
human-readable reason attached to every adjustment).
3. Deduplication — two keys plus a backstop
- Cross-run dedup by job id AND normalized company+title: boards repost
the same role under a new id, so id-only "exclude seen" leaks reposts.
- Backstop: before inserting a curated match into the tracker, query the
tracker for the company/title. The strongest roles are reposted most —
without this check you will re-surface roles the candidate already applied
to.
4. The curation bar — score is a first pass, not a verdict
The agent reads every gate survivor's full description and applies
judgment the gate cannot:
- KEEP: roles squarely in the candidate's target tracks; genuine
entry-level roles in adjacent tracks with transferable evidence.
- CUT: roles that keyword-match but mismatch in kind — e.g. trading-desk
roles surfacing for "quant", pure software/data-engineering roles for
"data", ops/middle-office dressed as "analyst", commission sales dressed
as "investment", citizens-only defense/government labs, wrong locations,
far-future start dates.
- Quality over quantity. A short honest list beats a padded one; if asked
for a fixed count and true fits fall short, say so rather than pad.
- Assign an honest fit tier (Strong / Good / Moderate) and put every caveat
in the notes column — the human decides with eyes open.
5. One master tracker
A single append-only table (Notion or equivalent) for matching AND tracking —
never a new table per day. Columns: title, company, location, fit tier,
notes (why + caveats), seniority, status, apply URL, date. Status lifecycle:
To Apply → Applied → Assessment → Interview → Offer, with Rejected and
Dropped as exits. Prefer a reversible Dropped status over deleting
rows — strategy changes; deletes don't undo.
6. Inbox feedback sync
The other half of the loop: application status flows back from email.
- Search the inbox (
newer_than:2d) for confirmations, rejections,
assessment invites, interview requests; reconcile each hit against the
tracker; update statuses or add untracked rows.
- Idempotent by design — re-running the sync on the same mail must
produce zero writes; verify this on the first scheduled runs.
- Consolidate inboxes first: auto-forward secondary addresses into one
primary inbox so a single connector sees everything. Verify forwarding
actually works (it is not retroactive), and backfill history once.
7. Daily rhythm and digest
One scheduled run per day: scrape → gate → curate → tracker → feedback sync →
a dated report file → a digest email to the human (subject: N new matches,
M status changes, action items). If programmatic send is not configured,
leave a ready-to-send draft rather than failing silently. Log every run in
the workspace ledger with counts (fetched → deduped → passed gate → curated).
Operating gotchas
- Verify a top pick's minimum-years and sponsorship language in the actual JD
before recommending it — gate regexes are not a substitute for reading.
- Watch scheduled runs for keyword drift (a new term flooding the digest with
off-target roles) and prune within a day or two.
- MCP-connector steps (tracker, inbox) may require an interactive session;
design every automated half to degrade gracefully into "report and stop".
1---2name: job-search-pipeline3description: Methodology for an agent-operated daily job-search pipeline: pluggable posting sources (a managed LinkedIn scraper via an Apify actor, free official job-board APIs, a JobSpy sidecar for Indeed), an ATS-style knockout gate for hard disqualifiers, transparent weighted scoring, a human curation bar, a single master application tracker, and an inbox-driven status feedback loop. Use when building or operating a recurring job-matching cycle, screening postings against a candidate profile, or reconciling application statuses from email.4---56# Job-Search Pipeline78The funnel, end to end:910```11sources → normalize + dedup → knockout gate → weighted score12 → human curation → master tracker → inbox feedback sync → daily digest13```1415The mechanical steps run as tested code; the judgment steps (reading full job16descriptions, deciding true fit, writing tracker rows) stay with the agent in17an interactive session. Reference implementation:18[auto-job-seek](https://github.com/chenxi-bot21/ai-job-search-pipeline).1920## 1. Sources — pluggable, never scraped in-house2122Define one `JobSource` interface and add implementations behind it; extend by23adding a source, not by editing callers.2425- **Managed scraper (recommended for LinkedIn):** an Apify actor. The input26 contract is a JSON file — `keyword[]`, `locations[]`, `publishedAt`27 (`r86400` = last 24h), `maxItems` — and the output is a dataset of raw28 postings. Cost scales with `maxItems` (cents per run). Do not scrape29 LinkedIn directly (ToS).30- **Free official APIs:** many government job boards expose public JSON31 search endpoints with no key (e.g. Singapore's MyCareersFuture:32 `POST api/v2/search`). Zero cost, high signal, small daily volume — a33 supplement, not a pillar.34- **JobSpy sidecar for Indeed:** run `python-jobspy` under a pinned35 interpreter via `uv run --python 3.12 --with python-jobspy` when the main36 environment's Python is too new for its dependency pins; write CSV, ingest37 through the normal pipeline. Never force-install it into the main env.38- **Persist every raw scrape** (`output/raw.json`) and every gate survivor39 with its full description (`candidates_full.json`). Curation needs full JD40 text, and raw files make any run re-screenable without re-paying.4142### Keyword hygiene (hard-won)43- `maxItems` is usually a **global** cap: one broad keyword can eat the whole44 budget and starve the rest to zero results. Audit the per-keyword result45 distribution after config changes; prune noise magnets (e.g. a term that46 returns mostly engineering titles), keep the cap generous.47- **Never search for and penalize the same track.** If a keyword is worth48 scraping, the scorer must not down-rank its results — resolve the49 contradiction in one direction.50- Prefer **ranking boosts over tighter hard gates** when steering the mix51 (e.g. boost explicit graduate/trainee titles rather than lowering the52 max-experience cutoff and losing good borderline roles).5354## 2. Knockout gate — hard disqualifiers first5556Separate non-negotiables (instant disqualify) from soft signals (rank). The57gate checks, in text of the posting:58- **Work authorization** — citizens-only / no-sponsorship / clearance59 phrases, when the candidate needs sponsorship.60- **Experience floor** — the JD's *minimum* required years exceeds the61 candidate's ceiling ("minimum 3 years" means ≥3; a new grad fails it).62- **Degree** — PhD/postdoc required and the candidate is below.6364Everything else is a score, not a gate: skill overlap, title relevance,65domain fit, seniority alignment, location, plus bounded focus adjustments66(±N points for priority/deprioritized title keywords, clamped, with a67human-readable reason attached to every adjustment).6869## 3. Deduplication — two keys plus a backstop7071- Cross-run dedup by **job id AND normalized company+title**: boards repost72 the same role under a new id, so id-only "exclude seen" leaks reposts.73- **Backstop:** before inserting a curated match into the tracker, query the74 tracker for the company/title. The strongest roles are reposted most —75 without this check you will re-surface roles the candidate already applied76 to.7778## 4. The curation bar — score is a first pass, not a verdict7980The agent reads every gate survivor's **full description** and applies81judgment the gate cannot:82- **KEEP:** roles squarely in the candidate's target tracks; genuine83 entry-level roles in adjacent tracks with transferable evidence.84- **CUT:** roles that keyword-match but mismatch in kind — e.g. trading-desk85 roles surfacing for "quant", pure software/data-engineering roles for86 "data", ops/middle-office dressed as "analyst", commission sales dressed87 as "investment", citizens-only defense/government labs, wrong locations,88 far-future start dates.89- **Quality over quantity.** A short honest list beats a padded one; if asked90 for a fixed count and true fits fall short, say so rather than pad.91- Assign an honest fit tier (Strong / Good / Moderate) and put every caveat92 in the notes column — the human decides with eyes open.9394## 5. One master tracker9596A single append-only table (Notion or equivalent) for matching AND tracking —97never a new table per day. Columns: title, company, location, fit tier,98notes (why + caveats), seniority, status, apply URL, date. Status lifecycle:99`To Apply → Applied → Assessment → Interview → Offer`, with `Rejected` and100`Dropped` as exits. **Prefer a reversible `Dropped` status over deleting101rows** — strategy changes; deletes don't undo.102103## 6. Inbox feedback sync104105The other half of the loop: application status flows back from email.106- Search the inbox (`newer_than:2d`) for confirmations, rejections,107 assessment invites, interview requests; reconcile each hit against the108 tracker; update statuses or add untracked rows.109- **Idempotent by design** — re-running the sync on the same mail must110 produce zero writes; verify this on the first scheduled runs.111- **Consolidate inboxes first:** auto-forward secondary addresses into one112 primary inbox so a single connector sees everything. Verify forwarding113 actually works (it is not retroactive), and backfill history once.114115## 7. Daily rhythm and digest116117One scheduled run per day: scrape → gate → curate → tracker → feedback sync →118a dated report file → a digest email to the human (subject: N new matches,119M status changes, action items). If programmatic send is not configured,120leave a ready-to-send draft rather than failing silently. Log every run in121the workspace ledger with counts (fetched → deduped → passed gate → curated).122123## Operating gotchas124125- Verify a top pick's minimum-years and sponsorship language in the actual JD126 before recommending it — gate regexes are not a substitute for reading.127- Watch scheduled runs for keyword drift (a new term flooding the digest with128 off-target roles) and prune within a day or two.129- MCP-connector steps (tracker, inbox) may require an interactive session;130 design every automated half to degrade gracefully into "report and stop".