AI Resume Screener
Use the Talent Intelligence pack to automatically score and rank resumes against a job description. Integrates with your ATS in minutes.
Manual resume screening is slow, inconsistent, and scales poorly. This tutorial shows you how to wire up LiyaEngine's Talent Intelligence pack to automatically analyse CVs against a job description — returning a structured score, skills match, red flags, and a hiring recommendation for every candidate.
What you'll build:
- A resume analysis API endpoint
- A batch screening script that processes multiple resumes
- A ranked results view with score breakdowns
Prerequisites: A LiyaEngine API key with the Hiring domain enabled. Check your dashboard to confirm hiring appears in your enabled domains.
Step 1 — Understand the response schema
The Talent Intelligence resume-analysis intent returns a structured JSON object. Knowing the schema before you build makes the integration much cleaner.
interface ResumeAnalysisOutput {
candidate_summary: string; // 2-3 sentence overview
overall_score: number; // 0–100
recommendation: "strong_yes" | "yes" | "maybe" | "no";
skills_match: {
matched: string[]; // skills in JD that candidate has
missing: string[]; // skills in JD that candidate lacks
bonus: string[]; // skills not in JD but relevant
};
experience_analysis: {
years_relevant: number;
seniority_match: "under" | "match" | "over";
highlights: string[];
};
red_flags: string[]; // concerns worth probing in interview
interview_questions: string[]; // generated questions based on gaps
metadata: {
guardrails_passed: boolean;
processing_ms: number;
};
}Step 2 — Single resume analysis
The simplest call — analyse one resume against one job description:
// lib/analyseResume.ts
const LIYA_API_KEY = process.env.LIYA_API_KEY!;
const LIYA_API_BASE = process.env.LIYA_API_URL ?? "https://api.liyaengine.com";
export async function analyseResume(
resumeText: string,
jobDescription: string,
candidateId: string,
) {
const res = await fetch(`${LIYA_API_BASE}/v1/hiring/resume-analysis`, {
method: "POST",
headers: {
"Authorization": `Bearer ${LIYA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
resume_text: resumeText,
job_description: jobDescription,
options: {
generate_interview_questions: true,
flag_red_flags: true,
seniority_check: true,
},
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Analysis failed: ${err.error?.message ?? res.statusText}`);
}
const { data } = await res.json();
const output = data.output;
return {
candidateId,
score: output.overall_score,
recommendation: output.recommendation,
summary: output.candidate_summary,
skillsMatch: output.skills_match,
redFlags: output.red_flags,
interviewQuestions: output.interview_questions,
};
}Step 3 — Next.js API route
Expose the analysis as an API route your frontend or ATS webhook can call:
// app/api/screen-resume/route.ts
import { NextRequest, NextResponse } from "next/server";
import { analyseResume } from "@/lib/analyseResume";
export async function POST(req: NextRequest) {
const body = await req.json();
const { resumeText, jobDescription, candidateId } = body;
if (!resumeText || !jobDescription) {
return NextResponse.json(
{ error: "resumeText and jobDescription are required." },
{ status: 400 },
);
}
try {
const result = await analyseResume(
resumeText,
jobDescription,
candidateId ?? crypto.randomUUID(),
);
return NextResponse.json({ success: true, data: result });
} catch (err: any) {
return NextResponse.json(
{ success: false, error: err.message },
{ status: 500 },
);
}
}Test it with curl:
curl -X POST http://localhost:3000/api/screen-resume \
-H "Content-Type: application/json" \
-d '{
"candidateId": "cand-001",
"jobDescription": "Senior TypeScript engineer. 5+ years experience. React, Node.js, PostgreSQL required.",
"resumeText": "Jane Smith. 6 years building full-stack web apps. Expert in React, TypeScript, and Node. Led 3-person team at Acme Corp..."
}'Expected response:
{
"success": true,
"data": {
"candidateId": "cand-001",
"score": 87,
"recommendation": "strong_yes",
"summary": "Strong senior full-stack candidate with 6 years of directly relevant experience. Demonstrated TypeScript and React expertise with team leadership. Minor gap: no explicit PostgreSQL experience mentioned.",
"skillsMatch": {
"matched": ["TypeScript", "React", "Node.js"],
"missing": ["PostgreSQL"],
"bonus": ["Team leadership", "Architecture design"]
},
"redFlags": [],
"interviewQuestions": [
"Tell me about your experience with relational databases — have you worked with PostgreSQL or similar?",
"Describe the largest system you've architected end-to-end."
]
}
}Step 4 — Batch screening
For screening a pool of candidates, run analyses in parallel and sort by score:
// scripts/batch-screen.ts
import { analyseResume } from "../lib/analyseResume";
import fs from "fs";
const JOB_DESCRIPTION = fs.readFileSync("job-description.txt", "utf-8");
const RESUME_DIR = "./resumes";
const CONCURRENCY = 5;
async function batchScreen(candidates: { id: string; filePath: string }[]) {
const results = [];
for (let i = 0; i < candidates.length; i += CONCURRENCY) {
const batch = candidates.slice(i, i + CONCURRENCY);
const batchResults = await Promise.all(
batch.map(c => {
const resumeText = fs.readFileSync(c.filePath, "utf-8");
return analyseResume(resumeText, JOB_DESCRIPTION, c.id);
}),
);
results.push(...batchResults);
console.log(`Processed ${Math.min(i + CONCURRENCY, candidates.length)}/${candidates.length}`);
}
return results.sort((a, b) => b.score - a.score);
}
async function main() {
const files = fs.readdirSync(RESUME_DIR).filter(f => f.endsWith(".txt"));
const candidates = files.map(f => ({
id: f.replace(".txt", ""),
filePath: `${RESUME_DIR}/${f}`,
}));
console.log(`Screening ${candidates.length} candidates...`);
const ranked = await batchScreen(candidates);
console.log("\n=== RANKED RESULTS ===");
ranked.forEach((r, i) => {
console.log(`${i + 1}. [${r.score}/100] ${r.candidateId} — ${r.recommendation}`);
if (r.redFlags.length > 0) {
console.log(` ⚠ Red flags: ${r.redFlags.join(", ")}`);
}
});
fs.writeFileSync("screening-results.json", JSON.stringify(ranked, null, 2));
console.log("\nResults saved to screening-results.json");
}
main().catch(console.error);Step 5 — Display results in your UI
A minimal results table with colour-coded scores:
// components/CandidateTable.tsx
"use client";
interface Candidate {
candidateId: string;
score: number;
recommendation: string;
summary: string;
redFlags: string[];
}
const RECOMMENDATION_COLORS: Record<string, string> = {
strong_yes: "#10b981",
yes: "#0ea5e9",
maybe: "#f59e0b",
no: "#ef4444",
};
export function CandidateTable({ candidates }: { candidates: Candidate[] }) {
return (
<div style={{ overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
<th style={{ padding: "12px 16px", textAlign: "left", fontSize: 12, color: "rgba(240,244,255,0.5)", fontWeight: 600 }}>Candidate</th>
<th style={{ padding: "12px 16px", textAlign: "center", fontSize: 12, color: "rgba(240,244,255,0.5)", fontWeight: 600 }}>Score</th>
<th style={{ padding: "12px 16px", textAlign: "left", fontSize: 12, color: "rgba(240,244,255,0.5)", fontWeight: 600 }}>Decision</th>
<th style={{ padding: "12px 16px", textAlign: "left", fontSize: 12, color: "rgba(240,244,255,0.5)", fontWeight: 600 }}>Summary</th>
</tr>
</thead>
<tbody>
{candidates.map(c => {
const color = RECOMMENDATION_COLORS[c.recommendation] ?? "#6b7280";
return (
<tr key={c.candidateId} style={{ borderBottom: "1px solid rgba(255,255,255,0.05)" }}>
<td style={{ padding: "14px 16px", fontSize: 14, fontWeight: 600, color: "rgba(240,244,255,0.9)" }}>
{c.candidateId}
{c.redFlags.length > 0 && (
<span style={{ marginLeft: 8, fontSize: 11, color: "#f59e0b" }} title={c.redFlags.join(", ")}>
⚠ {c.redFlags.length}
</span>
)}
</td>
<td style={{ padding: "14px 16px", textAlign: "center" }}>
<span style={{
fontSize: 15, fontWeight: 700,
color: c.score >= 80 ? "#10b981" : c.score >= 60 ? "#f59e0b" : "#ef4444",
}}>{c.score}</span>
</td>
<td style={{ padding: "14px 16px" }}>
<span style={{
fontSize: 12, fontWeight: 600, padding: "3px 10px",
borderRadius: 20, background: `${color}18`,
border: `1px solid ${color}30`, color,
}}>{c.recommendation.replace("_", " ")}</span>
</td>
<td style={{ padding: "14px 16px", fontSize: 13, color: "rgba(240,244,255,0.55)", maxWidth: 320 }}>
{c.summary}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}What's next
- ATS webhook — post analysis results directly to Greenhouse, Lever, or your custom ATS via their candidate note APIs
- Custom scoring weights — configure the Talent Intelligence pack to weight skills vs experience vs seniority differently for each role type
- Skills gap reports — aggregate
missingskills across all candidates to understand the talent pool and refine your JD - Guardrails — enable
bias_checkin guardrails config to flag responses that reference protected characteristics
Ready to build?
Get your API key and start integrating in minutes.