🧠AI

Building JobPilot: An Autonomous Career Agent That Never Nags You

I built an agent that discovers jobs, scores real fit, and prepares applications in the background — so job searching stops being a second job. Here's what I built for Google's All Things Agentic hackathon, and what I learned building it.

Shivam Sahil
Shivam Sahil
Found my Ikigai in helping and uplifting world!
Aug 29, 2026
📖9 min read
#ai
Building JobPilot: An Autonomous Career Agent That Never Nags You

This post was written for, and about, my submission to Google's All Things Agentic hackathon on Devpost.

Here's a walkthrough of how JobPilot actually got built, in the order it got built, including the part that turned out to be the hardest and the parts I deliberately left out. If you want to see it running before reading the build steps, here's a short demo first:

https://youtu.be/tJJQEAwKync

›Step 0 — Start from something that already worked

I didn't start from zero. I already had a working, personal resume pipeline: achievements lived once as dated, sourced facts, and resumes were generated views over that evidence — never hand-typed claims that could drift from the truth. That system solved truthfulness but not effort — finding postings, judging fit, and re-tailoring a resume for each one was still entirely manual. The plan for JobPilot was to keep that fact base as the foundation and wrap a persistent agent around it, so a human only shows up to approve or reject, not to re-derive the same judgment call over and over.

›Step 1 — A cloud-neutral domain model, before touching any agent framework

Before writing a single line against Google ADK, I defined the actual business objects as plain Python with zero cloud SDK imports: Opportunity, Company, Application, Interview, CareerProfile, plus the fit-scoring rubric and the autonomy/approval policy. The rule I held myself to: nothing about what a fit score means or when an action needs human approval should ever import google.adk or a Firestore client. That discipline is what let the same logic run against Google ADK today, with an AWS Strands runtime targeting the identical core later, without duplicating a single scoring rule.

›Step 2 — Context Manager: the durable record everything else reads from

Next was the Context Manager — the owner-scoped store for profile, preferences, facts, jobs, applications, and interview history. Locally it's SQLite; in the cloud it's Firestore, behind the same interface, so the rest of the system never knows which one it's talking to. This is also where the resume-import feature lives: paste in a resume or a project update, and a bounded Gemini call turns it into a structured draft you review before saving — it never writes directly, because an unreviewed auto-write into your source-of-truth facts is exactly the kind of thing that should require a human glance first.

›Step 3 — JD Analyser: a rubric, not a black-box score

Every discovered posting gets scored on six named, weighted criteria — job fit, candidate preference, location match, freshness, company reputation, and stated benefits — combined as a weighted mean, so a number on a card is always explainable back to a reason, never an opaque LLM verdict. Getting this right mattered more than it sounds: a "62" that can't tell you why is not something you can trust enough to let run unattended.

›Step 4 — Job Lookup: Google ADK, Gemini, and a search tool with real restrictions

Discovery uses a Google ADK Agent wired to the google_search tool, grounded on Gemini, with an explicit instruction set: prefer official company career pages, never invent or repair a URL, exclude LinkedIn. Results get cached for hours rather than re-queried on every refresh, both for cost and so repeated cycles are idempotent instead of duplicating work.

›Step 5 — The part that actually turned out to be hard: making search understand intent

This is the step I didn't plan for, and it's the one worth walking through in detail. The first version of query matching was a flat keyword list: split the objective into words, drop stopwords, keep the first four, then gate every candidate posting through a whole-word literal match. Two failures showed up immediately and looked, from the outside, like the same bug: an objective naming "cloud" could never match a posting that only said "GCP" — the fix for that is recall, via synonym expansion — while a posting that merely shared one incidental word could still surface as a false "match" regardless of actual relevance, which is a precision problem, and needs the opposite kind of fix. Fixing recall without also fixing precision just produces more noise, not less.

The rebuild replaced the flat keyword bag with a faceted QueryIntent: roles, skills, locations, seniority, and employment type, each treated as its own kind of term, each carrying its own expansions and whether a term was stated by the user or expanded by the model. Within a facet, terms widen the search — naming both GCP and AWS should return more results, never fewer. Across facets, matches only rank, they never gate, so widening never turns into silent overreach. And every expansion the model proposes gets checked back against what the user actually typed, so a "widened" search can't quietly invent a skill nobody claimed. An offline LiteralIntentExtractor keeps the old literal behavior as a deterministic floor with no model configured, so tests and a fresh clone both stay reproducible; the Gemini-backed extractor sits behind the same interface for the deployed path.

›Step 6 — Orchestrator: event-driven, resumable, checkpointed

Everything runs as named lifecycle events — OpportunityDiscovered, OpportunityScored, OpportunityShortlisted, ApplicationPrepared, and so on — with state checkpointed at every step in the same durable store from Step 2. A background agent that can't survive a restart isn't actually autonomous, it's a script that happens to run unattended until it doesn't; checkpointing is what makes "resume from where it left off" true instead of aspirational.

›Step 7 — Application Tracker: read-only, revocable, and never sending anything

Connecting a mailbox is optional and read-only — it can detect application-status signals in a bounded, owner-consented scan window, classified with a Gemini call, but it cannot send, label, archive, or delete anything, and every status it infers still requires human approval before it changes application state.

›Step 8 — Control API and the UI: a thin surface over real background work

The product surface is a small FastAPI control API plus a React control panel, holding no cloud credential of its own. It exposes the standing objective, the Job Lookup feed, and a JD Analyser board with an explicit Prepare application action gated behind human review — a simple control surface over a real background workflow, deliberately not a polished chatbot pretending to have autonomy it doesn't.

›Step 9 — Deploying it for real, on a hackathon budget

The whole stack — both Cloud Run services, a bounded Cloud Run Job for the worker, Firestore, Pub/Sub, Cloud Scheduler, Secret Manager, and Artifact Registry — is provisioned with Terraform against a remote, locked state backend. Credits, not code, forced some of the more interesting decisions here: scale-to-zero between sessions, search results cached for hours instead of re-queried per refresh, a capped number of search phrases per discovery cycle, and the resource-heavy half of the pipeline running as a job that exits rather than an always-on worker. Those choices turned out to help correctness too, not just cost — caching identical queries is also what keeps repeated runs of the same objective from duplicating work.

›What I deliberately left out

Mock interview preparation is part of the design but isn't built yet in this submission. Calendar-based interview detection, persisted mock-interview scoring history that actually feeds back into future matching, and porting the same provider-neutral core onto an AWS Strands runtime are next — the last one especially, since it's the real test of whether the cloud-neutral design was true or just a diagram.

›Try it yourself

The full spin-up path — clone, run locally with no cloud credentials required for basic discovery, or deploy your own Google Cloud stack with Terraform — is documented end to end in SPIN-UP.md in the repository.

Back to AI

Enjoyed this article?

Share it with others or explore more content

All Blogs