Candidates
Candidate
Job title
Stage
Score
Actions
Loading pipeline…
POST /api/scout/score so every browser gets the same number.
| Bucket | Weight | Counts when | Fill (0–1) |
|---|---|---|---|
| Required Skills | 35 | Always (a JD with no extractable required skills returns a flat 50 / Fair Fit) | Prominence-weighted share of required skills matched — a skill repeated often in the JD counts more than one mentioned once |
| Preferred Skills | 15 | JD lists nice-to-have skills | Plain share of preferred skills matched |
| Clearance | 20 | JD states a required clearance level | 1.0 meets or exceeds (or holds an unspecified clearance) · 0.5 holds a lower level · 0 none |
| Education | 15 | JD states a degree requirement | 1.0 meets or exceeds · 0.5 lower degree · 0 none |
| Location / Commute | 15 | JD is remote, or both the JD state and the candidate state are known | 1.0 remote or same state · 0 different state. Unknown on either side and not remote → bucket drops out entirely |
k8s → kubernetes,
nodejs → node.js, postgres → postgresql, …).react ↔ react native developer).all-MiniLM-L6-v2) cosine similarity
≥ 0.58 (0.55 threshold + a 0.03 margin). Borderline pairs below that
margin fall back to the deterministic rules above, so scores stay stable.agents/scout/scoring.py) against the JD — not by Vapi's built-in analysis —
and stored as fit_score / fit_reason on the call.
| Factor | Effect on the score |
|---|---|
| Hard eligibility gates work authorization · clearance · required years · must-have skills · location / onsite |
Weighted heavily. Failing any gate the JD states outright forces a low score even when the rest of the screen is strong. |
| Evidence from the call only | Anything the screen didn't cover is never assumed in the candidate's favour — it is called out in the reason instead. |
| Determinism | Run at temperature 0.2 with a strict JSON response, so repeat
scores on the same transcript barely move. |
SCOUT_AUTOADVANCE_THRESHOLD env var). At or above it, Scout moves the
applicant to Present screen to Hiring Manager in JazzHR and emails the
candidate a booking invite; below it the call is left for a recruiter to review.
public/scout/talent-bank.html).
A recruiter types a phrase or pastes a whole job description; Scout reads the
intent, ranks the résumé corpus, and shows one calibrated match % per candidate.
Served by POST /api/scout/talent/match in
backend/talent_endpoint.py.
gpt-4o in JSON mode (understand_query(), temperature 0) and
parsed into an editable intent:
required_skills[], preferred_skills[],
location, years_min, title,
domain, plus a clean search_text. If the LLM is
unavailable it falls back to the deterministic keyword extractor
(find_keywords / canonical_skill).filters, which skips the LLM re-parse so the recruiter's
refinement is honoured verbatim.search_text is embedded with
text-embedding-3-small and used to over-fetch the nearest ~30–45
résumés by pgvector cosine distance
(ORDER BY embedding <=> query_vector) from the
resume_index table. Recall is deliberately not hard-filtered, so
strong near-misses still surface (with their gaps shown) instead of being dropped.compute_score) —
Required 35 / Preferred 15 / Clearance 20 / Education 15 / Location 15, renormalized
to whichever buckets the query actually states, clamped 5–99. The final ordering is
this calibrated score, with cosine as the tie-breaker — not raw similarity.matched / missing detail as
✓ met and
⚠ gap pills — e.g. “✓ React”, “⚠ needs AWS”,
“⚠ 3 yrs (<5 needed)”. This is why a match % is defensible: you can see exactly
what earned or cost each point.