← Projects

/projects/job-application-tracker/

Privacy-Conscious Job Application Tracker

Application updates arrive in inconsistent emails, but giving an LLM direct control of a tracker creates a new class of mistakes.

  • Python 3.13
  • Gemini / Vertex AI
  • Gmail API
  • Google Sheets API
  • Firestore
  • Cloud Run Jobs
  • Pydantic
  • RapidFuzz
  • Docker
  • pytest

Overview

A privacy-conscious automation that keeps a job-application spreadsheet synchronized with lifecycle emails while preserving a strict boundary between probabilistic interpretation and persistent writes.

Input

  • Read-only Gmail messages

Processing

  • Gemini extracts a typed ApplicationEvent
  • Deterministic matching and decision engine

Output

  • Validated Google Sheets update
  • Firestore review queue for ambiguous cases

Problem

Confirmations, assessments, interviews, rejections, and offers all arrive in different formats, so manually reconciling them with a tracker is repetitive and easy to neglect. Letting an LLM choose spreadsheet rows or write statuses directly would replace that inconvenience with a less visible risk: a plausible but incorrect update.

Approach

The system gives Gemini one constrained responsibility: convert untrusted email text into a typed ApplicationEvent. A separate decision engine normalizes company and role names, performs controlled matching, and chooses create, update, review, or ignore. Missing identifiers, multiple matches, low confidence, and unsupported status transitions fail closed into review instead of being guessed.

Technical Implementation

Built in Python 3.13 with Pydantic schemas, RapidFuzz matching, the Gmail and Google Sheets APIs, Gemini through Vertex AI, and Firestore for processed-message state and the review queue. Gmail access is read-only, Sheets read and write credentials are separated, and production runs as a scheduled Dockerized Cloud Run Job with structured operational logging.

Evaluation & Results

The pytest suite exercises decision logic, matching, extraction configuration, orchestration, processed-message persistence, review-queue behavior, write intents, and configuration validation. The central invariant is behavioral rather than a model score: malformed, uncertain, or ambiguously matched input must not silently become a spreadsheet write.

What I Learned

LLMs are useful at interpreting varied language, but interpretation and mutation have different risk profiles and should not share an authority boundary. Running as an ephemeral job also made durable deduplication and retry state part of the core design rather than an operational afterthought.

View repository on GitHub