Back to selected work

Job Search Agent

A multi-user job intelligence and notification system that makes expensive model work the last step—not the first—and keeps every decision explainable.

FastAPILangGraphPostgreSQLOpenAIAuth0

The problem

Job listings arrive from providers with different fields, weak location evidence, duplicate postings, and descriptions that are expensive to evaluate semantically. Multi-user schedules add ownership, retry, and privacy boundaries.

The design needed to personalize discovery without multiplying API or LLM calls, retain the evidence behind each recommendation, and avoid duplicate emails when scheduled work retries.

Graph workflow

Manual and scheduled runs enter the same compiled LangGraph. Each stage advances only when its artifacts are complete; otherwise the router records what it can and closes the run safely.

START

01 · Prepare

load_profileLoad profile

Resolve the owning profile version.

profile ready
generate_queriesGenerate queries

Build bounded role and location searches.

  • Profile errorFinalize
  • Query-planning errorPersist failed run

02 · Discover

searchSearch

Execute bounded provider runs.

results
normalizeNormalize

Build canonical job artifacts.

complete
deduplicateDeduplicate

Resolve stable job identity.

  • All providers failedNormalize failure evidence
  • Incomplete search, normalization, or identityFinalize

03 · Evaluate

filterHard filter

Apply deterministic eligibility rules.

eligible jobs
scoreScore

Run isolated semantic evaluation.

scored jobs
rank_selectRank & select

Apply threshold and result limit.

  • Incomplete filter, score, or ranking artifactsFinalize

04 · Deliver

persistPersist

Commit history before effects.

persisted
decisionSelected jobs?

Choose the notification path.

  • Not persisted or no selected jobsFinalize without notification
notifyNotify

Send an idempotent digest.

always
finalizeFinalize

Close the run and status.

END

One correlation ID connects the database run, logs, notification intent, and privacy-projected LangSmith trace.

Production decisions

  1. Filter before scoring. Definite age, location, authorization, title, and salary mismatches are rejected before they can spend an LLM request.
  2. Keep graph state small. Full profiles, jobs, and evaluations live in run-scoped artifacts; the LangGraph state contains identifiers, routing status, timestamps, and warnings.
  3. Correlate without leaking content. The search-run ID is reused across PostgreSQL, logs, notifications, and traces. Trace exports allow IDs, versions, counts, and usage—not résumé text or job descriptions.
  4. Persist before sending. Notification intent is committed before Resend is called. A stable idempotency key and database claim prevent retry-driven duplicate delivery.

Engineering outcomes

Failure isolation

One provider or per-job scoring failure can produce a useful partial result without erasing completed work.

Bounded cost

Query, result, concurrency, token, call, and estimated-cost ceilings constrain each run.

Explainable history

Immutable observations and evaluation versions preserve what was seen and why a job ranked.

Shared execution path

Manual and scheduled runs use the same graph, persistence, tracing, and notification boundaries.