Six years in, you know two things about your next job that your résumé cannot say: why you left the last one, and which parts of it you would not do again. Every tool asks for the document anyway, hands it back reformatted, and shows you roles that share its keywords.
A job description is not a direction. Drizzle reads a person once — résumé, a voice call, the paths they try on — and matches on trajectory: role family, domain, altitude. Understand once, exploit many. Everything downstream runs off that one read.
What it does
- 01
Drop a résumé
One dropzone. No account, no form.
Parsed to text, then stored verbatim as an immutable source before anything touches it — so every fact downstream can be traced back and re-derived later.
- 02
Extract into a relational shape
A clean, editable résumé, in seconds.
The extractor is held to a schema, so it returns typed rows instead of prose I have to parse back out. Editing a field writes a new source — your correction is evidence too.

- 03
Talk to it
A call, not a chat box.
The prompt is assembled once from your map, your live roles and the people it could introduce you to. Mid-conversation the agent calls back into the app, and roles appear on screen while you're still talking.

- 04
Let it change its mind
Nothing visible. That's the point.
After the call a second agent reads the transcript and writes insights, each with a confidence. Nothing is deleted — a changed view supersedes the old one and keeps the chain.
- 05
Read the market, separately
Nothing you see. It runs offline, on a schedule.
Postings are pulled from company boards continuously, which costs almost nothing. Turning each one into facts and scores is the expensive pass, so it runs once per role well before anyone asks for a match — never inside a request.
- 06
Rank against where you're going
Roles with a fit number, and the reason for it.
Three things decide it: skills you can evidence, how the work suits you, and how close the role sits to your direction. The percentage opens into the sentence behind it — including what the role wants that your résumé doesn't show.

How it works
Each call, and what it cost.
Separate the immutable from the derivable
What is kept, and what is recomputed
Sources are append-only and never edited; every derived row carries its source id and a confidence. That costs a join on nearly every read, and it means improving a prompt re-derives history instead of orphaning it.
Supersede, never delete
How the picture of a person changes
A revised conclusion supersedes its predecessor and keeps the chain intact. Contradiction stays queryable, so the system can say that March disagrees with today. Overwriting would have let March quietly become today.
Four layers, one direction of flow
The shape of the data
Sources hold raw evidence, facts the parsed résumé, insights what the calls concluded, variants what gets rendered. Flow is one-way — nothing downstream writes back — so any layer can be dropped and rebuilt from the one above it.
Keep the expensive read off the request path
Ingestion
Collecting postings is cheap and runs on a schedule; interpreting them is not, so it happens once per role offline — roughly ten seconds — and never while someone waits. Each row stores the prompt version that produced it, so a prompt change forces a re-read instead of mixing vintages.
Place each model where its latency budget is
Model placement
Postings are batch work with nobody waiting, so they run on a local 4090; 2,000 roles through a hosted API is cost without benefit. Résumé parsing sits on the request path, so it runs hosted. Both sides of retrieval share one embedding model, or the distances aren't comparable.
Make the model commit
Posting → twelve scored axes
Each posting yields hard facts plus twelve scored axes: what the role demands, what it offers. My first model came back at roughly 0.5 on everything, which is a model declining to discriminate. It took six prompt revisions to fix, and the variance check that caught it now gates any replacement.
The profile is the query
What retrieval means here
Nobody types anything. Hard filters cut the pool first, and what survives goes to the scorer. Applications and dismissals feed back as a weight adjustment afterwards, so ranking personalises without training a second model.
Requirements are two-sided
Hard gates
Credentials and years are binary, not soft signals — no score should recover a role that needs a licence someone lacks. I only checked the lower bound at first, so an eight-year analyst got shown internships. Being over the bar disqualifies too.
Calibrate the similarity, don't use it raw
Cosine → a score
Cosine isn't a score. I measured the pool's distribution and mapped it linearly: the median, 0.41, becomes the floor; 0.68, the genuine best-match ceiling, becomes 1.0. Anchoring the top at the p99 instead compresses every good match into a tie.
Three signals, renormalised when one is absent
The blend
Evidence, fit and direction at 0.35 / 0.35 / 0.30. Fit was 0.45 until I demoted it — it's the softest of the three, estimated by a model on both sides. A missing component drops out and the rest renormalise, so absent data never scores as good or bad.
Rank beside the data, not at the edge
Where the arithmetic runs
One call gathers the candidate pool with each role's distance to the direction already computed, and scoring runs in a function next to the database. The edge Worker only fetches the result. I tried doing the arithmetic there first and blew the CPU budget.
Freeze the fixtures, then let them fail
Regression testing a ranker
Twenty-five assertions over fixed profiles pin orderings rather than scores — scores drift, orderings shouldn't. Eight archetypes then run against the live pool with an expected top-K and a must-not-appear list. The must-not half is where an internship ranking for a senior shows up.
Ask for what a résumé structurally cannot hold
Why there is a call at all
A résumé records what happened. It cannot say why you left in 2019, which of those jobs you would refuse to repeat, or where you are trying to get to — and trajectory needs all three. Spoken answers are hedged in ways a date range is not, so every insight the call produces carries a confidence and can be superseded.
Trade control of the turn loop for latency
Who owns the conversation
A spoken turn has a few hundred milliseconds, so the live path does the minimum and everything expensive moves after the call. My first design owned every turn and could re-steer mid-sentence; the vendor's agent owns it now — real control traded for a native speech path.
What I tried
Including the ones that lost.
Run the whole voice loop locally, then measure it against hosted
hosted won the live path · local kept for everything off itWhisper, a local model and Kokoro on 24GB, to see whether hosted inference was needed at all. Steady-state latency was competitive; cold start was not. The first turn spent about ten seconds loading three models, which no conversation survives.
Cost a user before scaling anything
hosted spend stayed inside the noise floorA first run is two model calls — parse, then score — at fractions of a cent. Per-user cost was never the exposure; interpreting the whole market was, and that sits on hardware already bought. A per-account ceiling caps the blast radius of a runaway loop.
Measure whether a model discriminates, not whether it reads well
model rejected · the variance check became the acceptance barA faster model produced plausible output, so I measured score variance instead of reading samples. Five of twelve axes were effectively constant — 0.60 for an intern, 0.60 for a CTO. Everything downstream ranks on those numbers, so the throughput bought nothing.
A silent regression after swapping the embedding model
recalibrated against the measured distributionNew model, old calibration. Its similarity distribution sits lower and tighter, so every trajectory score collapsed to 0.5 — no exception, no alert, the feature simply inert in production. A frozen fixture set caught it: two matches tied at 1.0 that could not both be perfect.
Verify two providers share one vector space
identical — the split is safeThe pool is embedded locally, live queries in the cloud. Same model name, different vendors — comparable only if the spaces are identical, which a shared name does not guarantee. Measured: cosine 1.0000. Otherwise ranking silently compares two coordinate systems.
Give the conversation a way to write to the screen
the thing worth keepingLetting the agent call back into the app changed what a call is worth. Mid-sentence it surfaces real roles, opens one, names a person to talk to. A conversation that leaves state behind beats one the user has to remember.
Where it is
- The spine — four layers, sources as evidence
- Résumé ingest and an editable view
- Voice mentor with live tool calls
- Insight extraction from transcripts
- pgvector trajectory matching over the spine
- Ranking in an edge function, next to the data
- Learned drift from what you apply to and dismiss
- Retire filled and expired roles from the poolnext
- A distilled extractor — smaller, quantized, cheapernext