
The shape of the problem
Want to build a recruiting agent? Here is how, and where the memory part gets hard
Think about the best recruiter you ever worked with. The thing that made her great was not her ATS. It was her head. She would call you out of nowhere and say "you mentioned eight months ago you wanted to move closer to your parents in Lyon, a role just opened in Lyon." She remembered a throwaway line from a coffee chat a year earlier, connected it to a job that did not exist back then, and reached out the day it became real.
Most recruiting software cannot do this. It stores a resume and forgets the human. It keeps a status field and loses the reason someone said no, the offhand comment about wanting remote work, the former manager they admired. Your recruiting agent can be the recruiter who remembers, if it has a memory that holds onto the small true things and notices when the world changes.
Korely is that memory.
How you build it
Everyone has the tools to build the agent. Nobody wants to build the memory
- Wire up the agent brain
Drop Claude Code or Cursor on top of your ATS and let it read job reqs, parse resumes, and draft outreach. This part is basically solved today, an afternoon of work.
- Give it a place to write down what it learns
After every interview the agent saves notes as memories, scoped to that candidate: current title, salary expectation, what they are looking for, the offhand comment about wanting remote work, who they interviewed with.
- Let it remember the relationships, not just the rows
When a candidate mentions a former manager, a tech stack, a company they admire, store those as connected facts so the agent can later look up everything stored about Postgres (
get_facts(entity="Postgres")) or search for fintech experience, and combine the two. - Make it answer from what is true now
A candidate said no last spring because the salary was low. Six months later their title changed and they are open again. The agent needs to know the new fact is true and the old one is history, not just average them into mush.
- Re-surface old candidates the day they become placeable
When a fact about a past applicant flips, a title bump, a new availability, a layoff at their company, the agent should treat that database row as a fresh lead instead of a dead one.
- Now the hard part
To do steps 2 through 5 well you would need to build a bi-temporal fact store, an entity graph so candidates link to skills and companies, a way to detect when a new note contradicts an old one, and a retrieval layer fast enough to run on every outreach. That is months of infrastructure work, not a weekend.
- The part you skip
With Korely you skip that entire build. You call
korely.add()when the agent learns something andkorely.search()when it needs to recall. The bi-temporal facts, the contradiction handling, the recency, and the graph are the product, hosted in the EU, the whole thing a bi-temporal graph-RAG memory layer starting at 19 euro a month + VAT.
The shape of the fix
One memory store feeds every write and every read, across time
March, after the interview
A candidate says no
- Sara: "not open until I get a Staff title"
- The agent records a candidate-scoped fact
Korely memory
A bi-temporal fact links into the graph
- Candidate node ties to skills and companies
- Valid-from and recorded-at timestamps
- EU-hosted, no generative model on reads
Months later, a sourcing pass
A fresh lead surfaces
- Sara is now Staff, the old objection is history
- get_context() reads the facts open today
How Korely fits
The agentic memory layer underneath the recruiting agent
What Korely saves you building is the part that is genuinely hard and genuinely boring. A bi-temporal fact store that tracks both when a fact was true in the real world and when you learned it, two-stage contradiction resolution so a new interview note can correctly supersede an old one without losing the audit trail, an entity graph that links candidates to skills, companies, and people, and a retrieval layer over pgvector and the typed facts, Postgres only.
This matters specifically for recruiting because a candidate database is the worst kind of memory problem: it is huge, it changes constantly, most of it is stale, and the value is buried in the few records that just became relevant again. Korely picks the records that match the question, not the most recent ones, and returns them in one block under a token budget you set, 800 tokens by default, however many candidates the database holds.
Reads run no generative model, so the agent can check memory on every single outreach without running up a bill. And the facts are time-aware, so the agent answers from what is true about a candidate now, not from whatever you happened to write down first. The store is hosted in the EU; to extract facts, the text of each write, with up to ten earlier memories of the same candidate (300 characters each) and the earlier facts it is compared with, goes to the model of your project's region, Google's Gemini API by default or Scaleway in Paris if you choose the EU region. Building that yourself is months. Renting it is 19 euro + VAT to start.
Show me the code
A minimal write-then-recall flow
# ── After the interview: the agent records why Sara said no ─────
from korely_memory import Korely
korely = Korely(api_key="kor_live_...")
# One candidate-scoped fact, written the moment it is learned
korely.add(
"Sara Conti: current title Backend Engineer. Not open to a "
"lateral move. Wants a Staff title before switching.",
agent_id="client-acme-backend-search",
user_id="cand-sara-conti",
)
# ── Months later: a routine sourcing pass recalls the fact ─────
context = korely.search(
"candidates who wanted a Staff title before moving",
agent_id="client-acme-backend-search",
)
# context → the closest notes about Sara, best first, each with its score and the first 280 characters of the memory.
# The agent drafts warm outreach grounded in what she told you,
# not a cold note that re-reads a static resume row.
The same memory is reachable from any agent, Claude Code, Cursor, n8n,
LangGraph, or a plain HTTP client. Keep each client in its own
project, so a fintech search never sees a healthcare search, use
agent_id
values to separate roles or recruiters within a client, and store each
candidate under their own
user_id.
When a candidate becomes placeable again
The day the fact flips, the dead row turns into a placement
A candidate, Sara, interviews for a senior backend role in March and turns it down. Her note in the agent memory says current title Backend Engineer, not open to a lateral move, wants a Staff title before switching. Eight months later Sara updates her LinkedIn to Staff Engineer, and your agent sees it during a routine sourcing pass. Because the memory is time-aware, the agent does not just see an old "not interested" flag.
It sees that the exact condition Sara named, getting to Staff, has now become true, and that her earlier objection is history, not the current fact. So the agent surfaces Sara as a warm lead the same day, with the outreach already written: "Last spring you said you wanted a Staff title before moving. Congrats on the promotion, the role we talked about is still open and now it is a real step up for you."
A recency-window memory would have either buried that note under eight months of newer activity or answered with the stale "not interested." The time-aware fact is what turns a dead database row into a placement.
korely.add(
"Sara Conti: Backend Engineer, wants Staff before moving.",
agent_id="client-acme-backend-search",
user_id="cand-sara-conti",
timestamp="2025-03-12",
)
# Eight months later, her title changes
korely.add(
"Sara Conti is now Staff Engineer. Condition met, open again.",
agent_id="client-acme-backend-search",
user_id="cand-sara-conti",
timestamp="2025-11-04",
)
# Facts are extracted a few seconds after add() returns; read once both memories show status "ready".
ctx = korely.get_context("is Sara open?", agent_id="client-acme-backend-search", user_id="cand-sara-conti")
# → a block built from the facts open today (Staff Engineer); a note whose
# facts were all superseded stays, marked "(true when said, changed later)".
# The history of one memory, via the REST API:
# GET /v1/memories/{id}/history -> the memory's events: facts extracted and when a later fact invalidated them
# For a candidate's whole arc: get_facts(user_id="cand-sara-conti", include_invalidated=True) Frequently asked
Recruiting agent memory, common questions
My ATS already stores candidate data. Why do I need Korely?
An ATS stores records. It does not remember the human. It keeps a resume and a status field, but it loses the offhand line from the screening call, the reason someone said no, and the day a past candidate's title changed and made them placeable again. Korely is the memory layer your agent writes those living facts to, scoped per candidate, time-aware, and searchable, so the agent recalls what is true about a person right now instead of re-reading a static row.
How does the agent know a past candidate is open again?
Because the facts are bi-temporal. When a candidate's title, availability, or company changes, the agent writes the new fact and Korely marks the old one as superseded while keeping it in the timeline. On the next sourcing pass korely.get_context() answers from the facts open today, and the facts that stopped being true stay as history, so a row that was a dead "not interested" six months ago surfaces as a warm lead the day the condition they named comes true. Nothing is overwritten, nothing is lost, the agent just answers from what is true now.
Will checking memory on every candidate slow my pipeline down?
No. Reads run no generative model, so the agent can hit memory on every outreach, every sourcing pass, and every interview prep without running up a model bill. The expensive thinking happens in your agent. The recall is a retrieval step, not a model turn.
How is this different from just stuffing my candidate notes into the prompt?
A candidate database is too big and too stale to paste into a context window, and a plain recency window buries the few records that just became relevant under months of noise. Korely picks the facts and memories that match the question, not the most recent ones, and returns them under a token budget you set, 800 tokens by default.
Can I keep separate memory for different roles, teams, or clients?
Yes. Keep each client in its own project (each API key belongs to one project), so a fintech search never sees a healthcare search and facts of one client never change facts of another. Within a client, agent_id can separate roles or recruiters for reads, but each distinct agent_id counts toward the agent limit of your plan across all your projects (Hobby 2, Developer 10, see pricing); for many roles or recruiters, use one agent_id and separate them with metadata or run_id, which search() filters on. The same memory store serves every agent you plug in, whether that is Claude Code, Cursor, an n8n flow, or a plain HTTP call to the REST API.
Where does candidate data live, and can I delete it?
In a managed Postgres store hosted in the EU, with a vector and graph index, nothing for you to run. Candidate data is sensitive, so store each candidate under their own user_id: you can list everything about them page by page, or erase it with one call (DELETE /v1/users/{user_id}/memories) when they ask: the memories and facts are deleted from the live database and leave the encrypted backups within 35 days. You do not build or host any of the memory infrastructure yourself.
Give your recruiting agent a memory that re-surfaces the right candidate
The Python and Node SDKs are live and the Hobby tier is free, so you can sign up, get an API key, and write your first candidate memory in minutes, then move up to 19 euro a month + VAT when your pipeline outgrows it.
Looking for a different shape? See the other nine use cases →