AI-powered job search agent that scores listings against your CV, detects skill gaps over time, and delivers daily email digests. Built for developers who'd rather configure YAML than click through job boards.
Time to First Digest: < 5 minutes from clone to email in your inbox.
Job Sources Intelligence Output
───────────── ──────────── ──────
Indeed ─┐
Google ─┤ ┌─ Language detection ┌─ Email digest
LinkedIn ┼─→ Dedup → Prefilter ─┤ US-only detection ─→ │ (daily, tiered)
Greenhouse└─ Deal breaker scan │
Lever ──┘ │ ┌─ Heuristic gate │
LLM Parse ─┤ (cost control) ─→ ├─ Gap history
└─ LLM Score vs CV │ (JSONL, 90 days)
(full CV in context, │
not RAG — see ADR-001)└─ JSON output
73 listings scraped → 16 pass prefilter → 16 parsed → scored and ranked → email with apply/review/skip tiers. Runs in ~90 seconds. Costs ~$0.11/run.
git clone https://github.com/yourusername/jobagent.git
cd jobagent
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# Configure
cp .env.example .env # Add OPENAI_API_KEY, GMAIL_ADDRESS, GMAIL_APP_PASSWORD, NOTIFY_EMAIL
nano config/profiles/juan.yaml # Edit profile to match your background
# Run
python main.py --notifyCheck your email. You should have a digest with scored job listings, strengths, gaps, and direct apply links per role.
Each job is evaluated across 5 dimensions against your full CV:
| Dimension | Weight | Source |
|---|---|---|
| Domain fit | 0–25 | Work history domains × recency × duration |
| Seniority fit | 0–20 | Auto-computed from target level + career track |
| Technical depth | 0–20 | Role requirements vs candidate foundation |
| Profile evidence | 0–20 | How well CV excerpts support JD requirements |
| Strategic impact | 0–15 | Trajectory advance vs lateral move |
Jobs scoring ≥50 land in Tier A (apply), 30–49 in Tier B (review), <30 in Tier C (skip).
A post-parse heuristic gate (ADR-006) skips expensive LLM scoring for jobs the heuristic already identifies as poor fits. They still appear in your digest — they're just not scored with the full rubric.
Every scored job's strengths and gaps are persisted to data/gap_history/. Over time this reveals:
- Skill gaps that recur across high-fit roles (actionable: take a course, build a project)
- Storytelling gaps where you have the experience but your CV doesn't surface it (actionable: rewrite bullets)
- Scoring bugs where the system flags a gap that contradicts your CV (actionable: adjust the scoring prompt)
Gap analysis across rolling windows is planned. The data accumulates from day one.
This project was built using Claude Code as a development accelerator. The architecture, trade-off decisions, prompt design, and product direction are mine — Claude Code handles implementation under constraints I define and maintain.
This is a deliberate design choice, not a shortcut. A senior PM in 2026 who can't leverage AI coding tools is leaving significant velocity on the table. The question isn't whether to use them, but how to maintain quality and ownership when you do.
How I maintain ownership and quality:
The repo is structured so that the high-leverage decisions live in human-readable files that Claude Code reads as context — not scattered through Python strings:
CLAUDE.md— architecture, invariants, and constraints Claude must followprompts/— scoring rubric and parser logic, versioned as source of truthpatterns/— interface contracts per module, updated before Claude touches related code.claude/rules/— session-level conventions (coding style, doc-sync protocol)
When I change the scoring model or remove a feature, I update the relevant patterns/ file first. Claude Code reads it and implements accordingly. When it doesn't — I catch it in review and tighten the rules. That feedback loop is itself a product decision.
What this means in practice:
Every architectural decision in this repo has an ADR explaining the trade-off. Every module has an interface contract in patterns/. Prompts are treated as code — versioned, externalised, never inlined. The result is a codebase I can reason about, modify, and hand off — even if most of the Python was generated.
See ADR-002 for the full rationale on LLM-friendly DX as a design principle.
Every significant trade-off is documented as an ADR in docs/decisions/:
| ADR | Decision | Key trade-off |
|---|---|---|
| 001 | Full CV in context over RAG | Eliminated ChromaDB. CV is 5K tokens — RAG retrieval caused information loss without cost savings |
| 002 | LLM-friendly DX as design principle | Repo designed for LLM consumption: patterns, schemas, versioned prompts |
| 003 | gpt-4o-mini for parsing, gpt-4o for scoring | Parsing is extraction (mini sufficient); scoring needs nuanced judgment (4o) |
| 004 | Email digest over web app | 90% of value at 10% of effort. Push beats pull for job search consistency |
| 005 | Monochrome zinc + orange accent | Accessibility (deuteranopia-safe), graceful email client degradation |
| 006 | Post-parse heuristic gate | Cost control: skip LLM scoring for obvious non-fits. Jobs still visible in digest |
All configuration is YAML. Profile drives scoring weights, search config drives sources.
config/
├── profiles/juan.yaml # Your background, target role, skills, salary floor
├── preferences.yaml # Deal breakers, title rules, excluded companies
├── searches.yaml # Job board search terms and locations
└── watchlist.yaml # Company ATS boards to poll (Greenhouse/Lever slugs)
Seniority weights are auto-computed from target.level + target.track in your profile. You declare intent ("I want principal IC roles"), the system calculates the weights. See the seniority weight generation section in CLAUDE.md.
GitHub Actions runs the pipeline daily at 07:00 CET on weekdays:
# .github/workflows/daily_digest.yml
on:
schedule:
- cron: '0 6 * * 1-5' # 06:00 UTC = 07:00 CET
workflow_dispatch: {} # Manual triggerRequires repo secrets: OPENAI_API_KEY, GMAIL_ADDRESS, GMAIL_APP_PASSWORD, NOTIFY_EMAIL.
├── main.py # Pipeline orchestrator
├── scraper.py # Indeed/Google/LinkedIn via python-jobspy
├── ats_scraper.py # Greenhouse/Lever API poller
├── prefilter.py # Fast keyword filtering (no API calls)
├── parser.py # LLM extraction (gpt-4o-mini)
├── scorer.py # LLM scoring (gpt-4o, full CV context)
├── gap_tracker.py # Persists strengths/gaps to JSONL
├── notifier.py # Email digest via Gmail SMTP
├── user_config.py # Profile loading, seniority weight computation
├── config/ # YAML configuration (profiles, searches, preferences)
├── knowledge/ # Per-user CV/knowledge base for scoring
├── prompts/ # Versioned business logic prompts (source of truth)
├── patterns/ # Interface contracts for LLM developers
├── schemas/ # Output format contracts (JSON Schema)
├── docs/decisions/ # Architecture Decision Records
├── templates/ # Jinja2 email templates
├── data/ # Runtime data (gap history, gitignored)
└── output/ # JSON results per run
- Python 3.12
- OpenAI API (gpt-4o-mini for parsing, gpt-4o for scoring)
- python-jobspy + Greenhouse/Lever/Ashby APIs + WTTJ Algolia for job sourcing
- Gmail SMTP for email delivery
- Jinja2 for email templates
- GitHub Actions for daily scheduling
No database. No web framework. Intentionally simple — see the ADRs for why.
$0.11/run. $0.04/job). The heuristic gate keeps scored jobs to a minimum.$2.42/month at 22 weekday runs. Parsing uses gpt-4o-mini ($0.001/job), scoring uses gpt-4o (
| Phase | Status |
|---|---|
| Scrape + prefilter + parse + heuristic rank | ✅ Complete |
| LLM scoring with full CV context | ✅ Complete |
| Email digest + GitHub Actions | ✅ Complete |
| Gap persistence | ✅ Complete |
| DX layer (ADRs, patterns, schemas, prompts) | ✅ Complete |
| Onboarding from CV (onboard.py) | ✅ Complete |
| Gap analysis + recommendations | 🔜 Planned |
| CV tailoring per application | 📋 Future |
MIT