Skip to content
ZeyadKhalil
Agentic AI · Semantic Search · Full-Stack Product

Agentic AI Job Application Copilot

A full-stack AI product that discovers graduate roles, ranks them against a candidate profile using embeddings and exposes the workflow through a production-style frontend. The system combines scheduled scrapers, semantic matching, API design, server-state management and a focused 28-test suite.

Backend
FastAPI · SQLAlchemy 2 · APScheduler
Scraping
Greenhouse-backed pipelines
Matching
Embeddings-based semantic ranking
Frontend
Next.js 15 · TanStack Query
Testing
Pytest · 28 unit tests

Overview

The Job Application Copilot is a small but seriously engineered agentic AI system: it crawls graduate listings on a schedule, ranks them against the candidate's CV using embeddings, surfaces the best matches in a real product UI, and helps prepare the application materials around them.

The engineering value is in the system boundaries: how scheduled workers persist data, how the matching service exposes results through a stateless API, how the frontend handles cache invalidation for a changing dataset and how tests protect the scraping and ranking logic from regressions.

Problem framing

A graduate job search is an information problem dressed up as an emotional one. Hundreds of new postings appear every week across employer ATSes (Greenhouse, Workday, Lever, Ashby) and most of them are nearly irrelevant for any given candidate. The two real costs are:

  • Discovery cost: finding the small subset of postings that actually fit, quickly enough to apply early.
  • Application cost: preparing tailored materials for the ones that fit, without burning out on the ones that do not.

The Copilot addresses both sides of that pipeline with a persistent backend, repeatable data workflows and a frontend designed for repeated use.

Architecture

  1. Step 01

    Scheduler

    APScheduler · cron-style jobs

  2. Step 02

    Greenhouse scrapers

    Per-employer adapters

  3. Step 03

    Postgres / SQL

    SQLAlchemy 2 ORM

  4. Step 04

    Embedding pipeline

    CV + JD vectors

  5. Step 05

    Matching service

    Cosine similarity + filters

  6. Step 06

    FastAPI

    REST endpoints

  7. Step 07

    Next.js 15 UI

    App Router · RSC

  8. Step 08

    TanStack Query

    Server state + caching

The split between the long-running scheduler process and the stateless API is deliberate. The API stays restartable and horizontally scalable, while the scheduler holds the cron state.

Backend

  • FastAPI with Pydantic models on every request boundary. Endpoint surface kept narrow: list jobs, get job, refresh now, profile update, match-explanations.
  • SQLAlchemy 2 with the modern 2.x typed query API and explicit session lifecycle. No implicit thread locals or global session. Clean separation of read and write paths.
  • Schemas designed around the job lifecycle: jobs, employers, job_versions (postings change), candidate_profiles, match_scores, applications.
  • Migrations under version control in an Alembic-style workflow, so the scraper schema evolves without breaking running deployments.

Scrapers

  • APScheduler drives periodic crawls per employer. Job intervals are jittered to avoid thundering-herd patterns against employer ATSes.
  • Per-employer Greenhouse adapter classes implement a common interface (list_jobs(), fetch_jd(job_id)) so adding a new source is a small, well-tested change rather than a refactor.
  • Diff-based ingestion. Only changed or newly published postings are re-embedded, which keeps the embedding bill small.
  • Failure handling: retries with backoff, structured error logs, and a per-source health metric exposed to the API. A broken scraper does not silently halt the rest of the pipeline.

Matching pipeline

Matching is a two-stage cascade rather than a single expensive semantic call.

  1. Hard filters first. Visa eligibility, location, seniority (graduate / entry-level), and explicit-exclude keywords. These cheap filters eliminate the vast majority of postings before any embedding comparison runs.
  2. Embedding-based semantic ranking. The candidate's CV is embedded once and cached; each candidate posting is embedded; cosine similarity ranks the survivors of stage one.
  3. Explainability. Top matches return not just a score but the contributing JD sections: "these requirements aligned with these CV bullets". This is the difference between a black box and a tool a candidate will actually use.

Agentic workflow design

The "agentic" part is not a marketing label. It is a specific design choice. The system is built as a set of cooperating, restartable workers with explicit handoffs:

  • Discovery worker: finds candidate postings through scraping.
  • Enrichment worker: embeds, deduplicates and classifies postings.
  • Ranking worker: produces match scores against the candidate profile.
  • Application-prep worker: generates a starting draft of cover-letter context and tailored bullet points for high-match roles, with the candidate firmly in the loop.

Each worker is independently testable, idempotent on retry, and uses the database as its handoff point. There are no in-memory queues to lose state on restart.

Frontend

  • Next.js 15 with the App Router; React Server Components for the static shell, client components only where they earn their cost.
  • TanStack Query for server state, which fits a product where the server-side dataset is constantly changing under the user. Stale-while-revalidate, optimistic updates on apply / dismiss actions, and well-defined query keys keyed by filter state.
  • Filters (location, seniority, source, employer) are URL-synchronised so a candidate can bookmark or share a particular slice of roles.
  • Match-detail view shows the ranked alignment between CV bullets and JD requirements, not just a single score.

Testing strategy

The 28-test pytest suite is small but is on the parts of the system where a regression would actually hurt:

Scraper adapters

Frozen-fixture replay tests

Schema mapping

JD JSON → ORM round-trip

Match scoring

Edge cases · empty CV · zero overlap

API contracts

Pydantic request/response shapes

Migrations

Up/down on a clean DB

Total
28

All green on CI

The deliberate choice was to write fewer, higher-value tests on the boundary contracts and on the matching invariants, rather than chasing line-coverage. Scraper tests use frozen JSON fixtures so the suite does not depend on external services.

Operability

  • Health endpoints expose per-source last-successful-crawl timestamps so a silently broken scraper is visible immediately.
  • Structured logging with correlation IDs flowing from scraper to API to UI.
  • Local dev stack is one docker compose up away from a production-shaped environment, which keeps debugging fast.

Engineering principles applied

  • Database as the system of record. Workers communicate via the database, not via in-memory queues, so restarts never lose state.
  • Small surface, sharp tests. 28 tests, all on business-meaningful invariants.
  • Cheap stages first. Hard filters before semantic ranking; no expensive embedding calls on irrelevant postings.
  • Idempotency everywhere. Re-running a scrape, a match or a worker job produces the same end state.
  • Candidate in the loop. The agent prepares drafts; the candidate ships. Trust beats automation.

Future work

  • Multi-source ingestion for Workday, Lever, Ashby and direct careers-page adapters alongside Greenhouse.
  • Calibrated match-score reliability diagrams to turn the cosine similarity into a probability the candidate will be shortlisted, learnt from outcomes.
  • Automated cover-letter draft generation grounded in CV evidence, with explicit refusal to invent experience the candidate does not have.
  • Outcome tracking across applications, responses and interviews so the matcher can be tuned against actual signal.