Back to case study Technical architecture

PM Job Agent - Technical Architecture

Autonomous daily job scraping, scoring, and email delivery pipeline. Built as a cloud-native workflow that scans 8 platforms, ranks 50 roles, and keeps running cost at INR 0/month.

GitHub Actions 8 scrapers ~380 raw jobs/day 50 ranked and emailed 08:15 IST daily INR 0/month

Pipeline at a glance

Six boxes from trigger to delivery.

01

Trigger

08:15 IST dual trigger with idempotent guard.

02

Scrape

8 platforms, about 380 raw roles per day.

03

Dedup

URL and semantic cross-platform collapse.

04

Score

Hard filters, deterministic rules, then LLM.

05

Rank

Score bands, queue merge, top-50 cap.

06

Deliver

Excel attachment and email digest around 08:18 IST.

Daily trigger

Dual-trigger scheduling.

cron-job.org is the primary scheduler and GitHub cron is the fallback. Both land in the same GitHub Actions workflow, and a pre-flight check exits quickly if the digest already ran that day.

Primary

External cron

cron-job.org triggers the GitHub workflow at 08:15 IST through the workflow dispatch API.

Fallback

GitHub cron

Native scheduled workflow runs later as a zero-cost safety net if the primary trigger fails.

Guard

Idempotent run

A run-history check makes double-firing safe. The second run exits without resending.

Why GitHub Actions, not a VPS? Zero idle cost, encrypted secrets, run logs, and no 24/7 process supervision for a workload that runs for minutes a day.

Step 1 - Scrape

Eight platform scrapers.

Each scraper has platform-specific constraints, freshness caps, seniority filters, and error isolation. A crash in one source does not abort the run.

LinkedIn

Guest API

Paginated job search with experience filter and 72-hour freshness cap.

IIMJobs

Internal REST API

IT, systems, and consulting verticals with post-fetch seniority filtering.

Adzuna

Public API

Multi-board aggregator across several query combinations.

Indeed

Apify actor

Managed actor with structured output and PM-title filtering.

Instahyre

Authenticated HTTP

Login, session cookies, opportunities API, then seniority filtering.

Hirist

Selenium

Headless Chrome across product-management role slugs.

We Work Remotely

RSS + cloudscraper

RSS pull, freshness cap, language checks, and liveness validation.

Wellfound

ScraperAPI proxy

Residential proxy path with structured extraction and global-hiring checks.

Step 2 - Deduplicate

SQLite-backed memory.

The database stores seen jobs, overflow queue items, and cached scores. URL matching catches obvious repeats. Semantic company-role keys catch the same role posted across different platforms.

seen_jobs

Every URL already emailed plus a normalized company-role key. 30-day rolling window.

job_queue

Overflow beyond the top-50 cap and deferred items for leaner days.

score_cache

Persisted LLM scores keyed by URL to save quota and avoid repeated scoring.

Why SQLite committed to the repo? The database is tiny and single-writer. Committing it back gives free backup, versioning, and zero managed database overhead.

Step 3 - Score

Three-layer scoring pipeline.

Deterministic filters run first, LLM only for the ambiguous tail. This keeps the system predictable and protects free-tier quota.

Layer 1

Hard filters

Seniority keywords, freshness caps, title filters, and geography exclusions before a job enters the scoring pool.

Layer 2

Rules

Weighted product fit, AI/GenAI relevance, seniority match, and location/work-mode signals.

Layer 3

LLM review

Groq scoring for the unclear jobs where language nuance matters.

Why LLM only for the ambiguous tail? Rules handle the obvious. The LLM earns its quota where language ambiguity actually matters.

Steps 4 and 5

Rank, package, and deliver.

Must-apply

Score 65 and above. Highest fit against the profile, shown as the top tier.

Worth a look

Score 55 to 64. Borderline fit, used when the top tier is not already full.

Overflow queue

Anything beyond 50 moves to the queue and can re-enter on future runs.

Excel attachment

Rank, Score, Role, Company, Platform, Location, Work Mode, Posted, Reason, and Apply link.

Summary email

Stats, platform breakdown, and a clean call-to-action to open the workbook.

Delivery and health

Resend email delivery plus a separate health report so failures are visible.

External services

Free-tier discipline by design.

Groq API

LLM scoring with llama-3.3-70b-versatile. About 5 RPM used against 30 RPM free.

Resend

Transactional email and attachments. About 60 emails/month against 3,000/month free.

Adzuna

Aggregator API. About 60 calls used against 1,000/month free.

Apify

Managed Indeed actor, currently below the monthly platform credit.

ScraperAPI

Residential proxy path for Wellfound where direct access can fail.

GitHub Actions

Runner, dispatch, state commit-back, and operational logs.

Operations

Runtime modes, guardrails, and evals.

The agent is designed to run unattended, but not blindly. It has explicit modes for testing, safety checks around scraped content, and evaluation signals that show whether the daily digest is still useful.

Runtime modes

  • Production: full pipeline, email, DB writes, commit-back.
  • Dry run: scrape, score, and rank without email or writes.
  • Preview: saves digest HTML instead of sending.
  • No-persist: sends a real email but skips DB writes.

Guardrails

  • Excel formula-injection escape on scraped text.
  • 30-second bounds on LLM and email calls.
  • Per-scraper isolation so one failure does not kill the run.
  • Cross-platform dedup in two passes.
  • Private repo, encrypted secrets, no plaintext profile logging.

Reliability layer

Guardrails and evals make the agent trustworthy.

The architecture is not only scrape, score, send. It also checks whether the input is safe, whether the run is healthy, whether the ranking is relevant, and whether the final delivery is complete.

Input safety

Scraped content guardrails

Escapes spreadsheet formulas, normalizes source text, keeps profile data out of plaintext logs, and avoids trusting scraped fields blindly.

Run safety

Failure isolation

Each scraper can fail independently, external calls have time bounds, and the daily idempotency check prevents duplicate sends.

Relevance eval

Quality feedback loop

Apply rate, rejected roles, duplicate misses, and weak-fit reasons show whether the scoring weights need adjustment.

Delivery eval

Digest completeness

Health reporting checks source counts, top-50 packaging, attachment generation, and email delivery so failures are visible.

At a glance

The operating shape.

8Scrapers
~380Raw jobs/day
3Scoring layers
50Jobs emailed
30Day dedup window
08:15IST daily
INR 0Monthly cost

Forkable pattern

To adapt this for another person or role, swap the profile, platforms, scoring weights, and delivery channel. The load-bearing choices are the dual trigger, three-layer scoring, SQLite-in-repo state, dedup strategy, and free-tier discipline.