← Blog

How to Build a Resume Screening Tool With Claude

2026-10-28 · 5 min read · SubToAPI Team

Building a resume screening tool with Claude means feeding resume text (and a job description) into the model with a structured prompt, getting back consistent, scored output in JSON, and wiring that into a pipeline that can process hundreds of applications without manual review of each one. The core challenge isn't getting Claude to "read" a resume — it's getting consistent, structured, comparable output across hundreds of different resume formats so you can rank candidates reliably.

This guide walks through the actual architecture: extracting text, prompting for structured scoring, handling batches, and avoiding the two things that break most screening tools — inconsistent output formats and bias creeping in from poorly designed prompts.

Why Claude works well for resume screening

Resumes are messy, unstructured documents. People format dates differently, list skills in prose or bullet points, and use wildly inconsistent section headers. Traditional keyword-matching ATS systems handle this badly — they miss qualified candidates whose resumes don't contain the exact phrase from the job posting.

Claude reads resumes the way a human reviewer would: it understands that "led a team of 5 engineers" implies management experience even without the word "manager," and it can reason about whether 3 years of Django experience is a reasonable substitute for a Flask requirement. That reasoning is what you want to capture in a structured, repeatable way.

Step 1: Extract resume text

Most resumes arrive as PDF or DOCX. Extract plain text first — Claude handles text much more reliably than raw file uploads for this use case, and it keeps your pipeline simple and fast.

import pdf from "pdf-parse";
import fs from "fs";

async function extractResumeText(filePath) {
  const buffer = fs.readFileSync(filePath);
  const data = await pdf(buffer);
  return data.text;
}

For DOCX files, use a library like mammoth to pull raw text. Strip excessive whitespace but keep line breaks — they help Claude distinguish sections.

Step 2: Design a structured scoring prompt

The key to a usable screening tool is forcing Claude to return the same JSON shape every time, regardless of resume format. Give it the job description, the resume text, and an explicit schema.

const systemPrompt = `You are a resume screening assistant. Given a job description and a resume, evaluate the candidate's fit and respond ONLY with valid JSON matching this schema:

{
  "overall_score": number (0-100),
  "years_relevant_experience": number,
  "matched_skills": string[],
  "missing_skills": string[],
  "education_match": "yes" | "partial" | "no",
  "summary": string (2-3 sentences),
  "recommendation": "advance" | "maybe" | "reject"
}

Do not include any text outside the JSON object. Base your evaluation only on evidence in the resume text — do not infer protected characteristics like age, gender, ethnicity, or nationality, and do not factor them into scoring.`;

That last instruction matters. Explicitly telling the model to ignore protected characteristics and stick to job-relevant evidence reduces the risk of it picking up on signals like university names, graduation years, or names that correlate with demographic groups.

Step 3: Call the model with job + resume pairs

async function screenResume(jobDescription, resumeText) {
  const response = 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-5",
      max_tokens: 500,
      system: systemPrompt,
      messages: [
        {
          role: "user",
          content: `JOB DESCRIPTION:\n${jobDescription}\n\nRESUME:\n${resumeText}`
        }
      ]
    })
  });

  const data = await response.json();
  return JSON.parse(data.content[0].text);
}

If you're routing Claude calls through SubToAPI, this is a standard call to the Messages endpoint — see the quickstart and messages docs for the full request/response shape. Running it through an application API key (sub_live_...) rather than individual developer credentials means you get usage metadata per team and can swap models without touching application code.

Step 4: Batch process a candidate pool

For a real applicant pool, you'll be running this against dozens or hundreds of resumes. Process them with controlled concurrency so you don't hit rate limits, and always store the raw JSON response alongside the resume for audit purposes — hiring decisions often need a paper trail.

async function screenBatch(jobDescription, resumes) {
  const results = [];
  const concurrency = 5;

  for (let i = 0; i < resumes.length; i += concurrency) {
    const batch = resumes.slice(i, i + concurrency);
    const scored = await Promise.all(
      batch.map(r => screenResume(jobDescription, r.text))
    );
    results.push(...scored.map((s, idx) => ({ ...s, candidateId: batch[idx].id })));
  }

  return results.sort((a, b) => b.overall_score - a.overall_score);
}

If you need to stream progress updates to a reviewer dashboard while a large batch is processing, SubToAPI supports streaming responses too — see /docs/streaming if you want per-candidate feedback to appear in real time rather than waiting for the full batch.

Step 5: Add human review, not full automation

Don't ship a tool that auto-rejects candidates without a human in the loop. Use Claude's output to triage and rank, not to make final decisions. A practical setup:

This keeps the tool legally safer (many jurisdictions require human review in automated hiring decisions) and catches cases where Claude's scoring missed context a recruiter would catch — like a career gap explained in a cover letter.

Step 6: Handle edge cases

questions

Does Claude replace an ATS? No — Claude handles the judgment layer (reading and scoring resumes against a job description). You still need an ATS or database to store candidates, track pipeline stages, and manage recruiter workflows.

How do I keep scoring consistent across different resume formats? Use a strict JSON schema in your system prompt, set max_tokens generously enough to avoid truncation, and validate every response against the schema before storing it — reject and retry malformed responses.

Is it safe from a hiring-bias perspective? Explicitly instruct the model to ignore protected characteristics, keep a human reviewer in the loop for all final decisions, and log every score with its reasoning so you can audit decisions if challenged.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →