← all posts

I Built a Job Pipeline That Rejects Jobs for Me

October 2026

The Setup

I built an AI job pipeline. Six phases, four machines, running daily at 10 AM.

Phase 1 scrapes the job sites for new postings. Phase 2 enriches them — fetches the full description, extracts company and role details, embeds everything into a vector database. Phase 3 ranks each job with an LLM, scoring it against my profile and my stated preferences. Phase 4 writes a daily digest. Phase 5 tailors my CV for the top ten. Phase 6 syncs it all across my home lab.

For weeks it ran beautifully. Clean cycles. Fresh postings. A daily email pointing me at the best matches.

Then I started getting rejected. Not slowly. Flooded.

The Problem Nobody Warns You About

The pipeline was excellent at one thing: finding jobs that looked like a match.

A job titled "CTO" at an AI-forward edtech company? The LLM scored it 92 out of 100. Remote, good salary range, AI in the description, my background on paper. It ticked every box the ranker knew to check.

What it didn't read was the other paragraph. The first one, actually:

"Important note about this position: If your background is primarily in infrastructure, IAM, or cybersecurity, this role is not a fit."

My entire career is infrastructure, IAM, and cybersecurity. The role had been written to exclude exactly me — in its opening paragraph — and my pipeline had scored it 92 and put it at the top of my list. I would have applied, burned a tailored CV and a cover letter, and received an instant auto-rejection. Or more likely, silence.

I got lucky on that one. I read the JD before applying, saw the exclusion, and moved on. But the pattern kept showing up, and it showed up in places that had nothing to do with job descriptions. An HR headhunter found me for a VP of AI Infrastructure role — onsite but local to me, a brand-new Center of Excellence, the exact work I've spent years building. On paper, a home run. The call lasted five minutes. Her tooling had missed the 20+ years of Professional Services on my CV, and the client had explicitly stated: "we do not want anyone with a consulting background." She read me that line from the intake form before we both agreed there was no point continuing.

Two systems, same failure. Both summed the positive signals. Neither checked the negative space.

The Negative-Space Problem

In AI we talk endlessly about positive signals — the keywords that match, the skills that overlap, the title alignment. But every decision document has a negative space: the sentences that define who it is not for. As more people automate matching — and as the systems being screened push back — that negative space is moving out of the expected places. It shows up everywhere, once you know to look:

  • "This is not a cloud architecture role" — at a company whose entire product is cloud infrastructure
  • "Engineering leaders who have not shipped production code in the last 3 years should not apply" — on a "VP of Engineering" posting
  • "Required: 5+ years in K-12 edtech" — in paragraph seven of a job that matched on title, salary, location, and AI
  • "We are not building a Center of Excellence" — posted by a company that badly needs one, and that my CCoE experience matches perfectly
  • An LLM sees "cloud transformation" and my CCoE history and emits a 94. It does not see the sentence saying my specific background is the thing being filtered out. This is a current-state problem with the whole keyword-driven job industry, not just with AI — but AI made it worse for me personally, because it multiplied the volume of near-miss applications I was generating by an order of magnitude.

    The Fix

    In mid-August I changed the workflow. The pipeline still runs — scrape, enrich, rank, digest, tailor — but between "ranked" and "applied" I added a step: read the full JD for exclusion language. Phrases like:

  • "Not a fit if your background is primarily in..."
  • "Important note about this position"
  • "This role will not be considered for..."
  • "X+ years in a domain I don't have"
  • "Engineering leaders only — this is not a transformation role"
  • The results were immediate. Over the weeks that followed, the pipeline screened hundreds of jobs, and roughly 15% of my top-ranked roles turned out to be structurally mismatched. Not marginal differences — roles my own ranker had scored 80+, and that had been written to exclude my exact background. That is 15% of my applications that would have been instant auto-rejections: wasted CVs, wasted cover letters, and a tracker full of false signals about which way the market was moving.

    Then I started automating the review itself, and that's where it got interesting.

    What the Text Can and Can't Tell You

    The first thing the automated screen taught me: there are two failure classes that no amount of keyword matching fixes, because they aren't in the text you're reading.

    The listing is a ghost. One role ranked near the top of my pool — a great fit score, a well-written job description, everything a match should look like. The job ID returned an HTTP 404 on the employer's own careers site. Not a typo. The posting had closed — and LinkedIn was still serving it as open. No model can catch that, because the text is perfect; it's a real job description that is simply no longer accepting applications. The only way to know is to check existence, not meaning. Listing validity is now a first-class check in my workflow, with one strict rule: an unknown result removes nothing. A bot-wall, a timeout, a redirect — those are "unknown", not "gone". When the yield is under 1%, a wrongly dropped role is a role I never apply to.

    The score is a keyword match, not a fit. One role scored the highest fit of any job in its entire pool — 0.87 out of 1.0. "Director – Data Platforms & Architecture" at a cloud consultancy. My CV checked out on Azure, cloud, and leadership. Then I found the employer's own page and read the full description — 15,000 characters of it, three times what LinkedIn had served. LinkedIn had truncated the posting at 5,000 characters, right in the middle of the responsibilities. Past the cut lived the actual requirements: deep expertise in Databricks, Spark, Delta Lake, and Unity Catalog, and 10+ years in data platforms and data engineering. My CV contained zero of those terms. The top score in the pool didn't just overrate me — the input it scored was missing the half of the job description that actually decided the outcome.

    The same shape keeps showing up as domain kills: a high fit label on a discipline I don't practice — knowledge graphs, ontologies, semantic layers — while the model cheerfully sums the one or two overlapping words. A high score is not evidence of fit when the domain is foreign, which is the opposite of what every threshold-tuning instinct assumes.

    The Follow-Through: A Screener That Reads the Negative Space

    Which led to the build. I had already written, in an earlier draft of this post, that the fix should be "an exclusion screen upstream of application." So I built it — and the real build corrected my own proposal.

    The screener asks two calibrated questions of every job — does this text disqualify my background? and does this role fit my profile? — and routes deterministically in code. Exclusion language above a threshold means skip. Fit above a threshold means apply. Everything else gets a human review. No LLM is asked to make a three-way decision; it answers the two questions it can answer, and the routing logic lives where it can be unit-tested. My original idea of "one binary flag" turned out to be wrong: exclusion is a presence question, fit is a global judgment, and conflating them makes the threshold unfixable.

    Two design rules came out of the first week of running it, and I'd defend both to anyone building on top of a model:

  • UNKNOWN must never SKIP. If the tool can't fetch the listing, can't see the full text, or hits a bot-wall, that's a review — not a pass, and certainly not a skip. A system that drops what it can't see is a system that quietly shrinks your options.
  • A fire demotes to REVIEW; it never SKIPs. The role-shape check — deterministic rules that catch "Director" titles demanding hands-on C#/.NET — flags a candidate for human eyes. Only explicit exclusion language ever disqualifies. A false flag costs thirty seconds of reading; a missed role costs an application. You bias for recall and let a person do the precision.
  • And one engineering post-mortem worth telling. The first version of the screener truncated each job description to its first 1,500 characters to keep LLM costs down. That was a real bug. Exclusion language does not live in the first 1,500 characters — it lives in the "important note" paragraph, halfway down, after the responsibilities bullets. A truncated read scored a planted exclusion below its own clean-text floor, and the role routed to APPLY. The fix was to score every overlapping window of the document and take the maximum — if any paragraph excludes you, the job excludes you. Truncation had turned a structural problem into a statistical one, and no amount of threshold tuning on the truncated input could have fixed it. Same lesson as the ghost listing, same lesson as the 15,000-character job description: capture the right input first. The model was never the bottleneck — the data we fed it was.

    The Broader Lesson

    This isn't a job-search problem. It's an AI problem.

    Every agentic system — every pipeline that takes input, scores it, and acts on the result — has this blind spot. The model is trained to find matches. It is not trained to find mismatches. And the mismatches are where the real cost lives.

    A false positive in a job pipeline is a wasted application. A false positive in a medical diagnosis is an unnecessary procedure. A false positive in a security alert is an analyst burning out on ghosts. The negative space is always there. The question is whether your system reads it — and whether you built it to read the whole document, existence included.

    That's the real lesson of agentic AI: the most important action your system takes is the one it doesn't take. Leverage what AI does well — summarize, rank, draft — and design the workflow to catch what it structurally can't: the disqualifier in paragraph seven, the posting that closed, the requirements that got truncated off the page.

    The Numbers

    Since mid-August the pipeline has screened hundreds of jobs. The review step catches roughly 15% of top-ranked roles as structurally mismatched — roles that would have been instant auto-rejections or silent ghosts. The rejection rate hasn't dropped to zero; the market is what it is. But the pipeline no longer spends its GPU budget, and my time, sending me into roles that were written to exclude me — and the tracker now tells a story aligned with reality instead of one aligned with keyword overlap.

    The analytics are only as honest as the inputs. Capture the whole document. Check that the thing exists. Read the negative space. Design for the action your system should not take.

    The pipeline runs daily on four machines across my home lab. The code is private, but the lessons aren't. If you're building agentic systems that make decisions, read the negative space. It's where your model is unintentionally lying to you.

    Built on a home lab, powered by local models, and owned by Andrew Katana.

    Connect on LinkedIn →