How to Build a Claude-Powered Resume Screener
How to Build a Claude-Powered Resume Screener
A Claude-powered resume screener takes incoming resumes and a job description, extracts the relevant facts (skills, years of experience, education, past titles), and scores or ranks each candidate against the role — automatically, consistently, and in seconds instead of hours. The core idea is simple: resumes are unstructured text, Claude is good at turning unstructured text into structured judgments, and you need structured output to actually sort a pipeline.
This guide walks through the architecture, the prompt and schema design, and the API calls you need to get a working screener running, whether you're building it into an ATS, a recruiting tool, or an internal hiring dashboard.
Why Claude fits this problem
Resume screening isn't keyword matching — it's reasoning about fit. A candidate with "built REST APIs in Flask" should match a job asking for "backend development experience" even without an exact string match. Claude handles this kind of semantic matching well, and more importantly it can return consistent structured output (via tool use / JSON schemas) so you can actually compute rankings instead of parsing free text.
The other practical requirement is throughput: screening 200 resumes for one opening means 200 API calls, ideally in parallel, with predictable cost and no surprise rate-limit failures. That's an infrastructure problem as much as a prompting problem.
Step 1: Extract resume text
Before you call Claude, get clean text out of whatever format resumes arrive in (PDF, DOCX, plain text). Use a PDF/DOCX text extraction library on your backend — this is a solved problem and doesn't need an LLM. Feed the extracted text, not the raw file, into your prompt.
Step 2: Define the screening schema
The single most important design decision is forcing structured output. Instead of asking Claude to "write a summary of fit," define a tool/schema that captures exactly what you need to rank candidates:
{
"name": "screen_resume",
"description": "Extract candidate data and score fit against a job description",
"input_schema": {
"type": "object",
"properties": {
"candidate_name": { "type": "string" },
"years_experience": { "type": "number" },
"matched_skills": { "type": "array", "items": { "type": "string" } },
"missing_requirements": { "type": "array", "items": { "type": "string" } },
"education_match": { "type": "boolean" },
"fit_score": { "type": "number", "description": "0-100 overall fit score" },
"summary": { "type": "string", "description": "2-3 sentence rationale" }
},
"required": ["candidate_name", "fit_score", "summary"]
}
}
This gives you a consistent JSON object per resume that you can store in a database and sort by fit_score.
Step 3: Call the API with the tool defined
If you're already using SubToAPI to turn your Claude access into an HTTPS API, the call looks like this:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4",
"max_tokens": 1024,
"tools": [{
"name": "screen_resume",
"description": "Extract candidate data and score fit against a job description",
"input_schema": { "...": "see schema above" }
}],
"tool_choice": { "type": "tool", "name": "screen_resume" },
"messages": [
{
"role": "user",
"content": "Job description:\n<JOB_DESCRIPTION>\n\nResume:\n<RESUME_TEXT>\n\nScreen this candidate using the screen_resume tool."
}
]
}'
Forcing tool_choice to the specific tool guarantees you get back structured input instead of a conversational response — no regex parsing required. See /docs/tools for the full tool use reference and /docs/messages for the base message format.
Step 4: Batch and rank
In production you're screening many resumes against one job description, so parallelize calls per resume and collect results:
async function screenResume(jobDescription, resumeText) {
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
tools: [screenResumeTool],
tool_choice: { type: "tool", name: "screen_resume" },
messages: [{
role: "user",
content: `Job description:\n${jobDescription}\n\nResume:\n${resumeText}\n\nScreen this candidate.`
}]
})
});
const data = await res.json();
return data.content.find(c => c.type === "tool_use").input;
}
const results = await Promise.all(resumes.map(r => screenResume(jobDescription, r.text)));
results.sort((a, b) => b.fit_score - a.fit_score);
Run this through a queue (e.g., process 10–20 concurrently) rather than firing hundreds of requests at once, both to stay within rate limits and to keep costs predictable.
Step 5: Keep a human in the loop
A screener should shortlist and explain, not auto-reject. Always surface the missing_requirements and summary fields alongside the score so a recruiter can override the model, and keep an audit trail of what the model saw and returned — this matters for fairness and, in some jurisdictions, for compliance with automated-decision disclosure rules. Don't feed protected-class information (age, photos, names that imply ethnicity) into the scoring step if you can avoid it; strip or anonymize fields before the API call where your process allows it.
Handling cost and visibility
Resume screening at volume racks up token usage fast — each call includes the full job description plus a full resume. Two practical levers help: trim resumes to relevant sections before sending them, and track per-call cost so you know what each hire funnel costs. If you're routing calls through SubToAPI, usage metadata is attached to every response, so you can see token counts and cost per screening call without building your own tracking layer, and manage API keys per environment (staging vs. production) from one dashboard. Check /pricing for plan details and /docs/quickstart to get an API key running in minutes.
Streaming for a review UI
If recruiters are reviewing screens live rather than batch-processing overnight, you can stream the summary field so it appears progressively in the UI instead of waiting for the full tool call to resolve. See /docs/streaming for how streaming responses work alongside tool use.
Questions
Can Claude screen resumes against multiple job descriptions at once? Not in a single call — run one API call per (resume, job description) pair so the scoring stays specific to that role's requirements. Batch and parallelize calls instead of combining job descriptions into one prompt.
How accurate is automated resume screening with an LLM? It's good at consistent rule-based matching (skills, years of experience, keywords) but should be treated as a shortlisting aid, not a final decision-maker. Always review the rationale field and keep humans making the actual reject/advance decision.
Do I need to fine-tune Claude for resume screening? No. A well-defined tool schema and a clear job description in the prompt are sufficient for most roles. Fine-tuning adds cost and maintenance without a clear accuracy benefit for this kind of structured extraction task.