case study
JobFlow: an application agent that has to be trusted with my name
I built JobFlow, an agent that screens LinkedIn postings against my own rules and fills the Easy Apply form — designed so it can never quietly speak for me.
Context
A personal project, built and run end to end. JobFlow searches LinkedIn for the titles I care about, screens each posting against rules I wrote, fills the Easy Apply form, and records why it applied or skipped. I was both the engineer and the person whose name goes on every application it sends — which turned out to be the constraint that shaped the whole design.
Problem
Applying to jobs at volume is repetitive work that rewards persistence rather than judgement: the same details typed into the same forms, hundreds of times. But the naive version of this tool is genuinely dangerous. An agent filling forms in my name can send a wrong salary figure, answer a screening question incorrectly, or apply to something I would have rejected on sight — and every one of those reaches a real employer, under my name, with no way to recall it. The interesting problem was not making it apply. It was making it safe to let apply.
Approach
I made the rules layer answer first and the model answer last. Screening questions — notice period, salary, work authorisation, willingness to relocate — are resolved deterministically from a profile file I control, matched by phrase. Only a question that no rule covers ever reaches a model. That ordering means the answers that matter most are the ones a model never touches, and it also makes the common case free and instant.
I built two application modes and made the cautious one the default path in the documentation. In review mode the agent fills the entire form and then stops, leaving it on screen for me to submit or discard — and it never guesses in this mode, because when a human is present to be asked, it asks. Live mode is the one that acts alone, and it exists behind an explicit flag plus a typed confirmation.
I made every inferred answer visible rather than silent. A guess is logged as a guess; a model-written answer is logged separately as 'AI answered'. Two files record the full decision trail — one row per application, and the complete reasoning per evaluation — so any application can be traced back to the exact rule or inference that produced it. An agent acting in my name that cannot explain itself afterwards is not something I would run.
I bounded what the model is allowed to return. It is instructed to refuse rather than invent, and any answer that is too long, or that names an option not present on the form, is discarded rather than submitted. The model can decline; it cannot improvise something onto a real employer's form.
I made the escape hatches work under automation. A floating quit control finishes the application in progress and then stops, rather than abandoning a half-filled form — a partially submitted application is worse than no application. The daily cap is configurable and the docs tell you to set it to five for the first live run, because five is enough to catch a mistake and small enough that the mistake is cheap.
Trade-offs
I made the safe mode slower on purpose. Review mode requires me to sit and approve each application, which throws away most of the throughput the tool exists to provide. I still made it the recommended path, because the failure it prevents — a wrong answer sent under my name to an employer I actually want — costs far more than the time it saves. A tool that applies faster than I can check is only valuable while it is right.
I let live mode guess rather than stall. When no human is watching, an unanswerable question gets a plausible answer and the application continues, instead of the whole run halting on one field. That is a real concession: it means some applications carry an answer I did not write. I bought it back by logging every instance under a word you can search for, so the guesses are reviewable after the fact even though they were not approvable before it.
I read the resume as plain text and documented what that loses. Pulling text out of a PDF destroys its layout, so columns and tables run together, and a scanned resume yields nothing at all. I could have added a document-parsing pipeline; instead I wrote down the limitation, made it fall back to the profile summary, and told the reader to check their PDF has real text. The failure is visible in the docs rather than hidden in the output.
I named the terms-of-service risk in the README instead of burying it. Automated applications can conflict with LinkedIn's terms and accounts do get restricted. It is not my decision to make on someone else's behalf, so the tool states it plainly and leaves it with the person running it.
Outcome
262 tests run against a fake browser and a stubbed model client, so the full suite passes with no internet connection, no API key and no LinkedIn account — the design that keeps the model out of the deterministic paths is what makes that possible.
Every decision is reconstructable after the fact: one row per application plus the full per-evaluation reasoning, both stored locally.
The site-specific selectors are isolated to the top of a single adapter file, so a LinkedIn redesign — the most likely way this breaks — is a known, contained repair rather than a rewrite.
It runs for me. I use the review mode, which is the honest measure of whether I trusted my own guardrails.