Guide

Jev for recruiters: how to clean a sourced candidate list for cents

Jev answers typed yes/no and multiple-choice questions for about four cents per million tokens. Here is how to use it to clean a sourced candidate list before outreach, with code.

You ran a search for senior backend engineers and exported 1,200 profiles. Before a single email goes out, somebody has to read every row, because the export always carries passengers: technical recruiters whose profiles mention Python, bootcamp graduates with "Software Engineer" in the headline, a company page that slipped through, the same person twice under two spellings. Reading 1,200 rows takes a day. Skipping the read burns your sender reputation and your credibility with the people who did fit.

Large language models can do that read, but running ChatGPT or Claude over every row of a four-figure list is slow, and the answers come back as prose you then have to parse. In September 2026 a model built for exactly this shape of work arrived. It is called Jev, it comes from a San Francisco lab called TypeSafe AI, and it does one thing: it answers typed questions about text, with a probability attached, in a fraction of a second.

This guide shows how to use Jev to check a sourced list before outreach. You get the eight checks worth running, a Google Sheets script you can paste in, a Python version for bigger lists, a way to set thresholds on your own data, what it costs, and the places where it gets recruiting data wrong.

What Jev is, in recruiter terms

TypeSafe came out of stealth on September 15, 2026 with $40 million in seed funding led by DCVC (Firecrawl). Jev is its first public model, and the lab calls it a "System One" model, after the fast, automatic kind of thinking in Daniel Kahneman's Thinking, Fast and Slow. The current version on OpenRouter is Jev 1.13, listed there as released on September 18.

The easiest way to picture it is a smart if statement. You send it a piece of text (the "state") and a set of questions, and it sends back answers in a fixed format. It never writes a sentence. It can't draft outreach, summarize a profile or explain its reasoning, and it isn't meant to.

Every question is one of three types:

Question type What comes back Recruiting example
Noul (yes/no) A probability from 0 to 1 that the answer is yes "This person works in recruiting, talent acquisition, staffing or HR."
Choice The chosen option, a probability for every option, and a confidence value "Which kind of work does this person do in their current role?" with options such as backend, frontend, data, not engineering, unclear
Score A position on a scale of 2 to 10 levels you define Keep it for things like how urgent a client email is. This guide doesn't use it on people.

All questions in one request are answered in parallel. TypeSafe's models page prices Jev at $0.042 per million input tokens, and output tokens are free. The same page lists limits of 250,000 tokens per second and 1,200 requests per minute, with a note that limits are adjusting as demand settles. TypeSafe quotes 70 to 500 milliseconds per call, most around 100 (Flavio Copes), and OpenRouter shows about 0.3 seconds.

Access is the one friction point this month. TypeSafe opened direct signups on September 20 and paused them on September 22 because of demand (Firecrawl). OpenRouter carries the model without a waiting list, so every example in this guide goes through an OpenRouter key.

Start with the list, before anyone applies

TypeSafe's own use-case map has a Recruiting entry that says Jev can "evaluate resumes, applications, and interview feedback against explicit, job-related criteria." That is technically true and it is the part of the funnel we would leave alone.

The reason is where the legal obligations sit. New York City's Local Law 144, the one US rule that requires a bias audit for automated hiring tools, covers tools whose output substantially assists decisions about candidates. The city's Department of Consumer and Worker Protection says in its FAQ that running a tool over a resume bank or using one for outreach to potential candidates falls outside the law, because a person becomes a candidate when they apply. We call that boundary the applicant line, and we cover it in detail in our AI bias audit guide. Colorado is moving in the same direction from the other side: SB 26-189, signed on May 14, 2026 and effective January 1, 2027, requires notice, post-decision disclosure and a path to human review when automated technology influences employment decisions about applicants (Seyfarth Shaw).

A sourced list sits before that line. Nobody on it has applied for anything. The questions you ask are about the record in front of you: is this a real person's profile, does the current role match what the search was for, is this a recruiter who got caught in a keyword net. That is data hygiene on your own prospecting list. The judgment about any individual stays with you, when you read the rows Jev keeps and decide who to contact.

Two caveats. The applicant line comes from the regulator's FAQ, not the statute, and the agency could revise it. And federal anti-discrimination law does not care where the line falls, so a check that quietly filters out one group is a problem wherever it runs. The section on what to keep out of Jev deals with that. None of this is legal advice.

There is a practical argument too. Resume evaluation leans on the things Jev is worst at, which TypeSafe documents itself: reading dates as ordered quantities, doing arithmetic, following multi-step reasoning. List checks are short, literal questions about a few lines of text, which is the shape it was built for.

Eight checks that clean a sourced list

We call this the list audit: eight questions, each narrow enough that one line of text settles it, run on every row before outreach. Six of them go to Jev. Two are code, because code does them better.

# Check Who answers What it catches
1 Role family of the current job Jev, Choice Frontend engineers in a backend search, product managers, students, anyone outside the role
2 Works in recruiting or staffing Jev, Noul Technical recruiters and sourcers whose profiles repeat the stack they hire for
3 Hands-on work with the must-have skill Jev, Noul (one per skill) Skills listed in a skills section but never mentioned in a job
4 Level stated in the current title Jev, Choice Titles that say "Intern" or "Head of" in a list meant for individual contributors
5 Type of current employer Jev, Choice Staffing-firm contractors when the client asked for product-company backgrounds, or the reverse
6 Too little information to tell Jev, Noul Empty profiles, company pages, placeholder records
7 Same person twice Code finds pairs, Jev confirms Duplicates that exact matching misses: "Jon" and "Jonathan", old and new employer
8 Tenure, dates and counts Code only Time in current role, years since a start date, number of positions

A few notes on writing these questions, taken from TypeSafe's guidance on yes/no questions. Ask one condition per question: "works in recruiting and lists Python" is two questions, combined afterwards in code. Phrase every yes/no question so that a high number means yes, and avoid negatives such as "is not a recruiter," which invert the meaning and confuse whatever reads the answer later. When the boundary is fuzzy, add a one-line description of what counts as true and what counts as false.

For Choice questions, always include an "unclear" option. Without it, Jev has to put a thin profile somewhere, and it will pick the nearest real category with a confidence that looks respectable. With it, thin profiles collect in one place you can review.

Check 7 works in two steps. A formula or a few lines of code finds rows that share a company and a similar title, which keeps the number of pairs small. Then each pair goes to Jev as one yes/no question, "These two records describe the same person," with both work histories as the state. TypeSafe publishes a similar pattern for matching records in its entity alignment cookbook.

What to keep out of Jev

Check 8 stays in code because of a limitation TypeSafe states plainly. Its page on Jev 1.13's weak spots lists date and time comparison ("reads dates as text, not as ordered quantities") and math and numbers among nine known failure modes. If your export has a start-date column, a spreadsheet formula gets time in role right every time. Jev will get it right most of the time, which on a list of 1,200 means wrong on some rows you will never look at.

The second thing to keep out is anything that works as a proxy for a protected characteristic. Don't ask Jev to guess age from graduation years, gender or ethnicity from a name, or anything from a photo. Don't build a question around employment gaps. Location deserves the same caution: filter by the market the role is open to in your search tool, and don't hand a model finer-grained geography to reason about.

The simplest way to enforce this is to control what you send. The scripts below send only work text: headline, current title, current company and a trimmed experience section. Names, emails and profile URLs stay in the spreadsheet and never reach the API. That also shrinks each request, which makes it cheaper and, per TypeSafe's own notes on irrelevant context, more accurate.

Run it in Google Sheets

This is the version for recruiters who don't write code. You paste one script into your spreadsheet, add a key, and get a Jev menu that fills eight result columns and a "next step" column saying keep, read or remove.

Before you start, lay out the sheet with a header row and these five columns: A name, B headline, C current title, D current company, E about and experience text. Most sourcing exports can be mapped to this in a couple of minutes. Columns F to N are where the results land.

Then:

  1. Create an account at openrouter.ai, add a few dollars of credit, and create an API key.
  2. In your spreadsheet, open Extensions, then Apps Script.
  3. Delete the placeholder code and paste the script below.
  4. Open Project Settings (the gear icon), scroll to Script properties, and add a property named OPENROUTER_API_KEY with your key as the value. Keeping the key there rather than in the code means you can share the sheet without sharing the key.
  5. Change MUST_HAVE and TARGET_ROLES at the top to match your search, save, and reload the spreadsheet.
  6. Click the new Jev menu, then "Check unchecked rows." Google will ask you to authorize the script the first time.
// Jev list audit for Google Sheets.
// Columns: A name (never sent), B headline, C current title,
// D current company, E about and experience text.
// Results go to columns F to M. Errors go to column N.

const API_URL = 'https://openrouter.ai/api/v1/systemone';
const MODEL = '~typesafe/jev-latest';
const MUST_HAVE = 'Python';          // one hands-on skill your search needs
const TARGET_ROLES = ['backend'];    // role_family values to keep
const BATCH = 25;                    // rows sent in parallel
const TIME_BUDGET_MS = 5 * 60 * 1000; // stop before Apps Script's 6-minute limit

const QUESTIONS = {
  role_family: {
    type: 'choice',
    instructions: 'Which kind of work does this person do in their current role?',
    criteria: {
      backend: 'Hands-on server-side or API software engineering',
      frontend: 'Hands-on web or mobile interface engineering',
      data: 'Hands-on data engineering, analytics engineering or machine learning engineering',
      infrastructure: 'Hands-on DevOps, SRE, platform or cloud infrastructure engineering',
      other_engineering: 'Hands-on software engineering that fits none of the options above',
      not_engineering: 'Recruiting, sales, marketing, product management, managing people without hands-on engineering, or studying',
      unclear: 'The text does not say enough to tell'
    }
  },
  talent_pro: {
    type: 'noul',
    instructions: 'This person works in recruiting, talent acquisition, staffing or HR.',
    criteria: {
      true: 'Their current job is finding, recruiting or placing other people',
      false: 'Their current job is doing the work itself, even if they mention hiring'
    }
  },
  must_have: {
    type: 'noul',
    instructions: 'The text describes a job in which this person worked hands-on with ' + MUST_HAVE + '.',
    criteria: {
      true: MUST_HAVE + ' is part of the work described in at least one role',
      false: MUST_HAVE + ' appears only in a skills list, or not at all'
    }
  },
  title_level: {
    type: 'choice',
    instructions: 'What level does the current job title state?',
    criteria: {
      entry: 'Intern, junior, associate, graduate',
      mid: 'A title with no level word, or "II"',
      senior: 'Senior, or "III"',
      staff_or_principal: 'Staff, principal, distinguished, lead individual contributor',
      manager_or_above: 'Manager, director, head of, VP, CTO',
      not_stated: 'The title does not state a level'
    }
  },
  employer_type: {
    type: 'choice',
    instructions: 'What kind of organization is the current employer?',
    criteria: {
      product_company: 'Builds and sells its own software or technology product',
      consultancy_or_agency: 'Delivers software projects for clients',
      staffing_firm: 'Places contractors or employees with other companies',
      non_tech: 'A business whose main product is not technology',
      academic: 'A university or research institute',
      self_employed: 'Freelance or own company',
      unclear: 'The text does not say'
    }
  },
  thin_record: {
    type: 'noul',
    instructions: 'This record has too little information about work to tell what the person does.'
  }
};

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Jev')
    .addItem('Check unchecked rows', 'checkRows')
    .addToUi();
}

function checkRows() {
  const key = PropertiesService.getScriptProperties().getProperty('OPENROUTER_API_KEY');
  if (!key) throw new Error('Add OPENROUTER_API_KEY under Project Settings > Script properties.');

  const sheet = SpreadsheetApp.getActiveSheet();
  const rows = sheet.getDataRange().getValues();
  const started = Date.now();

  // Rows whose "next step" (column M) is still empty.
  const todo = [];
  for (let i = 1; i < rows.length; i++) {
    if (rows[i][1] && !rows[i][12]) todo.push(i);
  }

  while (todo.length && Date.now() - started < TIME_BUDGET_MS) {
    const batch = todo.splice(0, BATCH);
    const requests = batch.map(i => ({
      url: API_URL,
      method: 'post',
      contentType: 'application/json',
      headers: { Authorization: 'Bearer ' + key },
      muteHttpExceptions: true,
      payload: JSON.stringify({ model: MODEL, state: stateFor(rows[i]), questions: QUESTIONS })
    }));

    UrlFetchApp.fetchAll(requests).forEach((res, k) => {
      const rowNumber = batch[k] + 1;
      if (res.getResponseCode() !== 200) {
        sheet.getRange(rowNumber, 14).setValue('HTTP ' + res.getResponseCode() + ': ' + res.getContentText().slice(0, 200));
        return; // column M stays empty, so the next run retries this row
      }
      const a = JSON.parse(res.getContentText()).answers;
      sheet.getRange(rowNumber, 6, 1, 8).setValues([[
        a.role_family.choice,
        round(a.role_family.confidence),
        round(a.talent_pro.noul),
        round(a.must_have.noul),
        a.title_level.choice,
        a.employer_type.choice,
        round(a.thin_record.noul),
        nextStep(a)
      ]]);
      sheet.getRange(rowNumber, 14).clearContent();
    });
  }
}

// Only work text is sent. Names, emails and URLs stay in the sheet.
function stateFor(row) {
  return {
    headline: String(row[1]).slice(0, 300),
    current_title: String(row[2]).slice(0, 200),
    current_company: String(row[3]).slice(0, 200),
    about_and_experience: String(row[4]).slice(0, 3000)
  };
}

// Starting thresholds. Replace them with the ones your own labels give you.
function nextStep(a) {
  if (a.talent_pro.noul >= 0.85 || a.thin_record.noul >= 0.85) return 'remove';
  if (a.role_family.choice === 'not_engineering' && a.role_family.confidence >= 0.8) return 'remove';
  const roleFits = TARGET_ROLES.includes(a.role_family.choice) && a.role_family.confidence >= 0.8;
  if (roleFits && a.talent_pro.noul <= 0.15 && a.must_have.noul >= 0.7) return 'keep';
  return 'read';
}

function round(x) {
  return Math.round(x * 100) / 100;
}

Add a header row for the results (F role_family, G role_confidence, H talent_pro, I must_have, J title_level, K employer_type, L thin_record, M next_step, N error) so the columns are readable. Each run works for up to five minutes and stops, because Apps Script ends any single execution at six. On a long list, click the menu again and it picks up at the first row without a next step. Rows that hit an error, such as a 429 when the rate limit is busy, get the message in column N and are retried on the next run.

Two things to adjust before you trust the output. First, the role options are written for an engineering search. For a sales or finance search, rewrite them in the same pattern, with the neighbours your exports tend to pull in listed as their own options. Second, the thresholds in nextStep are starting values, not recommendations. The section after the Python version shows how to replace them with your own.

If you would rather drag nodes than paste code, there is a community n8n node, n8n-nodes-agent-langfuse, that added a Jev decision node in version 0.7.0 and reuses n8n's OpenRouter credential (n8n community). The questions and thresholds carry over unchanged.

Run it in Python for bigger lists

Past a few thousand rows, a script on your own machine is faster and easier to rerun. TypeSafe's Python SDK installs with pip install typesafe-sdk and can be pointed at OpenRouter:

import csv
from typesafe_sdk import TypeSafeClient, Noul, Choice

client = TypeSafeClient(
    api_key="YOUR_OPENROUTER_KEY",
    base_url="https://openrouter.ai/api",
    model="~typesafe/jev-latest",
)

questions = {
    "talent_pro": Noul(
        instructions="This person works in recruiting, talent acquisition, staffing or HR."
    ),
    "role_family": Choice(
        instructions="Which kind of work does this person do in their current role?",
        criteria={
            "backend": "Hands-on server-side or API software engineering",
            "frontend": "Hands-on web or mobile interface engineering",
            "not_engineering": "Recruiting, sales, product management, or studying",
            "unclear": "The text does not say enough to tell",
        },
    ),
}

WORK_FIELDS = ("headline", "current_title", "current_company", "experience")

with open("sourced.csv") as f_in, open("checked.csv", "w", newline="") as f_out:
    reader = csv.DictReader(f_in)
    extra = ["role_family", "role_confidence", "talent_pro"]
    writer = csv.DictWriter(f_out, fieldnames=reader.fieldnames + extra)
    writer.writeheader()
    for row in reader:
        state = {k: row[k][:3000] for k in WORK_FIELDS}   # work text only
        r = client.system_one(state, questions)
        row["role_family"] = r.choices["role_family"].choice
        row["role_confidence"] = round(r.choices["role_family"].confidence, 2)
        row["talent_pro"] = round(r.nouls["talent_pro"].noul, 2)
        writer.writerow(row)

Add the remaining questions from the Sheets version in the same way. For tens of thousands of rows, the SDK also has an asynchronous client that sends requests concurrently; keep the concurrency well under the 1,200 requests per minute limit.

Set your thresholds on 60 rows of your own

Jev's selling point is that its probabilities are "trained to be calibrated": if it says 0.9 on a hundred inputs, about ninety should be right (Firecrawl). Treat that as a claim about TypeSafe's test data, not about your spreadsheet. On September 23, data scientist Alex Molas published a critique arguing that calibration depends on the data distribution as much as on the model, cited a test in which Jev put a fair coin landing heads at 0.92, and recommended using Jev's outputs to put records in order rather than reading them as probabilities you can take at face value (Alex Molas). TypeSafe's own confidence documentation says something compatible: the right threshold depends on your domain, so start conservative, test on your own data and adjust.

Doing that for a recruiting list takes about half an hour:

  1. Pull 60 rows from a real export, including some you know are wrong.
  2. Label them by hand before running anything: for each row, your own answer to the recruiter question, the role family and the must-have skill.
  3. Run the sheet on those 60 rows.
  4. Sort by each probability column. Find the value above which your labels are all yes, and the value below which they are all no. Those two numbers are your keep and remove thresholds for that question. Everything between them goes to "read."
  5. Paste them into nextStep and rerun.

If the "read" band ends up holding more than about a third of the list, our rule of thumb is to fix the question rather than loosen the thresholds. A wide middle usually means the wording leaves room for two readings, and Jev answers the one you wrote.

After that, spot-check ten of the removed rows every time you run a new list. The rows you never see are where a bad question does its damage.

What it costs

Jev bills input tokens only. With the trimming in the scripts above, each row is a few hundred tokens of profile text plus the six questions. We estimate 1,000 to 1,500 tokens a row, depending on how long the experience text runs. At $0.042 per million tokens, that works out as follows:

List size Estimated tokens Estimated Jev cost
1,000 rows 1 to 1.5 million About $0.04 to $0.06
10,000 rows 10 to 15 million About $0.42 to $0.63
100,000 rows 100 to 150 million About $4.20 to $6.30

At that price the model stops being the expensive part. The costs that remain are your time labeling the first 60 rows, the "read" band you still go through, and the Google Workspace or OpenRouter limits you might hit on very large runs. Recruitly, a recruitment CRM that put Jev into production the day it came out, describes the same effect: at "a few cents per thousand decisions," it now asks more questions than before and asks them every time (Recruitly). Recruitly also kept its previous language-model path as a fallback for low-confidence answers and outages, which is a sensible pattern to copy.

Where Jev gets recruiting data wrong

TypeSafe's Jev 1.13 weak-spots page lists nine failure modes. Five of them show up in candidate lists often enough to plan for.

Literal reading. Jev "answers the question you wrote, not the one you meant." Ask whether someone works in engineering and "Engineering Recruiter" can come back as a yes, because the words are right there. That is why check 2 exists as its own question instead of being folded into the role question.

Dates and numbers. Covered above. Anything with "years," "since," "at least" or a count belongs in a formula.

Too much text. Unrelated detail acts as a distractor. A profile pasted in full, with fifteen roles, volunteer work and recommendations, gives worse answers than a headline, the current role and the last two or three. The scripts cap the experience text at 3,000 characters for this reason.

Manipulative content. TypeSafe notes Jev is not inherently resistant to prompt injection or misleading framing. Hidden "ignore previous instructions" text is more common in inbound resumes than in sourced profiles, but it exists, and it is one more reason the keep rows get a human read before outreach.

Languages other than English. TypeSafe describes English as primary and other languages as supported with lower accuracy. If you source in Germany or Brazil, label your 60-row calibration sample from those profiles, not from US ones.

What Jev can't tell you about a candidate

Jev reads what is written in the row. That makes it good at catching records that don't belong on a list, and it also sets the ceiling. It can't find people your search missed. It can't tell you what someone is likely to cost, because nothing in a profile says so. And it can't tell you who is likely to reply, which is usually the difference between a clean list and a list that produces calls.

Those are the gaps Glozo is built for. Smart Search reads intent rather than keywords and surfaces people from more than 30 sources, ordered by closeness of match to what the role needs, which cuts down how much cleaning a list needs in the first place. Each profile carries a Market Value estimate and an Open to Offers signal, and the Sourcing Agent runs the search in the background and emails you when a shortlist is ready. The recruiter still reviews and decides. Jev and Glozo sit comfortably together: if you build lists elsewhere and bring them into a spreadsheet, the list audit above is worth running on them.

If you are assembling your own AI stack for sourcing, our guides to using Claude as a recruiter, MCP for recruiters and how AI recruiting agents work cover the pieces that generate text and take actions, which Jev deliberately does not.

Frequently asked questions

What is Jev?
Jev is a decision model from TypeSafe AI, a San Francisco lab that launched on September 15, 2026. It does not generate text. You send it text and typed questions, and it returns structured answers: a probability for yes/no questions, a chosen option with a probability for each option, or a position on a scale you define. It is priced at $0.042 per million input tokens with free output, and most calls return in well under a second.
How can recruiters use Jev?
The safest and most useful first job is checking a sourced list before outreach. Jev can answer short questions about each row, such as whether the current role matches the search, whether the person works in recruiting rather than engineering, whether a must-have skill appears in an actual job, and whether the record has enough information to judge. Dates, tenure and counts should be calculated in the spreadsheet instead, because Jev is documented as unreliable at comparing dates and doing arithmetic.
How much does it cost to check a candidate list with Jev?
With profile text trimmed to the headline, current role and recent experience, a row with six questions runs roughly 1,000 to 1,500 input tokens. At $0.042 per million tokens, that is about four to six cents for 1,000 rows and roughly five dollars for 100,000 rows. Output tokens are free, so the number of questions affects cost only through the extra input text each question adds.
Can I use Jev without writing code?
Yes, with some copy and paste. A short Google Apps Script can send each row of a spreadsheet to Jev through OpenRouter and write the answers into new columns, and a community n8n node supports Jev for people who prefer visual workflows. You still need an OpenRouter account and an API key. TypeSafe paused its own direct signups on September 22, 2026 because of demand.
Is it legal to use Jev on candidate data?
It depends on where in the funnel you use it, and this is not legal advice. New York City's regulator says in its Local Law 144 FAQ that using automated tools on resume banks or for outreach to potential candidates is outside the law, because coverage starts when a person applies. Evaluating applicants is different and can bring audit, notice and human review obligations, including under Colorado's SB 26-189 from January 1, 2027. Federal anti-discrimination law applies at every stage, so avoid questions that act as proxies for age, gender, ethnicity or other protected characteristics.
Should I trust Jev's confidence numbers?
Use them to order rows, and set your own thresholds before acting on them. TypeSafe trains Jev to produce calibrated probabilities, but calibration depends on the data, and at least one published critique found cases where it failed. Label about 60 rows from your own exports by hand, run Jev on them, and choose keep and remove thresholds from where your labels agree with its numbers. Everything in between gets a human read.