Recency-Aware Candidate Sourcing: Stop Surfacing 15-Year-Old Relevant Experience
Recency-weighted candidate search filters on current and last-role experience so 15-year-old relevant work stops surfacing the wrong people. Build it here.
Published
Jun 12, 2026
Written by
Nithish
Reviewed by
Manmohit Grewal
Read time
7
minutes

Recency-Aware Candidate Sourcing: Stop Surfacing 15-Year-Old Relevant Experience
Most sourcing tools score a candidate on everything they have ever touched, so a three-month internship from fifteen years ago counts the same as the role they are in today. A recency-weighted candidate search fixes that by filtering on what someone does now, or did in their last role, instead of the sum of their whole career. A two-person technical search firm we spoke with put the problem in one line. Their tools "don't take into account recency," so a candidate "could, like, maybe 15 years ago build a robotics toy, and it would pull into the search."
This guide is about the retrieval step, the part that decides who even enters your pool. We will walk through why lifetime-experience matching breaks, how current-versus-ever-worked filtering works as a query mechanic, and how to build a recency-aware search with real API calls you can run from your terminal or hand to a Claude Code agent. Scoring the people who survive that filter is a separate job, and we link out to it below. You can try the search side on a free Crustdata account with 100 credits to get started.
Why does lifetime-experience matching surface the wrong people?
The tools the team we spoke with had used all share one habit. They read a candidate's entire history as a flat bag of keywords, then match against it without asking when any of it happened. So a person picks up a skill once, early, and that skill follows them into every relevant search for the rest of their career.
One partner described the pattern exactly. A candidate had "been at AWS for eight years doing nothing with computer vision," yet a computer-vision internship from their master's program "still got them 100% matched." The same firm saw a profile where the only relevant line was "PCB as an internship that they did for three months," while "the bulk of what they've done is robotics." The tool counted the three months and ignored the years that came after.
This is where match percentages lose the recruiter's trust. The team we spoke with told us their top results were the ones they trusted least, because "the years of experience being inflated" by old internships meant a 100% match often was not one. They ended up spending judgment time on the middle of the list and skipping the top, which is the opposite of what a ranked list is supposed to buy you.
The root cause is a missing field: when a tool flattens a career into keywords, recency is information it threw away, so the model never had timing to reason with in the first place. The fix is to keep work history structured and dated, so current role and past role are distinct fields you can filter on, rather than text the matcher blends together.
What is recency-weighted candidate search?
Recency-weighted candidate search treats the timing of experience as a filter input you set deliberately. Instead of asking "has this person ever done X," it asks "is this person doing X now, or did they do it in their most recent role." The same skill, the same title, the same company, all get read against when they happened.
There are two levers, and they map to two distinct questions.
Current-only filtering: restrict a criterion to the candidate's present role. "Currently a robotics engineer" excludes the person who was one a decade ago and has been a manager since.
Recency weighting: keep older experience in the pool but down-weight it, so a recent match outranks an ancient one rather than tying with it. This matters when a slightly out-of-date match is still worth a look, which the team told us was true in the 60-to-80% band where their real judgment lives.
The distinction is practical. A current-only filter is a hard gate you apply during retrieval, before anyone enters the pool. Recency weighting is a soft adjustment you apply after, when you rank the survivors. Most teams want both, and the order matters: gate first so the obviously-wrong people never show up, then weight what is left.
How do you filter on current versus past experience at query time?
The mechanic that makes recency-aware sourcing possible is structured, dated work history with separate fields for current and past roles. Crustdata's Person Search exposes exactly that split. A filter on experience.employment_details.current.title matches only the candidate's present title, while a filter on experience.employment_details.title matches any title they have ever held, current or past. Choosing between those two fields is the whole difference between a recency-aware search and a lifetime-experience one.
The current-versus-ever-worked split
The team we spoke with kept seeing people surface because a tool matched on ever-worked. A computer-vision internship counts under experience.employment_details.title, because that field is the full history. It does not count under experience.employment_details.current.title, because the person's current title is something else entirely. Moving a criterion from the first field to the second is how you stop the internship from pulling someone in.
The same split exists for company. experience.employment_details.in on company name checks whether someone has ever worked somewhere, while experience.employment_details.current.company_name checks where they are today. For a recruiter hiring people actively in robotics, "currently at a robotics company" is a far cleaner signal than "worked at a robotics company at some point."
A current-title search with the REST API
The retrieval step looks like this as a direct call. It finds people whose present title is a robotics or hardware engineer, located near the Bay Area, and it will not match someone who held that title years ago and has since moved on.
curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Robotics Engineer|Hardware Engineer|Embedded Engineer" }, { "field": "professional_network.location.raw", "type": "geo_distance", "value": { "location": "San Francisco", "distance": 50, "unit": "mi" } } ], "limit": 25 }'
curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Robotics Engineer|Hardware Engineer|Embedded Engineer" }, { "field": "professional_network.location.raw", "type": "geo_distance", "value": { "location": "San Francisco", "distance": 50, "unit": "mi" } } ], "limit": 25 }'
curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Robotics Engineer|Hardware Engineer|Embedded Engineer" }, { "field": "professional_network.location.raw", "type": "geo_distance", "value": { "location": "San Francisco", "distance": 50, "unit": "mi" } } ], "limit": 25 }'
The (.) operator is a regex match, so the pipe means "or" across those title variants. Because the field is current.title rather than the history field, a candidate who did robotics in 2011 and has been a platform engineer since simply does not appear. That single field choice removes a large slice of the noise the team described.
How do you exclude an old role without losing a recent generalist?
A hard gate on current title is blunt, and the team we spoke with were clear that blunt tools cost them good people. Their senior partner does one-off searches where "there's probably 50 people in the entire country" who fit, and over-tight filters miss the non-obvious ones. So recency-aware sourcing needs a second move beyond the current-only gate.
Exclude an old title with not_in
Sometimes the cleanest rule is subtractive. If a title in someone's history is the thing dragging them into the wrong searches, exclude profiles where that value appears. The not_in operator removes any profile where one of the listed values shows up in their title history.
curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "experience.employment_details.current.function_category", "type": "(.)", "value": "Engineering" }, { "field": "experience.employment_details.title", "type": "not_in", "value": ["Teaching Assistant", "Research Intern", "Student"] } ], "limit": 25 }'
curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "experience.employment_details.current.function_category", "type": "(.)", "value": "Engineering" }, { "field": "experience.employment_details.title", "type": "not_in", "value": ["Teaching Assistant", "Research Intern", "Student"] } ], "limit": 25 }'
curl --request POST \ --url https://api.crustdata.com/person/search \ --header 'authorization: Bearer YOUR_API_KEY' \ --header 'content-type: application/json' \ --header 'x-api-version: 2025-11-01' \ --data '{ "filters": [ { "field": "experience.employment_details.current.function_category", "type": "(.)", "value": "Engineering" }, { "field": "experience.employment_details.title", "type": "not_in", "value": ["Teaching Assistant", "Research Intern", "Student"] } ], "limit": 25 }'
This keeps anyone currently in an engineering function while dropping profiles whose match was riding on an intern or student line. It is the structured-data version of the call the recruiter makes by hand when they spot an internship doing the heavy lifting on a "100% match."
Use tenure to rank older matches instead of dropping them
When you want to keep an older match but rank it lower, reach for the dated fields rather than a hard exclude. experience.employment_details.current.years_at_company_raw tells you how long someone has been in their present role, and experience.employment_details.current.start_date gives the date that role began. Pull both, and you can sort a list so the person who has been doing the relevant work for three years outranks the person who started last month, or the reverse if you are after a recent switcher. The point is that the timing is now a number you can sort on, instead of a thing you discover by clicking into every profile.
Where does the recency signal come from, and where does scoring take over?
Recency-aware sourcing leans on one thing being true: the work history has to be structured and dated, so current and past are real fields rather than free text a matcher has to guess at. The partner we spoke with summed up what actually decides a fit when he said the main thing is "the person's education and employment history and what the company did, where the person has actually worked at." All three of those are structured fields in Person Search, which is what lets you filter on the present rather than the lifetime.
Retrieval is only the first half, though. Filtering decides who enters the pool. It does not decide how good each survivor is once they are in it. The team we spoke with still wanted to spend their judgment on the 60-to-80% band, which is a ranking problem that comes after retrieval. Once you have a recency-clean pool, you weight the people in it against a rubric, and that is a different build with its own variables and disqualifiers. We cover that end to end in our guide to building a candidate scoring model with custom variables, including how to make recency one weighted variable among several rather than a hard gate. The clean handoff is to gate on recency during search, then score what is left.
For teams comparing where this structured, dated history comes from in the first place, our roundup of the best candidate enrichment APIs for recruiting platform builders walks through which providers return dated employment fields and which flatten them.
Build it in Claude Code, or call the API directly
There are two ways to wire recency-aware retrieval into a real workflow, and they hit the same data.
The MCP path, for low-code teams: a Claude Code agent with Crustdata's MCP server configured can run these searches from plain English. You tell the agent "find people currently working as robotics engineers near the Bay Area, and drop anyone whose only robotics line is an internship," and it composes the current.title filter and the not_in exclusion for you. It is the fastest way to test whether a recency gate cleans up a specific search before you write any orchestration code. The team we spoke with already ran their sourcing through Claude, so this path fit how they worked.
The direct-API path, for teams who want full control: you call POST https://api.crustdata.com/person/search yourself and own the filter logic, the pagination, and the ranking step. The Python version of the current-title search looks like this.
import requests resp = requests.post( "https://api.crustdata.com/person/search", headers={ "authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, json={ "filters": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Robotics Engineer|Hardware Engineer|Embedded Engineer", }, { "field": "experience.employment_details.current.company_industries", "type": "(.)", "value": "Robotics|Automotive|Aerospace", }, ], "limit": 25, }, ) profiles = resp.json().get("profiles", []) for p in profiles: current = p["experience"]["employment_details"]["current"] # current.title and current.years_at_company_raw are now yours to rank on print(current.get("title"), current.get("years_at_company_raw")) # See docs.crustdata.com for the full field and operator list
import requests resp = requests.post( "https://api.crustdata.com/person/search", headers={ "authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, json={ "filters": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Robotics Engineer|Hardware Engineer|Embedded Engineer", }, { "field": "experience.employment_details.current.company_industries", "type": "(.)", "value": "Robotics|Automotive|Aerospace", }, ], "limit": 25, }, ) profiles = resp.json().get("profiles", []) for p in profiles: current = p["experience"]["employment_details"]["current"] # current.title and current.years_at_company_raw are now yours to rank on print(current.get("title"), current.get("years_at_company_raw")) # See docs.crustdata.com for the full field and operator list
import requests resp = requests.post( "https://api.crustdata.com/person/search", headers={ "authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, json={ "filters": [ { "field": "experience.employment_details.current.title", "type": "(.)", "value": "Robotics Engineer|Hardware Engineer|Embedded Engineer", }, { "field": "experience.employment_details.current.company_industries", "type": "(.)", "value": "Robotics|Automotive|Aerospace", }, ], "limit": 25, }, ) profiles = resp.json().get("profiles", []) for p in profiles: current = p["experience"]["employment_details"]["current"] # current.title and current.years_at_company_raw are now yours to rank on print(current.get("title"), current.get("years_at_company_raw")) # See docs.crustdata.com for the full field and operator list
Both paths return the same dated, structured profiles. The agent path gets you to a working recency filter in an afternoon. The direct path gives you a pipeline you can run on a schedule across many open roles, which is what the team we spoke with wanted once they trusted the output, since roughly 80% of their week goes to sourcing and any search that runs without them is time back.
What recency-aware sourcing does not solve
A recency filter cleans up the obvious case where old experience pulls in the wrong person. It does not read judgment for you. The senior searches the team described, where the right candidate sits at an adjacent company under a title that does not match the role, still need a human deciding what counts as adjacent. A current-title gate could even hurt those searches by hiding a recent switcher whose new title has not caught up to their real work, which is why recency weighting and a human review step both matter.
The other honest limit is that structured history is only as good as the underlying data refresh. If a profile says someone is "currently" at a company they left two months ago, a current-title filter inherits that lag. The team we spoke with hit this with another tool that had stopped refreshing often, so a "great" profile turned out to have started a new role months earlier. Recency-aware search assumes the present role is actually present, which means the data layer has to keep employment up to date.
Crustdata is the data layer for recruiting teams building their own sourcing logic. It returns structured, dated work history with current and past roles as separate fields, so recency stops being something you reconstruct by clicking into profiles and becomes a filter you express in the query. Come and see what a recency-clean pool looks like across one of your own searches. Start free with 100 credits at crustdata.com, or book a demo to walk through the architecture.
Frequently asked questions
What is recency-weighted candidate search?
It is a search that reads experience against when it happened, so a recent match outranks an old one and current roles can be filtered separately from past ones. Instead of matching on everything a candidate has ever done, it asks what they do now or did in their last role, which keeps decade-old or internship experience from inflating a match.
How do you filter on current experience instead of lifetime experience?
Use a field that is scoped to the present role. In Crustdata's Person Search, experience.employment_details.current.title matches only the candidate's current title, while experience.employment_details.title matches any title they have ever held. Putting your criterion on the current field gates out people whose only match is an old role.
Why do sourcing tools surface candidates with irrelevant old experience?
Most tools flatten a career into a bag of keywords and match without timing, so a skill picked up once early follows the person into every related search. A computer-vision internship from years ago reads the same as eight years of current computer-vision work, which is how a poor fit lands as a top match.
Should you exclude old experience entirely, or just weight it down?
Both have a place. A hard current-only gate is right when the present role is the real requirement. Weighting is safer for senior or non-obvious searches, where a recent switcher or an adjacent-company candidate is worth keeping in the pool at a lower rank rather than dropping outright.
Is recency the same as a candidate scoring model?
No. Recency is a retrieval and ranking input that decides who enters the pool and roughly in what order. A scoring model rates the people already in the pool against a full rubric of weighted variables. Recency can be one variable inside that model, but the search-time gate runs first.
Products
Popular Use Cases
Competitor Comparisons
Use Cases
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2026 Crustdata Inc.
Products
Popular Use Cases
Competitor Comparisons
Use Cases
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2025 CrustData Inc.
Products
Popular Use Cases
Competitor Comparisons
Use Cases
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2026 Crustdata Inc.


