Skip to content

Gmail Job Tracker · Case study

Gmail Job Tracker

Job hunting turns your inbox into a filing problem. This app signs into Gmail, finds every application email, works out the company, role and stage, and puts each one on a board. You drag a card when it's wrong, and it never overrides you again.

Design EngineeringAI ProductAgent OrchestrationFull-stack
Role
Product design · Design engineering · Agent orchestration
Type
Full stack product
Timeline
1 day · September 2026
Stack
Next.js 16 · Gmail API · Claude Haiku · SQLite · Motion
The Interviewing column: three frosted-glass cards for Parallax, Bluepeak Analytics and Acme Corp under a yellow status dot and a count.
One application card: Acme Corp, Design Technologist, 2 emails.

The problem

Every application ends up as six emails from four senders.

Confirmations come from Greenhouse. Interview invites come from a recruiter. Rejections come from no-reply. Nothing in the inbox says which company or which stage.

“Thank you for applying to Acme Corp!”

From no-reply@greenhouse.io. Read as Applied · Acme Corp · Design Technologist.

“Let's schedule an interview - Acme Corp”

From a recruiter. Read as Interviewing · Acme Corp. Same card, moved over.

“Regarding your Design Technologist application at Brightwater”

From a person. Read as Rejected · Brightwater. The subject never says so.

The board

One card per company. Four columns. Nothing to file.

Cards sort themselves by the latest email. The count on each column is the only number on the page.

Cards lift on hover. Colour appears only on the status dot and pill.

Keeping up

New mail lands on the board by itself.

The app checks Gmail every two minutes, or when you press Sync now. A new confirmation becomes a new card. An interview invite moves a card over.

Sync now: Sundial arrives, then Halcyon moves to Interviewing.

Reading the email

A small model reads each email once and fills in a form.

Claude Haiku gets the subject, sender, snippet and the first 2,000 characters of the body. It must answer through one tool call with a fixed schema. The prompt’s hardest rule: the sender is usually an applicant-tracking system, so the company is the one named in the email, never the domain.

How the classifier reads an emailAn email (subject, sender, snippet, and the first 2,000 characters of the body) goes to Claude Haiku, which answers with a fixed-schema tool call: is_job_related, company, role, status and confidence. That becomes one card per company. If there is no API key or the model is down, a keyword heuristic produces the same answer.An emailSubject, from, snippetBody, first 2,000 charactersClaude HaikuOne tool call:record_classificationCompany is the onenamed in the email,not the sender.KeywordheuristicNo API key,or modeldownFixed schemais_job_related · companyrole · statusconfidenceOne card per companySorted by the latest email

Applied

Confirms an application was submitted or received.

Interviewing

Invites you to schedule or confirms an interview, phone screen or call.

Offer

Extends a job offer.

Rejected

Says you weren't selected, or the company is moving forward with others.

Correcting it

Drag it. Edit it. It stays.

The model gets things wrong. So every card is editable, and once you touch one, sync never overwrites your version.

Drag between columns.
Open a card, fix the role, see it save.
Not job-related? One click hides it. The email is remembered, so it never comes back.

What's stored

The board never keeps your email.

The database holds enough to draw a card and link back to Gmail.

Stored

Subject and sender

Received date

Gmail's own snippet, 200 characters

What the classifier decided

A link to the message in Gmail

Never stored

Email bodies

Read once, in memory, for classification.

The data-model contract from PROJECT.md, rendered as a frosted card: the SQL that defines the tables.
The data-model contract from PROJECT.md.

The design system

Calm glass, and colour only where it means something.

The canvas is a pale blue with blurred orbs. Cards and panels are frosted glass. The only real colour is status: yellow for interviewing, green for offer, red for rejected. It lives on the column dot and the pills, never on the card. Cards keep their identity as they move between columns. Every animation respects reduced motion.

The full board: a pale blue canvas with soft blurred orbs, four frosted-glass columns of application cards, and a glass header bar.
Canvas, orbs, glass. Status colour only on the dots and pills.
The header bar: an Up to date toast, Synced now, a Show ignored toggle, and the Sync now and Sign out buttons.
The header.
The Interviewing column with a yellow status dot and three cards: Parallax, Bluepeak Analytics and Acme Corp.
A column.
The side sheet for Acme Corp: editable company, role, status and notes, and two emails with Open in Gmail links.
The detail sheet.

Canvas and orbs

Glass: fill, hairline, top highlight

Status tones: neutral, yellow, green, red

Radii: 10 · 14 · 20 · 28 · pill

Springs: calm and soft, from one file

Reduced motion, in CSS and in code

How I built it

I designed the process, then ran it as a network of sessions.

I wrote the brief and the constraints. A planner turned them into a contract: stack, data model, API, and which files each coder owns. Two coders built the backend and the frontend in parallel, without seeing each other’s code. An integration session ran both halves together and fixed the seams with Playwright. A reviewer did a security pass.

The redesign used the same shape: a design doc, one session for primitives, two for screens, a screenshot review. Everything shipped the same day.

The session network behind the build and the redesignBuild: my brief goes to a planner on Fable that writes the PROJECT.md contract; a backend session and a frontend session, both on Sonnet, build in parallel; a Sonnet integration session runs a Playwright smoke test; an Opus reviewer does a security pass. Redesign: a DESIGN.md doc with tokens and component contracts goes to a primitives session; a board session and a detail sheet session work in parallel; a screenshot review follows, and it ships.BUILDBriefGoal and constraintsMePlannerWrites the contract, PROJECT.mdFableBackendData, API, mockSonnetFrontendBoard, componentsSonnetIntegrationPlaywright smoke test, READMESonnetSecurity reviewAuth, input checksOpusREDESIGNDesign docDESIGN.md: tokens, components, motionPrimitivesTokens, glass, motion, one sessionBoard sessionColumns and cardsDetail sheetPanel, sign-inScreenshotreview, then fixesShippedSame day

Two contracts

Stack decided up front

Data model as SQL, frozen

API routes and shapes

File ownership per session

Tokens once, in one file

Component props as a table

What I'd do next

Three things, in order.

Show the model's confidence

The classifier already returns it. Low-confidence cards should ask for a check.

Gmail push

Replace the two-minute poll with a Pub/Sub subscription once it's more than one user.

Watch someone else use it

It's built around my inbox. Other people's mail will break assumptions I can't see.