appscript.dev
Automation Advanced Drive Sheets

Build an AI resume screener

Score and summarise Northwind applicants against a role spec, into a hiring sheet.

Published Aug 15, 2025

A Northwind job opening pulls in fifty CVs, and the first pass through them is the same tedious job every time: open each PDF, hold the role spec in your head, decide whether the candidate is worth a closer look. By CV thirty the standard has drifted, and good applicants get missed because attention ran out.

This script does the first pass consistently. It reads every CV in a Drive folder, scores each one against the role spec with Claude, and writes the score plus a one-line summary into a hiring sheet. You still make every real decision — but you make it from a ranked shortlist instead of a folder of unopened PDFs.

What you’ll need

  • A Drive folder containing the applicant CVs as PDF files.
  • A Google Sheet to collect the results — you’ll put its ID in the config.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.
  • A short role spec — a paragraph of must-haves and nice-to-haves — that you pass to the function.

The script

// The spreadsheet that collects screening results.
const APPLICANTS_SHEET_ID = '1abcApplicantsId';

// How much of each CV to send. Enough for a one-page resume; keeps
// the prompt within a sensible token budget.
const MAX_RESUME_CHARS = 4000;

/**
 * Screens every PDF CV in a Drive folder against a role spec and
 * writes a score and summary for each into the applicants sheet.
 *
 * @param {string} folderId - The Drive folder holding the CVs.
 * @param {string} roleSpec - A short description of the role's requirements.
 */
function screenResumes(folderId, roleSpec) {
  // 1. Get an iterator over the PDF files in the folder.
  const files = DriveApp.getFolderById(folderId)
    .getFilesByType('application/pdf');

  if (!files.hasNext()) {
    Logger.log('No PDF CVs in that folder — nothing to screen.');
    return;
  }

  const sheet = SpreadsheetApp.openById(APPLICANTS_SHEET_ID).getSheets()[0];

  // 2. Add a header row if the sheet is empty.
  if (sheet.getLastRow() === 0) {
    sheet.appendRow(['CV', 'Score', 'Summary', 'Link']);
  }

  let screened = 0;
  while (files.hasNext()) {
    const f = files.next();

    // 3. Read the CV text and trim it to a sensible length.
    const text = f.getBlob().getDataAsString().slice(0, MAX_RESUME_CHARS);

    // 4. Ask Claude to score the candidate, pinned to strict JSON.
    const prompt =
      'Score this candidate from 0-100 against this role:\n' + roleSpec +
      '\n\nResume:\n' + text +
      '\n\nReturn ONLY a JSON object — no prose, no markdown — in this shape: ' +
      '{"score": number, "summary": "one sentence"}.';
    const out = JSON.parse(stripFences(callClaude(prompt)));

    // 5. Append one row per candidate to the hiring sheet.
    sheet.appendRow([f.getName(), out.score, out.summary, f.getUrl()]);
    screened++;
  }

  Logger.log('Screened ' + screened + ' CV(s).');
}

/**
 * Claude occasionally wraps JSON in a ```json code fence. Strip it so
 * JSON.parse never chokes on the markdown.
 */
function stripFences(text) {
  return text.replace(/```(?:json)?/g, '').trim();
}

/**
 * Minimal Anthropic API call. The key lives in Script Properties — it
 * is never pasted into the code.
 */
function callClaude(prompt, model = 'claude-sonnet-4-6', maxTokens = 300) {
  const key = PropertiesService.getScriptProperties()
    .getProperty('ANTHROPIC_API_KEY');
  const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: prompt }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. screenResumes opens the Drive folder and gets an iterator over its PDF files. If there are none, it logs a message and stops.
  2. It opens the applicants sheet and adds a header row if the sheet is still empty, so the first run produces a labelled table.
  3. For each CV it reads the file’s text and trims it to MAX_RESUME_CHARS — enough for a one-page resume without overspending tokens.
  4. It builds a prompt that includes the role spec and the CV text, pinned to a strict JSON object with a numeric score and a one-sentence summary.
  5. It calls Claude Sonnet — judging a candidate against a spec needs reasoning, so the stronger model earns its cost here. stripFences removes any code fence, then JSON.parse turns the reply into a real object.
  6. It appends a row per candidate with the file name, score, summary, and a link straight back to the CV in Drive.

Example run

Call it from the editor with a folder ID and a role spec:

screenResumes('1AbcFolderId',
  'Customer support lead. Must-have: 3+ years in B2B SaaS support, ' +
  'team management. Nice-to-have: knowledge-base writing, multilingual.');

After a run over four CVs, the applicants sheet holds a ranked-ready table:

CVScoreSummaryLink
jordan-blake.pdf88Six years B2B SaaS support, led a team of eight — strong fit.open
priya-nair.pdf74Solid support background but limited management experience.open
sam-okafor.pdf61Strong writer, mostly consumer support, no team lead history.open
alex-kim.pdf38Career in field sales — little overlap with the support role.open

Sort the sheet by score and the shortlist is the top of the table.

Run it

This is an on-demand job — you run it when applications close, not on a schedule:

  1. In the Apps Script editor, write a small wrapper that calls screenResumes with your folder ID and role spec, then select it and click Run.
  2. Approve the authorisation prompt the first time.
  3. Open the applicants sheet, sort by the Score column, and start reading from the top.

Watch out for

  • getDataAsString reads a PDF as raw bytes. For text-based PDFs this usually yields readable text, but a scanned or image-only CV will come through as garbage — those candidates need OCR or a manual read.
  • Scores are a guide, not a verdict. Always read the CVs near your cut-off line yourself; a strong candidate with an unusual background can score low.
  • Be deliberate about fairness. An AI screener can carry bias from a vague role spec — keep the spec concrete and skills-focused, and never let the score be the only thing a candidate is judged on.
  • appendRow runs once per CV. A folder of fifty PDFs means fifty API calls plus fifty writes, which can approach the six-minute limit — split a large batch across folders, or collect rows and write them with one setValues.
  • The script re-screens every PDF in the folder on each run. Move screened CVs to a “done” subfolder, or check the sheet for the file name first, to avoid duplicate rows.

Related