Phase 4: Security Features — Part 1 (MFA & Adaptive Policies)

Here's the genuinely good news for this entire phase: because TaskFlow uses the redirect model we built in Phase 2 — where the actual login form lives on Okta's hosted page, not inside our Next.js app — everything in this lecture requires zero code changes. MFA, adaptive sign-on, self-service registration, social login, and passwordless — all of it is configured entirely in the Okta Admin Console, and your existing /api/auth/login route automatically benefits, because Okta simply shows a different, richer login experience on its own page. This is one of the strongest arguments for the redirect model over trying to build a custom login form yourself.

Multi-Factor Authentication: The Two Separate Policies You Need to Understand

A common point of confusion: people expect one setting called "enable MFA." Okta actually splits this into two distinct policies that work together, and understanding the difference will save you a lot of head-scratching:

  1. Authenticator Enrollment Policy — Controls which MFA methods (Okta calls these "authenticators") users are allowed or required to enroll in, and when they're prompted to enroll (immediately, or with a grace period of skips). This is under Security → Authenticators in the Admin Console. Note the naming: in Okta's current Identity Engine (what your Integrator Free Plan org runs), this was renamed from the older "MFA Enrollment Policy" you'll see in outdated tutorials.
  2. Authentication Policy (App Sign-in Policy) — Controls when an already-enrolled user is actually challenged for MFA during login — for example, every time, once per session, or only under risky conditions. This lives under Security → Authentication Policies → App sign-in.

In short: Enrollment Policy decides what methods exist for a user to use. Authentication Policy decides when Okta actually asks for them.

Step 1: Enabling Authenticators

Go to Security → Authenticators in the Admin Console. You'll see a list of available authenticator types. Okta enables Okta Verify (its own push-notification/TOTP app) and Password by default. To add more:

  • Click Add authenticator.
  • Common beginner-friendly choices: Email (sends a one-time code — good default, no app install needed), Phone (SMS or voice call), and Okta Verify (push notification to a phone, most secure and lowest-friction once installed).
  • For each one you add, you'll configure whether it's Required, Optional, or used only for specific policies.

For this course, enable Okta Verify and Email — enough to demonstrate a real MFA prompt without needing extra third-party accounts.

Step 2: Building the Authenticator Enrollment Policy

Still under Security → Authenticators, click the Enrollment tab. Here you define enrollment policies that determine which authenticators a given group of users must set up, and how strictly. For TaskFlow:

  1. Click on the default policy (or Add a Policy for a specific group, like requiring stricter enrollment for an "Admins" group later).
  2. Set Okta Verify to Required.
  3. Set Email to Optional (a backup method).
  4. Save.

Once saved, the next time a user without Okta Verify enrolled tries to log in, Okta's hosted page will automatically prompt them to set it up — scanning a QR code with their phone — entirely on Okta's side, before redirecting back to your redirect_uri.

Step 3: Requiring MFA at Login — the App Sign-in Policy

Enrollment alone doesn't force MFA to be checked at every login — that's the Authentication Policy's job. Go to Security → Authentication Policies → App sign-in, and find (or create) the policy attached to your TaskFlow application.

Every App Sign-in Policy starts with a single catch-all rule that applies to everyone by default. Click into it (or Add rule to create a more specific one above it) and configure:

  • IF conditions — who this rule applies to (e.g., "any user," or scoped to a specific group).
  • THEN — Access — set to Allowed.
  • THEN — Authentication requirements — this is the key setting. Choose Password + Another factor to require MFA on every login, or explore the Possession factor options (like Okta Verify specifically) for stronger requirements.
  • Re-authentication frequency — how often a returning user must re-prove MFA: every sign-in, once per session, or on a custom interval.

Save, and log out and back into TaskFlow (http://localhost:3000/login) to see it in action — Okta's hosted page will now prompt for your password, then a second factor, before redirecting back to /dashboard.

Step 4: Making It Adaptive — Contextual Rules

"Adaptive MFA" just means: instead of one blanket rule for everyone, you stack multiple rules with different conditions, and Okta evaluates them in priority order (rules are checked top to bottom; the first matching rule wins). This lets you ask for MFA only when something looks risky, and skip friction otherwise.

Inside the same App Sign-in Policy, click Add rule and explore the IF conditions available — these are the actual signals Okta can evaluate:

  • User's risk score — Okta's own behavioral risk analysis (available depending on plan/features enabled).
  • Device is not registered / New device — the specific behavior detector mentioned in Okta's own current release notes as something you can combine with "MFA required" in a policy.
  • Network zone — e.g., require stricter MFA outside your office's known IP range, or when connecting through an anonymizing proxy.
  • User's group membership — different rules for different groups (this becomes very useful once we build role-based access in Phase 5).

A realistic adaptive setup for TaskFlow: one rule at the top that says "if device is new/unrecognized, require Password + Okta Verify," and the catch-all rule below it set to "Password only" for recognized, trusted devices. Because rules are evaluated in order and the first match wins, place your stricter, more specific rules above the general catch-all.

Step 5: Verifying the Right Engine

One thing worth double-checking now, since old tutorials frequently mix this up: if you ever see menu items called "Sign On Policies" (singular, under an older-looking menu) instead of "Authentication Policies → App sign-in", that means you're looking at documentation for Okta Classic Engine, not Identity Engine — the two have genuinely different menus and concepts, and Classic Engine guidance won't match what you see in your own console. Every Integrator Free Plan org (what we set up in Phase 1) runs on Identity Engine, so always confirm you're following Identity Engine-specific docs and instructions like the ones in this lecture.


What TaskFlow Has Now

Without touching a single line of Next.js code, TaskFlow now enforces real, configurable, adaptive multi-factor authentication — enrollment requirements, per-app authentication rules, and contextual conditions like new-device detection — all live-tested through the same login button we built in Phase 2.

Module 8.5 — Project: Resume Analyzer Agent

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

  1. 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.
  2. createReactAgent from @langchain/langgraph/prebuilt with a checkpointer enables 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.
  3. 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.

Phase 4: Security Features — Part 1 (MFA & Adaptive Policies)

Here's the genuinely good news for this entire phase: because TaskFlow uses the redirect model we built in Phase 2 — where the actual l...