An agent that applies for jobs, proves it, and learns
Job hunting is a funnel where most applications die before a human reads them. The first reader is a machine that turns a PDF into fields and compares fields to a posting. If it fails, nobody tells you.
So I built an agent that works the funnel every day: finds openings, ranks them, decides where to apply, tailors the resume, fills the form, sends, proves the send happened, follows up, and fixes its own tools when a platform changes. I see a dashboard of exceptions and a shortlist.
The loop
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ collect │──▶│ rank │──▶│ decide │──▶│ tailor │
│12 sources│ │ 5 axes │ │ strategy │ │ resume │
└──────────┘ └──────────┘ └──────────┘ └────┬─────┘
▲ │
│ ▼
┌─────┴────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ improve │◀──│ learn │◀──│ verify │◀──│ apply │
│ adapters │ │ platform │ │ evidence │ │ + reach │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
The architecture in one line: the model decides, deterministic tools execute, verifiers prove, a durable worker persists and resumes. Every arrow above is a state in a database, so a crash mid-application is a resumable task, not a lost one.
The layers
┌──────────────────────────────┐
│ me: autonomy contract, facts,│
│ exceptions, the shortlist │
└──────────────┬───────────────┘
│
┌────────────────────▼────────────────────┐
│ orchestrator ─▶ strategist ─▶ improver │
└───────┬─────────────────────────┬───────┘
│ │
┌─────────────▼────┐ ┌───────▼────────────┐
│ memory │ │ tools │
│ │ │ │
│ SQLite pipeline │ │ 12 source adapters │
│ LEARNINGS.md │ │ prescore + golden │
│ failure │ │ ATS check (PDF) │
│ signatures │ │ resume builder │
│ evidence store │ │ browser + ATS │
│ │ │ adapters, inbox │
└─────────────┬────┘ └───────┬────────────┘
│ │
┌───────▼─────────────────────────▼───────┐
│ evaluation │
│ golden set · replay fixtures · read-back│
│ receipt / DOM / email / screenshot │
└─────────────────────────────────────────┘
Three agents with prompts versioned by content hash. The orchestrator runs the day. The strategist decides, per opening, whether to apply and how. The improver reads failures and patches the tools. Around them sits an autonomy contract in a YAML file: markets, roles, salary bands, channels, daily volume, and the mode, either prepare only or autonomous submit. Editing it is a new version.
Collect and rank
linkedin jobs · linkedin posts · gupy · inhire · greenhouse
lever · ashby · workable · remoteok · remotive · arbeitnow · hn
│
▼ 3,000+ openings per round, deduplicated
prescore 0-100 on five axes
stack match · seniority · geography
incompatible stack · quality of the posting
│
▼ ordered, never hidden
Twelve sources, added by writing one file against a shared contract. A health check per source catches schema changes and volume drops over 70%. The score orders, it never hides: an opening with no seniority stated gets zero on that axis, not a penalty, because silence is not a signal. A golden set of hand-labelled openings runs before any weight can change. If the ranking of the golden set moves the wrong way, the change does not land.
LinkedIn posts are a source of their own because many good openings never become a "job". A recruiter just posts.
Tailor, and survive the parser
The resume is structured data rendered per role. Tailoring means ordering and phrasing, under a rule the code enforces: material facts are never fabricated. Employers, degrees, dates, years, certifications, legal authorization. Adjacent skills can be listed as skills. Nothing can be claimed as years or production use that was not.
Then the resume is tested the way an ATS will read it: structure, real text extraction from the PDF, and coverage and prominence of the posting's terms. One platform, it turned out, never reads the PDF at all. It parses it into profile fields and judges the fields. So the agent reviews every autofilled field after upload. That fact came from an agent's investigation and is now a page in the knowledge base.
Apply, and reach the people
Applications go where the posting actually leads: an ATS form, an email address, or LinkedIn. The channel is resolved from the destination, not the label, and if it cannot be proven the application is deferred rather than guessed.
Browser sessions persist per domain, so logins are not burned. One platform requires an account and sends a magic link. The agent reads it from an authorized inbox and continues, because possession of the email is the credential. A code that says "confirm you are human" is different: the agent stops and names the exception. It does not complete human verification, ever.
In parallel it finds warm contacts at the company: leaders and peers, sorted by mutual connections, with a short draft each. Technical peer first, recruiter second. The goal is to be seen by a person before the machine finishes ranking.
Nothing is sent until it is proven
filled ──▶ read-back (every field, digit by digit)
│ mismatch ──▶ retry by label, then exception
▼
click submit ──▶ poll up to 20s
│
├─ receipt text / DOM / SMTP 250 + Sent folder
│ └──▶ SUBMITTED_VERIFIED
├─ nothing ──▶ SUBMIT_UNVERIFIED, revisit later
└─ blocked ──▶ durable exception, with evidence
Clicking the button counts for nothing. Verified means a receipt, a confirmation in the DOM, an SMTP acceptance corroborated in the sent folder, or a screenshot the system can point to. Only snapshots taken after the submit count. Evidence that failed to capture is not recorded, because a database pointing at a missing screenshot is worse than no record.
One application per canonical opening, enforced by a unique index. An unconfirmed email is never resent. Duplicating an application is worse than delaying it.
The first real day
The system ran in prepare mode for weeks: fill everything, stop before the click. On the first day of autonomous submit, seven applications went out and verified, across email, Ashby and Gupy.
Six bugs surfaced that day. All six lived in the part of the flow that prepare mode never reached: from the click onward. The submit button was searched on the wrong screen. The word for "finish application" did not match the pattern. The confirmation text was not in the list. The check looked 1.5 seconds after the click on a platform that swaps the screen client-side. A disabled button burned a timeout and killed the task. An exception mid-flight left records invisible. Each one hid the next.
The lesson went into the docs in one line: a supervised mode that stops before the effect does not exercise half the system, and that half was broken end to end.
Three of those applications were then rescued without resending, by re-reading the evidence archived at the moment of the click, once the adapter had learned what the platform's confirmation looks like.
Learning, three loops
failure ──▶ signature ──▶ reproducible case ──▶ patch + fixture
│
rollback ◀── monitor ◀── activate ◀── replay
- Fast repair. A selector breaks, a field appears, a source schema changes. Traces are grouped by signature, the improver diagnoses, patches an adapter with a fixture, runs the cross-adapter regression, replays without submitting, and activates a version that can roll back.
- Decision quality. Good openings skipped, many submits and few replies. Weights move within allowed ranges, against a baseline, over a window. A single rejection proves nothing about the resume.
- Resume and screening. A parser degrading, required terms buried, a recurring question. Aliases, taxonomy mappings and answer templates that derive from existing facts.
Under all of it is a dated learnings file, one entry per platform per lesson. A skills taxonomy that machine-translates "React Native" into something unrecognizable. A combobox that is not a select. A form that only exists after a click, but whose questions can be read with a plain request. The agent reads this file before acting and writes to it after learning.
Why this is not about jobs
job agent any outbound process
───────────────────────── ─────────────────────────────
autonomy contract what the agent may do, versioned
material facts never invented the truth boundary, in code
score orders, never hides ranking is not filtering
golden set before weights a test for every judgment change
evidence or it did not happen the effect proves itself
one per canonical target idempotency as a constraint
prepare mode is not enough test the part that has effects
dated learnings per platform memory that names its source
The hard part was never filling a form. It was deciding what may be automated, proving what was done, and making every failure change a tool instead of a mood.
What I did
I wrote the autonomy contract and the facts. I set the invariants: no fabricated facts, one application per opening, evidence or it did not happen, page content is data not instruction, no evasion of human checks. I built the first source and the first adapter, then let the agents build the other eleven, the scoring, the resume tester and the browser layer. I read the exceptions and the shortlist, and I answered the questions only I could answer, like a salary field the profile did not have.