Career Platform: from job discovery to recruiter follow-up

September 20, 2026

View code

I built Career Platform to find roles worth applying to and keep the rest of the search organized. A shortlist is useful, but it is only the beginning. After applying, I need to remember which version of my resume I sent, recognize the recruiter conversation that belongs to that role, and know what needs a response.

The platform keeps that context in one application record. A browser extension observes submissions, Outlook supplies correspondence, and Hermes can read the history through a custom MCP server to prepare replies and suggest interview times. I use the dashboard to inspect the evidence and review proposed actions.

Thirty seconds across discovery, application records, recruiter correspondence, and follow-up.
Read the walkthrough
The video opens on a saved shortlist with role descriptions and selection explanations. A job preview opens inside the dashboard. The next cut shows applications at several stages, followed by a record with submission evidence and history. In Review, a recruiter message is associated with its application. The linked correspondence is visible in Messages. A contextual reply is reviewed before an Outlook draft is created. The final view returns to the application record with its updated interview stage and history.

Two ways to build a shortlist

I started with Matt Herzog's job-boards tooling, which collects public postings from Ashby, Greenhouse, and Lever. My work began with turning that feed into a personal shortlist. Titles alone were an incomplete signal: similar engineering work appeared under different titles, while a familiar title could describe work I wasn't interested in.

The dashboard supports two sources. Model picks use my trained ranking policies. Codex picks are lists selected by an agent and published into the platform with an explanation for each choice. The application preserves the agent's ordering and keeps previous lists available, so a new day's selections do not erase yesterday's research.

Posting dates, source updates, company, and location are visible before opening a role. The shared preview contains the saved description and source link. Reading a role does not create an application or mark it submitted.

Saved daily shortlist with relevance explanations, company locations, and posting dates
Saved selections retain their order and explanations. Model picks are a separate source in the same view.

Teaching the ranking models

I built an LLM teacher that reads a job description alongside a profile of my experience and preferences. I began with local inference and moved labeling to Fireworks. The teacher labeled 2,000 roles, giving me a training set for smaller scikit-learn models that can score new postings without another full LLM assessment of every description.

Defining the labels took substantial work. I separated hands-on software relevance, interest in the work, and whether my experience makes an application plausible. The teacher scores each dimension from zero to four and supplies short quotes from the description to support its assessment. Qualification is deliberately permissive about adjacent skills and stretch roles; location and compensation remain separate constraints.

Each assessment produces two training labels. Selective favors interesting roles that look credible for my experience. Broad keeps a wider set of software roles for exploration. An ordinary backend role with a substantial experience gap can pass the broad policy without passing selective. I can change the search's breadth without relabeling the same descriptions.

The retained selective model uses semantic embeddings with logistic regression. The broad model uses word and character TF-IDF features with logistic regression. I grouped related descriptions during cross-validation so near copies would not appear in both training and validation. Agreement with the teacher measures whether the smaller models reproduce its judgments; it does not measure interview success.

Keeping the record after I apply

The browser extension recognizes supported job URLs against the catalog and records submission evidence. A click on Submit is an attempt. A confirmation page is stronger evidence, and a matching confirmation email can corroborate it. Keeping those observations distinct matters when an application form has another verification step or a request fails.

The application list shows where each application stands and which records need attention. Interviewing and offer stages take precedence over earlier submission evidence. A later confirmation email should not make an interviewing application look newly submitted again.

Application list showing submitted, interviewing, and offer stages
Each application has its own stage, dates, and pending decisions. Review holds the decisions that still need attention.

Opening a record brings together its history, messages, and documents. The document is the resume recorded for that submission, rather than whichever file is currently preferred. Posting history is separate: an employer can edit or close the listing while the application is still active.

The workspace also has to stay coherent when requests overlap. Request guards prevent a slow response for a previous application from replacing the one I am reading. The same records adapt to narrower screens, with list filters and navigation preserved when I go back.

Application timeline with recruiter contact and submission evidence
The record preserves what happened and the evidence behind it. Employer-side posting changes have their own history.

Giving the agent enough context

Recruiter emails do not always identify a job unambiguously. The system considers submitted applications when proposing an association and keeps uncertain matches in Review. I can inspect the message and candidate application before confirming the update. The accepted correspondence then appears in the record alongside the history it supports.

Review queue containing recruiter evidence and proposed application updates
Review connects a proposed interview update to its recruiter message and application before I confirm it.

Hermes reads these same records through bounded MCP tools. I can ask it through Telegram to summarize an application and prepare a response without pasting the recruiter thread, my resume, and the application history into another conversation.

Application history lives in an append-only SQLite ledger, independent of the agent's chat. The dashboard, extension, CLI, and MCP server use shared application services. That gives the agent a consistent account of the application, including the submitted resume and sanitized archived correspondence.

A reply proposal is attached to its application and reviewed in the unified queue. Approval applies to the exact content shown; changing it requires another approval. A worker creates the Outlook draft, and I send it myself. Interview scheduling uses the same separation: proposed times and private tentative calendar holds can be reviewed without sending an invitation to a recruiter.

Proposed recruiter reply with its application context and exact-action approval controls
The proposed text and its context are visible before approval. Execution creates an unsent Outlook draft.

Keeping the search moving

Collection, ranking, and mail processing have different workloads. I separated core and model workers so a long ranking run does not block routine automation. Durable work records, leases, and heartbeats allow interrupted jobs to recover. Ranking saves batch progress and reuses unchanged work; a full refresh is marked complete only after both policies have finished against the same catalog snapshot.

An external timeout needs more care than a local retry. An Outlook request might have succeeded before the connection failed, so immediately repeating it could create another draft. The platform keeps uncertain outcomes available for reconciliation. Replaying a completed action in the isolated demo still creates only one draft.

The operations view makes collection freshness and background work inspectable. An unavailable integration does not erase the application history or the saved evidence already attached to it.

Operations activity counts showing applications, pending reviews, notifications, and scheduled jobs
Activity counts show what is recorded and what still needs attention. Live external integrations remain disabled in this isolated scenario.

Running it beyond my laptop

I deployed the platform on AWS using Terraform and Docker Compose. GitHub Actions produces releases, and Systems Manager handles deployment. Persistent state lives on a separate encrypted volume; Tailscale provides private dashboard access. Remote inference keeps model execution outside the CPU services.

A single EC2 host fits the workload and keeps SQLite coordination local. Backups, pinned image digests, and schema-aware rollback support recovery. Moving to multiple hosts would require revisiting both database coordination and artifact storage.