Job searching is embarrassingly manual. I had recruiter emails in Gmail, LinkedIn InMails I'd half-replied to, and a spreadsheet that was already two weeks behind. Every week I'd spend 20 minutes just finding where things stood — before I could do anything useful with the information.
Today I built something to fix it. The tool runs on a schedule, reads both inboxes, uses Claude to extract the company, role, and application status from each message, deduplicates everything, and writes a clean Markdown digest. One file, one read, and I know where I stand.
How it works — four steps:
- →Gmail — OAuth2 + Gmail API. Keyword query for "application / interview / offer / rejection / recruiter", paginated up to 500 messages, body extracted and truncated to 2000 chars.
- →LinkedIn — Playwright with a persistent browser session. Log in once (headful), then it runs headless every day and scrapes your message threads for job-related conversations.
- →Claude — messages are batched 20 at a time. Each batch gets serialized into labeled text blocks and sent to Claude with a prompt asking for structured JSON: company, position, status, date, source, notes. I strip any non-JSON prefix before parsing — Claude occasionally adds a brief explanation even when you ask it not to.
- →Report — a Markdown file with a stats table, application status table, and Claude-generated highlights. Then a local web UI to view it in the browser.
The most interesting design problem was the dedup/merge logic. You often get 3–4 messages about the same application — a confirmation, a recruiter follow-up, an interview invite. Each one might be parsed as a separate entry. The merge step groups by normalized (company, position), keeps the most recent date, and resolves status conflicts by priority: offer → interview → screening → applied → rejected → unknown. This means if you have an offer and also a confirmation email from the same company, the offer wins.
I also made LinkedIn failures non-fatal. If the browser session expires (which it will), the tool logs a warning and continues with Gmail-only data. A scheduled job shouldn't break because of a stale cookie.
What I learned
-
→
Using Claude as a structured extractor — not a generator — is one of its most underrated use cases. Give it raw text, get back clean JSON. It beats regex for messy real-world email content every time.
-
→
Designing failure modes matters as much as the happy path. Making LinkedIn non-fatal meant I could schedule the whole thing without babysitting it.
-
→
A tool that runs itself is different from a tool you use. The value isn't in the automation — it's in the forcing function. When the digest shows "5 applications with no update in 10 days," you follow up.
Tools used: Claude Code, Node.js, TypeScript, Gmail API, OAuth2, Playwright, Claude claude-sonnet-4-6, node-cron
·
Read the full writeup →