Read the story

Architecture

Finds fresh jobs, tailors each resume.1,008 runs, June to October.

A Cloud Run job pulls fresh postings from 163 company boards and four aggregators, drops the roles I could never take, and has Gemini score the rest into a Google Sheet. A private console reads that Sheet, each tailored resume passes a judge before it reaches Drive, and the public demo runs the same console on fictional data.

40 min

from an empty Google Cloud project to a running pipeline and console, 3:14 to 3:54 AM on June 10

0

outreach emails it sends on its own. Every one waits in Gmail drafts for my click

5,093 jobs

tracked in the Sheet by July 23, with 22 to 58 fresh rows added each day

1,008 runs

of the pipeline, June 10 to October 8: every 30 minutes at first, then twice each morning

New York --:-- simulated

A demo tailor
JobPilot A Cloud Run job pulls fresh postings from 163 company boards and four aggregators, drops the roles I could never take, and has Gemini score the rest into a Google Sheet. A private console reads that Sheet, each tailored resume passes a judge before it reaches Drive, and the public demo runs the same console on fictional data. Resume judge ATS replica, best of 10 Vertex Gemini JSON schema contract Visitors 3 tries a day each Public demo Pages, fictional data Tailor worker Workers AI, Turnstile Job boards 163 boards, 4 aggregators jobpilot job Cloud Run, Python 3.12 Google Sheet database and dashboard Console Next.js 16, behind IAP Me decides, clicks Apply Cloud Scheduler fast and full runs Gmail and Drive drafts, PDFs, alerts jobs scored rows rows sign in score, rewrite issues run as me digest, alerts try it JD, token

Hover a part to trace what it touches. Click it for its notes.

  • Built by me
  • Managed by Google
  • Outside system
  • Main data path
  • On demand
  • Credentials cross

When it breaks

What each failure does, and what it does not do.

  1. A job source goes down

    Each source runs in its own try block, so one dead API never kills a run. Its error shows in the digest's run notes.

    src/jobpilot/pipeline.py:56

  2. Gemini returns bad JSON

    The batch gets one retry, then stays unscored and out of the Sheet, so the next run picks it up.

    src/jobpilot/scorer.py:97

  3. A posting has no description

    Tailoring fetches the live posting page first. Under 200 characters it refuses and says why on the row, with a retry button.

    src/jobpilot/tailor.py:140

What I tuned

Small choices that keep it fast and cheap.

  1. 2 GiB for 163 boards

    The parallel board fetch ran out of memory at the 512 MiB default, so the job runs with 2 GiB.

    .github/workflows/deploy.yml

  2. Dedup on company and title

    Location left the key after boards listed one role per city, 23 duplicate groups in 426 rows. When two sources carry a job, the higher fidelity one wins.

    src/jobpilot/dedup.py:30

  3. Fresh first

    Company boards keep 14 days, aggregators 7, and undated postings are dropped. A July audit found 2,960 New rows older than 30 days in the Sheet.

    src/jobpilot/config.py:65

Guards

Rules the code enforces, not rules people remember.

  1. Drafts only

    Outreach is created as Gmail drafts and nothing else. No published recipient email means no draft at all.

    src/jobpilot/outreach.py:135

  2. The tailor cannot invent

    The prompt allows reordering, rephrasing and emphasis, never a new employer, title, date, metric or tool. Length stays within 5 percent of the master so it fits one page.

    src/jobpilot/prompts/tailor_v1.txt

  3. No personal data in git

    My profile and resumes load from Secret Manager at run time. The public repo ships a Jane Doe template.

    src/jobpilot/config.py:128