A complete working AI agent that analyzes resumes and matches them to job descriptions
What This Project Does
Input: Resume text + Job Description
Output: Skill match score, gap analysis, improvement suggestions
Agent workflow:
1. Extract skills from resume
2. Extract requirements from job description
3. Match skills against requirements
4. Score the candidate
5. Generate specific improvement suggestions
This is a real, portfolio-worthy project. You could turn this into a product.
Project Setup
mkdir resume-analyzer cd resume-analyzer npm init -y
Update package.json:
{ "name": "resume-analyzer", "version": "1.0.0", "type": "module" }
npm install @langchain/langgraph @langchain/openai @langchain/core langchain zod dotenv
Create .env:
OPENAI_API_KEY=sk-proj-your-key-here
Project Structure
resume-analyzer/
├── .env
├── package.json
├── src/
│ ├── tools.js ← all tools the agent can use
│ ├── agent.js ← agent setup and configuration
│ └── index.js ← entry point — run the analyzer
├── data/
│ ├── resume.txt ← sample resume
│ └── job.txt ← sample job description
Step 1 — Sample Data
Create data/resume.txt:
Name: Sofia Sharma
Location: Bangalore, India
Email: sofia@example.com
EXPERIENCE:
Full Stack Developer — TechStartup Pvt Ltd (2022 - Present)
- Built and maintained MERN stack applications (MongoDB, Express, React, Node.js)
- Developed REST APIs serving 50,000+ daily active users
- Implemented JWT authentication and role-based access control
- Used Git for version control and GitHub Actions for CI/CD
- Worked with AWS S3 for file storage
Junior Developer — FreelanceProjects (2021 - 2022)
- Built responsive websites using HTML, CSS, JavaScript
- Integrated third-party APIs (Stripe, Twilio, SendGrid)
- Deployed applications on Vercel and Netlify
SKILLS:
JavaScript, TypeScript, React, Node.js, Express.js, MongoDB, MySQL
HTML, CSS, Tailwind CSS, REST APIs, Git, GitHub
AWS S3, Vercel, Docker (basic), Redis (basic)
Agile methodology, Code reviews
EDUCATION:
B.Tech Computer Science — VIT University (2018 - 2022) — GPA: 8.4/10
CERTIFICATIONS:
- AWS Cloud Practitioner (2023)
- MongoDB Developer Associate (2022)
PROJECTS:
1. E-commerce Platform — Built with MERN stack, integrated Stripe payments
2. Real-time Chat App — Socket.io, React, Node.js, deployed on AWS
3. Portfolio Website — Next.js, deployed on Vercel
Create data/job.txt:
Position: Senior Full Stack Engineer
Company: FinTech Solutions Ltd
Location: Remote / Bangalore
REQUIREMENTS:
- 3+ years experience with React and Node.js
- Strong TypeScript skills (required)
- Experience with PostgreSQL or MySQL (required)
- REST API design and development
- Experience with cloud services (AWS preferred)
- Knowledge of Docker and containerization
- Understanding of microservices architecture
- CI/CD pipeline experience (GitHub Actions, Jenkins)
- Experience with Redis for caching (preferred)
- System design knowledge
- Good communication skills
NICE TO HAVE:
- Next.js experience
- GraphQL knowledge
- Kubernetes experience
- Experience with AI/ML integrations
- Previous fintech experience
ABOUT THE ROLE:
This role requires building and scaling financial applications that process millions of transactions.
The ideal candidate has strong backend skills and understands performance optimization.
Step 2 — Tools
Create src/tools.js:
import { tool } from "@langchain/core/tools"; // tool from "@langchain/core/tools" — correct 2025 import location import { z } from "zod";
// ───────────────────────────────────────── // TOOL 1 — Extract Skills from Resume // Parses resume text and pulls out skills // ─────────────────────────────────────────
export const extractResumeSkilllsTool = tool( async ({ resumeText }) => { // resumeText = the full resume as a plain text string // This tool does text processing to extract skills // In this implementation we do basic keyword extraction // In production you could use a dedicated NLP model
const skillKeywords = [ // Programming Languages "javascript", "typescript", "python", "java", "golang", "rust", "c++", "php", "ruby", // Frontend "react", "vue", "angular", "next.js", "svelte", "html", "css", "tailwind", // Backend "node.js", "express", "fastapi", "django", "spring boot", "laravel", // Databases "mongodb", "postgresql", "mysql", "redis", "elasticsearch", "sqlite", // Cloud "aws", "gcp", "azure", "docker", "kubernetes", "terraform", // Tools "git", "github", "ci/cd", "github actions", "jenkins", "agile", "scrum", // Other "rest api", "graphql", "microservices", "socket.io", "jwt", "oauth", ];
const resumeLower = resumeText.toLowerCase(); // convert to lowercase for case-insensitive matching // "React" and "react" should both match
const foundSkills = skillKeywords.filter(skill => resumeLower.includes(skill) ); // filter = keep only skills that appear in the resume text // example foundSkills: ["javascript", "typescript", "react", "node.js", ...]
// Extract years of experience from resume text const experienceMatch = resumeText.match(/(\d+)\s*(?:year|yr)/gi); // regex matches patterns like "3 years" or "5yr" or "2 Years" // example match: ["2022 - Present", "2021 - 2022"]
const yearsMatch = resumeText.match(/\((\d{4})\s*-\s*(?:Present|\d{4})\)/gi); // matches date ranges like "(2022 - Present)" or "(2021 - 2022)" let totalExperience = 0;
if (yearsMatch) { yearsMatch.forEach(match => { const years = match.match(/\d{4}/g); // extract 4-digit years from each match if (years) { const startYear = parseInt(years[0]); const endYear = years[1] === "Present" ? 2025 : parseInt(years[1]); totalExperience += endYear - startYear; // add duration of this job to total experience } }); }
return JSON.stringify({ skills: foundSkills, // array of detected skills // example: ["javascript", "typescript", "react", "node.js", "mongodb"]
skillCount: foundSkills.length, // total number of skills found // example: 18
estimatedYearsExperience: Math.min(totalExperience, 15), // total years across all jobs (capped at 15) // example: 4 (2 years at TechStartup + 1 year freelance + rounding)
rawSkillsText: resumeText.substring(0, 500), // first 500 chars for additional context }); // returns JSON string — LLM parses this to understand the resume },
{ name: "extract_resume_skills", description: `Extracts and analyzes skills, technologies, and experience from a resume. Use this as the FIRST step when analyzing a resume. Returns a structured list of detected skills and estimated years of experience.`, schema: z.object({ resumeText: z.string().describe("the complete resume text to analyze"), }), } );
// ───────────────────────────────────────── // TOOL 2 — Extract Requirements from Job Description // Parses JD and pulls out what the company wants // ─────────────────────────────────────────
export const extractJobRequirementsTool = tool( async ({ jobText }) => { // jobText = the full job description as plain text
const requirementKeywords = [ "javascript", "typescript", "python", "java", "react", "node.js", "vue", "angular", "mongodb", "postgresql", "mysql", "redis", "aws", "gcp", "azure", "docker", "kubernetes", "microservices", "rest api", "graphql", "ci/cd", "git", "agile", "system design", "next.js", "express", ];
const jobLower = jobText.toLowerCase();
// Separate required vs nice-to-have skills const requiredSection = extractSection(jobText, ["REQUIREMENTS", "REQUIRED", "MUST HAVE"]); const preferredSection = extractSection(jobText, ["NICE TO HAVE", "PREFERRED", "BONUS"]); // extractSection finds the relevant section of the JD
const requiredSkills = requirementKeywords.filter(skill => requiredSection.toLowerCase().includes(skill) ); // skills mentioned in the REQUIREMENTS section // example: ["typescript", "react", "node.js", "postgresql"]
const preferredSkills = requirementKeywords.filter(skill => preferredSection.toLowerCase().includes(skill) && !requiredSkills.includes(skill) // exclude skills already in required list ); // skills mentioned in NICE TO HAVE section // example: ["next.js", "graphql", "kubernetes"]
// Extract minimum years of experience const yearsMatch = jobText.match(/(\d+)\+?\s*years?\s*(?:of\s*)?experience/gi); const minYears = yearsMatch ? Math.max(...yearsMatch.map(m => parseInt(m))) : 0; // take the maximum years mentioned — that's likely the senior requirement // example: "3+ years" → 3
return JSON.stringify({ requiredSkills, // skills that are REQUIRED (must have) // example: ["typescript", "mysql", "aws", "docker"]
preferredSkills, // skills that are NICE TO HAVE (bonus) // example: ["next.js", "graphql", "kubernetes"]
minimumYearsRequired: minYears, // minimum years of experience required // example: 3
totalRequirements: requiredSkills.length + preferredSkills.length, // total number of requirements found }); },
{ name: "extract_job_requirements", description: `Extracts required skills, preferred skills, and experience requirements from a job description. Use this as the SECOND step — after extracting resume skills. Returns required vs preferred skills separately.`, schema: z.object({ jobText: z.string().describe("the complete job description text to analyze"), }), } );
function extractSection(text, sectionNames) { // Helper to find a specific section in the job description text // sectionNames = array of possible headings to look for
for (const name of sectionNames) { const index = text.toUpperCase().indexOf(name); // find where this section heading appears
if (index !== -1) { const afterHeading = text.substring(index); // text from this heading onwards
const nextSectionMatch = afterHeading.slice(name.length).match(/\n[A-Z\s]{3,}:/); // find the next section heading (all caps text followed by colon)
if (nextSectionMatch) { return afterHeading.slice(name.length, name.length + nextSectionMatch.index); // return text between this heading and the next } return afterHeading.slice(name.length, name.length + 500); // if no next section, return next 500 chars } } return text; // if section not found, return full text }
// ───────────────────────────────────────── // TOOL 3 — Match Skills and Calculate Score // Compares resume skills vs job requirements // ─────────────────────────────────────────
export const calculateMatchScoreTool = tool( async ({ resumeSkillsJson, jobRequirementsJson }) => { // resumeSkillsJson = JSON string from extract_resume_skills tool // jobRequirementsJson = JSON string from extract_job_requirements tool
const resumeData = JSON.parse(resumeSkillsJson); const jobData = JSON.parse(jobRequirementsJson); // parse both JSON strings back into JavaScript objects
const resumeSkills = resumeData.skills || []; const requiredSkills = jobData.requiredSkills || []; const preferredSkills = jobData.preferredSkills || []; const minYears = jobData.minimumYearsRequired || 0; const candidateYears = resumeData.estimatedYearsExperience || 0;
// Find matched and missing required skills const matchedRequired = requiredSkills.filter(skill => resumeSkills.includes(skill) ); // skills that are required AND present in resume // example: ["typescript", "mysql", "aws"]
const missingRequired = requiredSkills.filter(skill => !resumeSkills.includes(skill) ); // required skills that are NOT in the resume // example: ["docker", "microservices"]
// Find matched preferred skills (bonus points) const matchedPreferred = preferredSkills.filter(skill => resumeSkills.includes(skill) ); // example: ["next.js"]
// Calculate score components const requiredScore = requiredSkills.length > 0 ? (matchedRequired.length / requiredSkills.length) * 70 : 70; // required skills worth 70% of total score // example: 4/5 required matched = 0.8 × 70 = 56 points
const preferredScore = preferredSkills.length > 0 ? (matchedPreferred.length / preferredSkills.length) * 20 : 20; // preferred skills worth 20% of total score // example: 1/3 preferred matched = 0.33 × 20 = 6.6 points
const experienceScore = candidateYears >= minYears ? 10 : (candidateYears / minYears) * 10; // experience worth 10% of total score // example: 4 years vs 3 required = full 10 points
const totalScore = Math.round(requiredScore + preferredScore + experienceScore); // final score out of 100 // example: 56 + 6.6 + 10 = 72.6 → 73
const verdict = totalScore >= 80 ? "STRONG MATCH — Highly recommended to apply" : totalScore >= 60 ? "GOOD MATCH — Apply with confidence" : totalScore >= 40 ? "PARTIAL MATCH — Apply but address skill gaps" : "WEAK MATCH — Significant skill development needed first";
return JSON.stringify({ totalScore, // overall match score 0-100 // example: 73
requiredMatchPercent: requiredSkills.length > 0 ? Math.round((matchedRequired.length / requiredSkills.length) * 100) : 100, // what % of required skills are matched // example: 80 (means 80% of required skills are in resume)
matchedRequired, // example: ["typescript", "mysql", "aws", "rest api"]
missingRequired, // example: ["docker", "microservices"]
matchedPreferred, // example: ["next.js"]
experienceMatch: { candidateYears, required: minYears, meets: candidateYears >= minYears, // example: { candidateYears: 4, required: 3, meets: true } },
verdict, // human readable overall assessment }); },
{ name: "calculate_match_score", description: `Calculates how well a candidate's skills match the job requirements. Use this THIRD — after extracting both resume skills and job requirements. Returns a detailed score breakdown with matched and missing skills.`, schema: z.object({ resumeSkillsJson: z.string().describe("JSON output from extract_resume_skills tool"), jobRequirementsJson: z.string().describe("JSON output from extract_job_requirements tool"), }), } );
// ───────────────────────────────────────── // TOOL 4 — Generate Improvement Suggestions // Gives specific, actionable advice // ─────────────────────────────────────────
export const generateSuggestionsTool = tool( async ({ missingSkills, candidateBackground, targetRole }) => { // missingSkills = skills the candidate is missing (comma-separated string) // candidateBackground = brief description of current background // targetRole = job title they are targeting
const missing = missingSkills.split(",").map(s => s.trim()).filter(Boolean); // convert comma-separated string to array and clean whitespace // example: "docker, microservices, kubernetes" → ["docker", "microservices", "kubernetes"]
// Learning path suggestions per skill const learningPaths = { "docker": { timeToLearn: "2-3 weeks", resources: ["Docker official docs", "Docker for Developers course on Udemy", "Play with Docker (free sandbox)"], project: "Containerize your existing MERN application", }, "kubernetes": { timeToLearn: "4-6 weeks", resources: ["Kubernetes official docs", "CKA exam prep course", "Killercoda free labs"], project: "Deploy your Dockerized app on a local K8s cluster using minikube", }, "microservices": { timeToLearn: "3-4 weeks", resources: ["Microservices.io patterns site", "Building Microservices book by Sam Newman", "YouTube: TechWorld with Nana"], project: "Split your monolithic app into 2-3 microservices communicating via REST", }, "graphql": { timeToLearn: "1-2 weeks", resources: ["GraphQL official docs", "How to GraphQL tutorial", "Apollo GraphQL docs"], project: "Add a GraphQL API alongside your existing REST API", }, "postgresql": { timeToLearn: "1-2 weeks", resources: ["PostgreSQL Tutorial website", "pgexercises.com for practice", "Supabase docs"], project: "Migrate one of your MongoDB collections to PostgreSQL", }, "system design": { timeToLearn: "4-8 weeks", resources: ["System Design Primer on GitHub", "Designing Data-Intensive Applications book", "ByteByteGo newsletter"], project: "Design and document the architecture of your most complex project", }, };
const suggestions = missing.map(skill => { const path = learningPaths[skill.toLowerCase()]; if (path) { return `${skill.toUpperCase()}: Time to learn: ${path.timeToLearn} Resources: ${path.resources.join(", ")} Hands-on project: ${path.project}`; } return `${skill.toUpperCase()}: Start with official documentation and build a small demo project to practice.`; });
const priorityOrder = missing.slice(0, 3); // top 3 missing skills — focus on these first
return `IMPROVEMENT PLAN FOR: ${targetRole} Background: ${candidateBackground}
PRIORITY SKILLS TO LEARN (top 3 missing required skills): ${priorityOrder.map((s, i) => `${i + 1}. ${s}`).join("\n")}
DETAILED LEARNING PATH: ${suggestions.join("\n\n")}
GENERAL ADVICE: 1. Focus on the top 3 priority skills before applying 2. Build projects that use these new skills — employers verify skills through code 3. Update your GitHub with these new projects before applying 4. Estimated time to be competitive: ${missing.length <= 2 ? "2-4 weeks" : missing.length <= 4 ? "4-8 weeks" : "2-3 months"}`; },
{ name: "generate_suggestions", description: `Generates a detailed, actionable improvement plan based on skill gaps. Use this LAST — after calculating the match score. Returns specific learning resources and project ideas for each missing skill.`, schema: z.object({ missingSkills: z.string().describe("comma-separated list of missing skills"), candidateBackground: z.string().describe("brief description of candidate's current background"), targetRole: z.string().describe("the job title the candidate is applying for"), }), } );
Step 3 — Agent Setup
Create src/agent.js:
import { createReactAgent } from "@langchain/langgraph/prebuilt"; // createReactAgent from langgraph/prebuilt — 2025 standard import // creates a ReAct agent: think → act → observe loop
import { ChatOpenAI } from "@langchain/openai"; import { MemorySaver } from "@langchain/langgraph"; import { extractResumeSkilllsTool, extractJobRequirementsTool, calculateMatchScoreTool, generateSuggestionsTool, } from "./tools.js";
// ───────────────────────────────────────── // CREATE THE RESUME ANALYZER AGENT // ─────────────────────────────────────────
export function createResumeAnalyzerAgent() {
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0, // temperature 0 = fully deterministic // analysis tasks need consistency — same resume always gets same score });
const checkpointer = new MemorySaver(); // enables conversation memory // agent remembers previous analyses in same thread
const agent = createReactAgent({ llm, // the language model
tools: [ extractResumeSkilllsTool, extractJobRequirementsTool, calculateMatchScoreTool, generateSuggestionsTool, ], // all four analysis tools — agent decides order
checkpointer, // enables memory via thread_id
prompt: `You are an expert technical recruiter and career coach AI. Your job is to analyze resumes against job descriptions and provide detailed, actionable feedback.
ANALYSIS WORKFLOW — always follow this exact order: 1. Call extract_resume_skills with the resume text 2. Call extract_job_requirements with the job description text 3. Call calculate_match_score with both outputs from steps 1 and 2 4. Call generate_suggestions with the missing skills from step 3 5. Compile everything into a final comprehensive report
FINAL REPORT FORMAT: ================================ 📊 RESUME ANALYSIS REPORT ================================
🎯 OVERALL MATCH SCORE: [X]/100 Verdict: [verdict from calculate_match_score]
📋 SKILLS BREAKDOWN: ✅ Matched Required Skills: [list] ❌ Missing Required Skills: [list] ⭐ Matched Bonus Skills: [list]
📅 EXPERIENCE: Candidate: X years | Required: Y years | [MEETS/FALLS SHORT]
🚀 IMPROVEMENT PLAN: [content from generate_suggestions]
💡 FINAL RECOMMENDATION: [Your expert opinion on whether to apply now or after skill development] ================================
Be specific, honest, and encouraging. Always provide actionable next steps.`, });
return agent; }
Step 4 — Entry Point
Create src/index.js:
import { createResumeAnalyzerAgent } from "./agent.js"; import { readFileSync } from "fs"; import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // LOAD RESUME AND JOB DESCRIPTION // ─────────────────────────────────────────
function loadFile(filePath) { try { return readFileSync(filePath, "utf-8"); // readFileSync = reads file synchronously // "utf-8" = treat file as text string (not binary) // returns the full file content as a string } catch (err) { console.error(`Could not read file: ${filePath}`); process.exit(1); // stop the program if file not found } }
// ───────────────────────────────────────── // MAIN — Run the analyzer // ─────────────────────────────────────────
async function main() { console.log("\n" + "=".repeat(55)); console.log("🤖 RESUME ANALYZER AGENT"); console.log("=".repeat(55) + "\n");
// Load files const resume = loadFile("./data/resume.txt"); const jobDescription = loadFile("./data/job.txt"); // load both text files from disk
console.log("📄 Resume loaded:", resume.split("\n")[0]); // print first line of resume (usually the name)
console.log("💼 Job loaded:", jobDescription.split("\n")[0]); // print first line of job description (usually the title)
console.log("\n🔄 Running analysis...\n");
// Create agent const agent = createResumeAnalyzerAgent();
// Build the analysis request const analysisRequest = `Please analyze this resume against the job description and provide a complete assessment.
RESUME: ${resume}
JOB DESCRIPTION: ${jobDescription}
Please follow the complete analysis workflow and provide the final report.`;
// Run the agent const result = await agent.invoke( { messages: [{ role: "user", content: analysisRequest }], // send resume + JD as user message }, { configurable: { thread_id: "analysis_001" }, // thread_id for this analysis session } );
// Extract and print the final answer const lastMessage = result.messages[result.messages.length - 1]; // last message = agent's final compiled report
console.log("\n" + "=".repeat(55)); console.log("📊 ANALYSIS COMPLETE"); console.log("=".repeat(55)); console.log(lastMessage.content);
// ── INTERACTIVE MODE ────────────────────────────────────────── // After the initial analysis, allow follow-up questions console.log("\n" + "=".repeat(55)); console.log("💬 FOLLOW-UP QUESTIONS"); console.log('Type a follow-up question or "exit" to quit'); console.log("=".repeat(55) + "\n");
const { createInterface } = await import("readline"); const rl = createInterface({ input: process.stdin, output: process.stdout });
const question = (q) => new Promise(resolve => rl.question(q, resolve));
while (true) { const userInput = await question("You: "); if (userInput.toLowerCase() === "exit") { console.log("\n👋 Goodbye!\n"); rl.close(); break; }
const followUp = await agent.invoke( { messages: [{ role: "user", content: userInput }] }, { configurable: { thread_id: "analysis_001" } } // SAME thread_id = agent remembers the analysis // agent can answer "what was my score?" without re-analyzing );
const followUpMsg = followUp.messages[followUp.messages.length - 1]; console.log("\nAgent:", followUpMsg.content, "\n"); } }
main().catch(console.error);
Step 5 — Run the Project
node src/index.js
Expected Output
=======================================================
🤖 RESUME ANALYZER AGENT
=======================================================
📄 Resume loaded: Name: Sofia Sharma
💼 Job loaded: Position: Senior Full Stack Engineer
🔄 Running analysis...
=======================================================
📊 ANALYSIS COMPLETE
=======================================================
================================
📊 RESUME ANALYSIS REPORT
================================
🎯 OVERALL MATCH SCORE: 74/100
Verdict: GOOD MATCH — Apply with confidence
📋 SKILLS BREAKDOWN:
✅ Matched Required Skills: typescript, mysql, aws, rest api, ci/cd, git
❌ Missing Required Skills: docker, microservices
⭐ Matched Bonus Skills: next.js
📅 EXPERIENCE:
Candidate: 4 years | Required: 3 years | MEETS ✅
🚀 IMPROVEMENT PLAN:
PRIORITY SKILLS TO LEARN (top 2 missing):
1. docker
2. microservices
DOCKER:
Time to learn: 2-3 weeks
Resources: Docker official docs, Docker for Developers...
Project: Containerize your existing MERN application
MICROSERVICES:
Time to learn: 3-4 weeks
Resources: Microservices.io, Building Microservices book...
Project: Split your app into 2-3 services
💡 FINAL RECOMMENDATION:
Sofia is a strong candidate with solid MERN experience and TypeScript skills.
The 2 missing skills (Docker, Microservices) are learnable in 4-6 weeks.
Recommendation: Learn Docker first (2-3 weeks), apply immediately after.
================================
=======================================================
💬 FOLLOW-UP QUESTIONS
Type a follow-up question or "exit" to quit
=======================================================
You: What should I learn first to improve my score?
Agent: Based on your analysis, Docker is the highest-priority skill to learn...
You: exit
👋 Goodbye!
3-Line Summary
- The Resume Analyzer uses four tools in a strict sequence — extract resume skills, extract job requirements, calculate match score, generate suggestions — the agent follows this pipeline automatically based on the system prompt instructions.
createReactAgentfrom@langchain/langgraph/prebuiltwith acheckpointerenables follow-up questions after the initial analysis — the agent remembers the full analysis in the same thread so users can ask "what should I learn first?" without re-running everything.- This project pattern — extract structured data from text, compare two datasets, score the match, generate actionable recommendations — applies to dozens of real use cases beyond resumes: product matching, candidate screening, document comparison, gap analysis.
Module 8.5 — Complete ✅
Coming up — Module 8.6 — Project: Research Agent
An agent that takes a topic, searches multiple sources, synthesizes information, and produces a structured research report. Uses everything from Phase 8 — tool calling, planning, memory, and multi-step reasoning.