← Back to Writing

I Built a Job Tracker That Reads My Gmail and LinkedIn

A personal automation tool that reads both inboxes, uses Claude AI to extract application statuses, and drops a clean digest in my terminal every morning.

Mar 2026 · 6 min read · View live demo →

Job searching is embarrassingly scattered. You have recruiter emails in Gmail, LinkedIn InMails, follow-ups you haven't replied to, and a spreadsheet that's two weeks behind. Every week I'd spend 20 minutes just finding where things stood — before I could do anything useful with the information.

So I built a tool to fix it. It runs every morning, reads my Gmail and LinkedIn messages, uses Claude to figure out what's an application update and what stage it's at, and writes a clean Markdown digest to a file. Then I open it, take 90 seconds to scan it, and actually know what's going on.

The problem in one sentence

Job search status lives in two places (Gmail and LinkedIn), in unstructured form, scattered over time — and manually triaging it every day is exactly the kind of repetitive cognitive work that software should handle.

What it does

The tool has four steps:

Non-obvious design choice: LinkedIn failures are non-fatal. If the browser session expires, the tool logs a warning and continues with Gmail-only data. Scheduling shouldn't break because of a stale LinkedIn cookie.

The stack

Node.js + TypeScript tsx (no build step) Gmail API + OAuth2 Playwright Claude (claude-sonnet-4-6) node-cron

I picked tsx over a full TypeScript compile setup because this is a personal tool — I want to run npm run digest and have it just work, without a build step in the way. The tradeoff is slightly slower cold starts, which don't matter here.

The Gmail integration uses standard OAuth2 with a refresh token stored in .credentials.json. One-time interactive setup, then silent refresh on every run. LinkedIn uses a persistent Playwright browser context in .browser-state/ — same idea: log in once, headless from then on.

Using Claude as a parser, not a generator

The most interesting part of the architecture is how Claude fits in. I'm not using it to write anything — I'm using it as a structured extractor. I serialize batches of 20 raw messages and ask it to return a JSON array of { company, position, status, lastActivity, source, notes } objects.

The response parsing strips any non-JSON prefix or suffix before calling JSON.parse — because Claude sometimes adds a brief explanation before the array, even when you ask it not to. Handling that edge case prevents the whole batch from failing.

After extraction, a deduplicateAndMerge function groups entries by normalized (company, position) key, keeps the most recent date, and resolves status conflicts by priority: offer > interview > screening > applied > rejected > unknown. This matters because you often get multiple messages about the same application.

The output

A daily Markdown file that looks like this — a stats table, a status table with emoji indicators, and Claude-generated highlights:

See a real sample report

Mock data with 10 applications, 2 interviews, 1 offer — rendered as a live page.

View demo →

What I'd do differently

Right now the LinkedIn scraper is brittle — it depends on CSS selectors that LinkedIn changes without warning. The right fix is probably to use the LinkedIn API (requires a partnership application) or move to email-only and ask recruiters to always follow up via email. For a personal tool, the current approach is good enough.

I'd also add a persistent SQLite store so the tool can detect changes in status over time, rather than re-extracting everything from scratch on each run. That's the next version.

Running it

Setup is three commands after you've put your API keys in .env:

There's also npm run start for a cron daemon that runs on a schedule (default: 7am daily), and npm run web for a local web UI that shows the latest digest and has a "Run Digest" button.

The actual value

Honestly, the value isn't the automation. It's the forcing function. When the digest runs and shows you "5 applications with no updates in 10+ days," you follow up. When you see "offer deadline: Mar 3" in the highlights, you don't miss it. The tool makes your job search legible — and legible things get acted on.

This is something a spreadsheet can technically do. But a spreadsheet requires you to maintain it. This one runs itself.

See it in action → Check out the live demo page with mock data showing what a real digest looks like.