The short version, for whichever hat you're wearing
- One command, nothing manual: I type
jobhunt run, and an agent finds, filters, applies, tracks and reaches out, end to end. - Never the same job twice: three small SQLite stores remember what I've seen, what I've applied to, and what today's run already found.
- It has its own inbox: a separate email address for applying and tracking, so it can read confirmations and replies without ever touching my personal mail.
- It plays fair: answers only from my profile, caps itself so no employer gets flooded, and simply skips Workday marathons and CAPTCHAs.
- Dedupe on two keys: the source's own job ID, and a fingerprint of company, title and location, because one job appears on five sites with five IDs.
- Known forms are filled by rules, unknown ones by computer use: the model only sees what rules can't handle, and every submit is preceded by a screenshot and followed by a check.
- An intent row before every submit: a crash can't turn into a duplicate application.
- Adapters per source type: official ATS feeds first (exact posting times, allowed to read), filtered search links second, career pages last.
- Local by design: one process started from a terminal, SQLite instead of a database server, and my own browser instead of cloud browsers.
- Secrets never in
.env: API keys there, website sessions in a dedicated browser profile, any password in the OS keychain.
- The goal is fewer, better, earlier applications, not more of them: a fit threshold, 10 a day at most, and no more than 2 roles per company a month.
- Terms of service are a design input: public feeds first; the agent uses LinkedIn and Google only at a human pace, in my own account, and never bypasses a CAPTCHA.
- About ten dollars a month in model calls; everything else runs free on the laptop.
It started with wanting to be first
Applying in the first hours after a role opens is widely reported to raise your odds of hearing back several times over (see the further reading). I was checking a dozen sites a day and still finding roles two days late. So I built a site to find them fast, and then, piece by piece, let AI take over everything I was still doing by hand.
What I actually use
It's a local app. I start it from my terminal, and a small page on localhost shows what it did. Nothing is hosted, and there's no scheduler: it runs when I tell it to.
Every flow, from setup to "delete everything"
Every job moves through one small state machine. Pick a flow to see who talks to whom, and where the job ends up.
jobhunt setup profile, résumés, criteria, the jobs inbox, then sign in to sites once
jobhunt run the whole thing: find, filter, apply, track, reach out
jobhunt watch keeps re-checking favourite companies hourly while the terminal is open
jobhunt search "backend" --since 1h search right now, nothing is applied
jobhunt review approve, edit or skip what the agent filled in
jobhunt open the local app on http://localhost:8765
jobhunt mode notify|review|auto how far the agent may go
jobhunt pause 7d stop applying; still finds and records
jobhunt doctor sources, sessions, keys and disk, checked
jobhunt export | jobhunt wipe everything as JSON + CSV, or delete it allOne run, end to end
No servers, no queues and no scheduler: one process that I start, with each stage writing to SQLite before the next one begins, so a run that stops halfway picks up where it left off. Press play to follow one morning.
Where new roles come from
"Search 90+ boards" sounds like one problem. It's five, because each kind of source says different things about when a job was posted, and has different rules about who may read it. I don't do any of it by hand: public feeds are read directly, and everything else the agent does in my own browser, the way I used to.
# ATS feeds: read directly, exact timestamps
GET https://boards-api.greenhouse.io/v1/boards/{company}/jobs # first_published, updated_at
GET https://api.lever.co/v0/postings/{company}?mode=json # createdAt (ms)
GET https://api.ashbyhq.com/posting-api/job-board/{company} # publishedAt
# Job boards: the agent opens these in my browser and reads the results
linkedin.com/jobs/search/?keywords=backend&f_TPR=r3600 # r3600 = past hour, r86400 = day
indeed.com/jobs?q=backend&fromage=1 # days
# Search engines: the agent runs these to find companies hiring on each ATS
"backend engineer" site:jobs.ashbyhq.com + past 24 hours
"platform engineer" site:boards.greenhouse.io + past 24 hours- Known pages are scripted, not reasoned about. A LinkedIn or Google results page has a stable layout, so Playwright loads the filtered URL and reads the results straight from the page. A model only steps in when a page looks different from what the script expects.
- Search engines grow the company list. Every
site:result on Greenhouse, Lever or Ashby reveals a company and its feed. New ones go intocompanies.yaml, so tomorrow that company is read directly, with exact times. - Human pace, on purpose. One page at a time, pauses between them, a few dozen pages a day, all in my own signed-in browser. LinkedIn's and Google's terms prohibit automated access, so this is a risk I take with my own accounts, and the reason it stays slow and small.
Searching like I mean it
My first tool, jobsfinder.streamlit.app, is still the heart of discovery. It turns one job title into search links for 90+ platforms in 12 groups (major ATS, payroll ATS, startup boards, smaller ATS, major boards, remote boards, tech boards and more), with presets like Top 10, All ATS, Remote Only and Tech Boards, filters for time posted, work type and job type, and fields for keywords, exclusions and companies. Today the agent opens those links instead of me. Build one yourself:
- Exact titles in quotes, variants with OR:
("backend engineer" OR "platform engineer" OR "software engineer II"). Without quotes, Google happily matches "backend" and "engineer" in different sentences. intitle:for the role, and only the role: titles usually say just "Software Engineer II". The focus (backend, full stack, frontend, data engineering) and the location are usually in the description. So the role family goes inintitle:, which stops a recruiter post that merely mentions "our engineers", while focus and place are matched anywhere on the page.site:per ATS, or per company:site:jobs.ashbyhq.comcovers every company on Ashby;site:jobs.ashbyhq.com/acmecovers one. Greenhouse, Lever, Workable, SmartRecruiters, Wellfound and the rest each get their own line.- Companies on their own domains:
inurl:careersandinurl:jobscatch careers pages and jobs. subdomains, andsite:.jobscatches the.jobstop-level domain some employers use. - Minus what you don't want:
-intern -"staff engineer" -contract -clearance, and companies I've ruled out. - Time in the URL:
&tbs=qdr:h(past hour),qdr:d,qdr:w, orafter:2026-11-01for a custom start date. It's the date Google indexed the page, which trails the real posting time, so it finds companies; their feeds give the exact time. - LinkedIn and Indeed take filters in the URL too: on LinkedIn,
geoId=103644278(United States),f_TPR=r3600(past hour),f_WT=2(remote),f_E=4(mid-senior),f_JT=F(full-time),sortBy=DD(newest first). On Indeed,fromage=1and the country's own site (ca.indeed.comfor Canada).
- Spread, don't hammer. Each run takes a different slice of the searches, and each search's time window matches how long since it last ran: visit Ashby's search every three days, and that visit asks for the past three days. Nothing is missed, and no site sees more than a person's worth of browsing.
- A budget per site per day (a few dozen LinkedIn pages, a few dozen Google searches), random pauses between pages, and nothing in parallel against one site.
- Feeds carry the load. Once a company's ATS feed is known, it's read directly, so the browser searches only have to discover companies, not re-find their jobs every day.
- When a site pushes back, the agent backs off. If Google shows its "unusual traffic" check, Google is done for the day and the rest of the rotation carries on.
How fast I see a new role
I start a run every morning, so on average a role waits half a day before I see it. For favourite companies, jobhunt watch keeps going in a terminal tab and re-reads their feeds every hour: same code, a smaller list, no model calls unless something is new.
Never the same job twice
Three small SQLite files, each answering one question. Splitting them keeps each one tiny, and means I can delete today's scratch data without touching my history.
-- seen.db: tiny, permanent
CREATE TABLE seen (source_key TEXT PRIMARY KEY, -- 'greenhouse:acme:4012345'
fingerprint TEXT NOT NULL, -- sha1(company|title|city), normalised
first_seen TEXT NOT NULL);
CREATE INDEX seen_fp ON seen(fingerprint);
-- applications.db: my record
CREATE TABLE applications (id INTEGER PRIMARY KEY, source_key TEXT UNIQUE, fingerprint TEXT,
company TEXT, title TEXT, url TEXT, resume TEXT, mode TEXT, -- notify | review | auto
status TEXT, -- queued | applying | needs_you | applied | failed | skipped | interview | offer | rejected
answers_json TEXT, screenshot TEXT, applied_at TEXT, notes TEXT);
-- run_cache.db: scratch for one run, kept 7 days
CREATE TABLE found (run_id TEXT, source_key TEXT, fingerprint TEXT, raw_json TEXT,
PRIMARY KEY (run_id, source_key));All three run in WAL mode, and a lock file guarantees one runner at a time, so SQLite never sees two writers. The local app only reads.
Deciding what's worth applying to
Cheap checks first, the model last. Rules cost nothing and throw out most roles; the model only reads what's left, and every score is cached by a hash of résumé and job description, so nothing is scored twice.
- Normalise first: title words (Sr → Senior, SDE II → Software Engineer II), location to a city or "remote", and the level to a number.
- Rules: title must match my families, the level my band, the location my list. My blocklist of companies and keywords applies here.
- Score: the model gets my résumé and the description, and returns a 0–1 fit with one sentence of reasons and the résumé to use. Under 0.75, it's only listed in the email.
- If the model is down, a keyword overlap score is used and everything goes to review instead of auto.
Is this role really for me?
Keyword search is generous. A role in Tokyo shows up because its description says "work with US clients and data". A recruiter role shows up because it says "hire backend engineers". But the description can't be ignored either: a title that only says "Software Engineer" hides whether it's backend, full stack or frontend, and some roles state their location only in the text. So search results are candidates, and each one is read in order: fields first, then the parts of the description that actually say what the job is and where.
- Location: fields first, then location sentences. Feeds give a location for most jobs (Greenhouse's
location.name, Lever's location category, Ashby's location and address), and a small lookup turns it into a country and a remote scope. When the field is empty or just says "Remote", a small model reads the description for sentences that state where the job is ("this role is based in Austin", "US-based remote", "must reside in Canada"). A sentence that only mentions a country ("our US clients") is not a location statement. Anything still unclear is dropped, not guessed. - Role family from title and department. The title must be in my engineering families, and the department too when the feed has one. "Technical Recruiter", "Sales Engineer" and "Engineering Manager, Payroll Ops" all contain the right words and all get dropped.
- Focus from the description. Titles hide it, so the small model reads the responsibilities and requirements (not the company boilerplate) and labels the focus: backend, full stack, frontend, data engineering, ML, mobile. My profile says which I want (backend, full stack, data engineering) and which I skip (frontend-only, mobile). Mixed roles keep their mix, and the fit score weighs it.
- Hard lines are a veto. "Must be located in Japan", "active clearance required" or "no sponsorship" end it, wherever else the role scores well.
- Every drop keeps its reason, so a wrong filter shows up in the run summary as a pattern, not a mystery.
How the agent applies
Yes, it uses computer use, but only where it has to. For the big ATS forms I've seen before, a mapping fills each field directly: fast, free and predictable. When a form is new or changes, a computer-use model looks at a screenshot and the page's accessibility tree and decides the next click or keystroke. Both run in Playwright, in a browser profile on my laptop that is signed in as me.
name: Ambrose Tuscano
email: ${JOBS_INBOX} # the agent's own address
work_authorization: { US: authorized }
over_18: yes # eligibility, not a birth date
notice_period: 2 weeks
resumes: { backend: resumes/backend.pdf, ml: resumes/ml-platform.pdf }
facts:
years_python: 6
willing_to_relocate: [Seattle, Vancouver, remote]
heard_about_us: Web search # always the honest answer: the agent found it
voluntary_disclosures: decline # gender, race, veteran, disability: "prefer not to say"
never_type: [date_of_birth, ssn, bank_details, passport_number] # skip and flag the job- Intent before action. The row is written as
applyingbefore the browser touches the form. If anything dies, the next run sees the intent and checks the site and my inbox before it ever tries again. - Known forms by rules. Greenhouse, Lever and Ashby forms have stable field names; a small mapping fills them in under ten seconds with no model call.
- Unknown forms by computer use. The model gets the screenshot, the accessibility tree and my profile, proposes one action at a time, and every action is logged. It may only click inside the application page.
- Free-text questions ("why this company?", "what are you proud of?") are written from my story bank and the job, as the next section shows.
- Proof: a screenshot before submit, the confirmation page after, and the confirmation email matched by company. No confirmation within an hour means the tracker says "unverified", not "applied".
Where its answers come from, and how it learns
Every form asks something new: "Are you 18 or older?", "Are you open to an on-call rotation?", "What are you most proud of?". The agent never invents an answer. It draws from three things I wrote once, remembers every question it has met, and asks me only when it meets something it truly can't answer. Pick a question to see what happens:
- Three sources, all written by me.
profile.yamlholds facts (work authorization, notice period, eligibility). My résumés hold the history.stories.yamlholds a handful of stories with real numbers, the raw material for every free-text answer. - An answer bank remembers every question. Each question it meets is stored with the answer it gave. A new wording ("Do you have the right to work in the United States?", "Where did you find this role?") is matched to a saved one by meaning: a small model compares them, only a confident match reuses the answer, and the new wording is saved too, so paraphrases pile up as aliases of one answer.
- It never waits on me. A question with no saved answer doesn't stop the run. The agent saves that form exactly where it is, parks the application in a "waiting on you" queue, and moves straight on to the next role. Ten roles can be parked on the same question; they all wait on one answer.
- It asks me at the end, once. When the run finishes, the parked questions are on my dashboard, each with the roles waiting on it. Most are a tap (yes or no); some need a line of text. The moment I answer, it's saved as a fact (with every wording it was asked in), and the agent reopens each parked form and submits it, without waiting for the next run. The question never comes back, however it's worded.
- Free-text answers are written, not copied. The best model gets the job description, the company's own about or engineering page, and the one or two stories from my bank that fit the role. It writes in plain first person, within the field's limit, and every claim must trace to a story or my résumé. The first time it meets a new kind of question, the draft waits for my approval; after that it writes on its own, with my approved answers as examples.
- It learns from my edits. When I change a draft, the edited version becomes the new example for that kind of question. It also notes which answers went out with applications that got a reply, though with so few data points that's a hint, not a science.
- Some things it never learns. Date of birth, Social Security number, bank details and passport numbers are never stored and never typed. A real application almost never needs them before an offer, so a form that asks is skipped and flagged as possibly not real.
- "Learning" means saved answers, not training. No model is fine-tuned on my data. Everything lives in my data folder; only the text a draft needs is sent to the model API.
# stories.yaml: real stories, real numbers, written once
- id: brandsafety-throughput
where: Amazon Ads
story: Tripled a brand-safety pipeline from 17M to 52M products a day; a 45-day backfill finished in under 14
fits: [data engineering, backend, performance]
- id: free-capacity
where: Microsoft, Azure Resource Graph
story: Helped a five-region scale-out that roughly doubled capacity with no extra cloud spend
fits: [platform, cloud, infrastructure]
-- applications.db: every question it has met
CREATE TABLE answers (question TEXT PRIMARY KEY, -- normalised wording
kind TEXT, -- fact | free_text | voluntary | never
answer TEXT, source TEXT, -- profile | story:brandsafety-throughput | me
approved INTEGER, uses INTEGER, last_used TEXT);
CREATE TABLE answer_aliases (wording TEXT PRIMARY KEY, question TEXT REFERENCES answers);The agent's skills
The agent isn't one big prompt. It's eight small skills, each a folder with instructions and a script, which the runner calls in order and I can test on their own.
find/ source adapters: ATS feeds, boards, career pages → normalised jobs
dedupe/ run cache, seen store, fingerprints → only new jobs
match/ rules, then a cached fit score → ranked shortlist
tailor/ pick the résumé, draft answers from profile.yaml → an answer sheet
apply/ field mappings + computer use in my browser profile → a submitted form
verify/ confirmation page and inbox check → applied | unverified
track/ applications.db, replies from email, the local board → statuses
outreach/ the four people searches, a three-sentence note, caps → the right person, messagedCheap models find, a good model applies
Most of the work is reading pages and sorting them, which a small, cheap model does well. Only the parts that act in my name get the strongest model I have.
Scripts and rules
- Reading ATS feeds
- Loading search pages with filters in the URL
- Dedupe, caps, known form mappings
free · most of the steps
Find and sort
- Pulling title, company, location and date out of a results page
- Placing an odd location in a country
- Tech or not, level band, first-pass fit score
fractions of a cent per job · most of the calls
Act in my name
- Computer use on unfamiliar forms
- Free-text answers drafted from my résumé
- Outreach notes, and the final fit check before applying
cents per application · a handful a day
Logins, secrets, and what it skips
The rule: .env holds keys and settings, never passwords. Website sessions live in a browser profile that only the agent uses. Any password it creates goes to the operating system's keychain.
# .env: keys and settings only
MODEL_API_KEY=... # fit scores and computer use
JOBS_INBOX=ambrosetuscanojobs@gmail.com # the agent's own address (section 07·b)
BROWSER_PROFILE=~/.jobhunt/browser # sessions live here, not passwords
MODE=auto # notify | review | auto, overridable per site
# sites.yaml: what the agent may do, per site
greenhouse.io: { account: none, find: feed, apply: auto }
lever.co: { account: none, find: feed, apply: auto }
ashbyhq.com: { account: none, find: feed, apply: auto }
myworkdayjobs: { account: per_company, find: agent, apply: skip } # a whole workday per application
taleo.net: { account: per_company, find: agent, apply: skip }
icims.com: { account: per_company, find: agent, apply: skip }
linkedin.com: { account: mine, find: agent, apply: via_company_site, pace: human }
google.com: { account: none, find: agent, pace: human }- Sign in once.
jobhunt setupopens the agent's browser profile, and I sign in to LinkedIn and the jobs inbox, two-factor included. Sessions are reused until they expire. - Heavy sites are skipped. Workday, Taleo and iCIMS want a new account per employer and a form that runs for pages. The agent records the role as skipped, with a link, and moves on.
- Passwords never touch a file. The few it needs live in the operating system's keychain (macOS Keychain, Windows Credential Manager) through
keyring. - CAPTCHAs are skipped too. They're never solved or bypassed. Because it's my real browser on my home connection, they're rare; when one appears, the role is marked skipped and the agent moves on.
- Workday lives up to its name: one application can take a whole one.
- Workday asks you to upload your résumé, then to type your résumé into boxes, then whether you'd like to upload your résumé. The agent is patient. Not that patient.
- On Workday, "Quick Apply" means "clear your afternoon".
- When a site asks the agent whether it's a robot, it gives the only honest answer: it leaves. Some tests you're meant to fail.
- It reads 1,240 job posts before breakfast, and is defeated by "select all squares with a bus".
- Every CAPTCHA is a tiny identity crisis. "Are you a robot?" "…Next job."
An inbox of its own
I made a separate email address just for job hunting, and gave the agent the keys to it. Everything job-related lands there, and nothing else. That means I can let it read every message without it ever seeing my personal mail.
- How it gets in: the agent's browser profile is signed in to the inbox for reading verification links. For everything else it uses the Gmail API with an OAuth token kept in the keychain, and no other account's data.
- Verification: every "thanks for applying" email is matched to its application by company and role. An application with no confirmation after an hour is "unverified", never "applied".
- One-time codes: some application forms email a security code before they accept a submission. The agent reads it from its inbox and finishes the form.
- Replies, never answered: rejections close the card, interview invitations move it to "interview" and land on my calendar list, and anything a person wrote goes on a "to respond" list. The agent never replies to anyone. Those messages are mine.
- Why a separate address: the blast radius. If the agent misreads or mis-sends, it does so in a mailbox that only ever held job mail, and recruiters see one consistent address for everything.
Finding the person behind the role
An application is one way in. A short note to the right person is often the better one. These are the four ways I find that person, the same steps I once wrote up for a friend. Methods 1 and 3 worked best for me; 2 and 4 matter just as much, especially at smaller companies where a job post may never reach LinkedIn.
The outreach skill does all four in my browser, after an application goes in:
- Find: run the four searches for the company and role, at a human pace, and collect the people: who posted, who engaged, who manages the team the description names.
- Rank: a hiring post's author first, then a manager in the named team, then a recruiter for that org. One person per role, never a crowd.
- Write: a note of three sentences, built only from my profile and the job: which role, one relevant thing I've done, and a thank-you. No invented connections, no flattery.
- Send, within limits: at most 5 notes a day, one per role, and one polite follow-up at most, a week later. Everything is logged against the application.
Hi Priya, I saw your post about the Platform team hiring an SDE II.
I just applied: I built event-driven pipelines on AWS at Amazon Ads and
now work on Azure Resource Graph. Thanks for sharing the role!The end of my day
This is the part I actually feel. Searching, filtering, form-filling and tracking used to eat my evenings. Now I open one page at the end of the day: everything it applied to, everything it skipped and why, interviews coming up, and the short list of people waiting for my reply.
The "End of day" tab in the screens above is that page. Its rule is simple: the agent tells me what needs a reply; it never sends one. Recruiters and managers who write back get a person.
My own dashboard
An agent that works while I don't is only useful if I can see everything it did. So JobHunt has its own dashboard: a small Streamlit app on localhost (the same tool I built the public search site with) that reads the three SQLite files. It's the one place I go to know where my job hunt stands, and the only place I ever act.
- Built on what the run already writes. Every panel is a query over
seen.db,applications.dband the run cache. No new data, no second source of truth, and nothing to keep in sync. - Read-mostly, by design. The dashboard only writes three things: my answers to waiting questions, my edits to drafts, and status changes I make by hand. Each goes through the same small module the agent uses, so the two never disagree.
- Answering here is acting. A Yes on "waiting on you" saves the fact and kicks off the parked applications straight away (06·b). Editing a draft updates the answer bank (06·b). Moving a card updates the tracker.
- Every number links to its rows. "48 not engineering" opens the 48 jobs with the reason each was dropped, which is how I catch a filter that's too strict before it costs me a good role.
- It tells me what not to do: nothing in "To respond" is ever answered by the agent, and the dashboard says so on the panel. Replies to people are the one job that stays mine.
Playing fair with job boards and employers
The easy version of this tool is a spam cannon: apply to everything, message everyone, as fast as possible. That burns recruiters' time, gets you ignored, and gets accounts blocked. Mine is built to send fewer, better applications.
- Public feeds first. ATS job-board feeds are published to be read; I read them politely, one request at a time per host, and respect
robots.txton career pages. - Honest about the grey areas. LinkedIn's and Google's terms prohibit automated access. The agent uses them only in my own account, at a human pace and volume, and never collects more than I would read myself. If either asked me to stop, it would.
- No CAPTCHA solving, no fake identities, no rotating IPs. If a site doesn't want a bot right now, the bot skips it and moves on.
- Caps an employer would agree with: a fit threshold, 10 applications a day, at most 2 roles per company in 30 days, 5 notes a day, one person per role.
- Everything sent is true. Answers and notes come only from my profile and the job. An application is still me talking to an employer.
Pull any plug
Everything here will fail one day. For each part: what I'd notice, how the tool finds out, and what it does on its own.
About ten dollars a month
The laptop, SQLite and Playwright are free. The only bill is model calls, and three habits keep it small: rules before the model, a cache keyed by résumé and job, and scripts instead of computer use on pages and forms I've seen before.
Set it up yourself
- Install: Python 3.12,
pip install -r requirements.txt, andplaywright install chromium. That's the whole stack. - Make a jobs inbox: a fresh email address used for nothing else, and a Gmail API token for it.
- Fill in
.envandprofile.yaml, and drop your résumés inresumes/. - Run
jobhunt setup: sign in to LinkedIn and the jobs inbox in the agent's profile, then a dry run that finds and scores but sends nothing. - Earn trust per site: review mode until a site has a clean record, then auto. Outreach starts as drafts for a week, then sends.
- Then just run it:
jobhunt runeach morning, andjobhunt watchin a spare tab if you want favourites hourly.
jobhunt/
jobhunt/ cli.py runner.py app.py (localhost board) db.py lock.py
skills/ find/ dedupe/ match/ tailor/ apply/ verify/ track/ outreach/
adapters/ greenhouse.py lever.py ashby.py careerpage.py linkedin.py search.py
mappings/ greenhouse.yaml lever.yaml ashby.yaml workable.yaml # field → profile key
data/ seen.db applications.db run_cache.db screenshots/
config/ profile.yaml sites.yaml companies.yaml .env
tests/ adapters (recorded pages) forms (saved pages) dedupe e2e (dry run)Every hard problem, and how I handle it
Thirty things that will happen sooner or later, each with the fix and what the fix costs. Filter by where they bite.
Keyword matching is very enthusiastic. Give it "Java" and no other filters, and it will tell you, with total confidence, that a barista job is a perfect fit.
What's next: the heavy ATSs, Workday first
Today the agent skips every "heavy" ATS: the ones that want an account per employer and a form that runs for pages. Workday is the biggest, but iCIMS, Oracle's Taleo and Recruiting Cloud, and SAP SuccessFactors work the same way, and together they're where most large employers hire. So they're next, one at a time, in the order they show up in my "skipped" panel. This part is planned, not built yet.
Workday goes first because it has the most skipped roles. Here's its wizard, page by page; the others get the same treatment.
- One ATS, not hundreds of sites. Every employer has its own Workday (or iCIMS, or Oracle) portal, but within one ATS the pages are the same. So each heavy ATS gets one field mapping, like Greenhouse has, keyed on the stable attributes it puts on its form fields. Computer use is only for each employer's custom questions, and what it learns on one portal is saved for every portal on that ATS.
- Accounts without me. One account per employer, created with the jobs inbox. The password goes to the keychain under that employer's portal, and verification emails and one-time codes are opened by the agent. The next application to that employer just signs in.
- The résumé-parsing trap. Workday, iCIMS and SuccessFactors all read the PDF into their own fields, and often scramble dates and titles. After every upload the agent compares each field with my profile and corrects it before moving on.
- A checkpoint per page. A six-page form that dies on page five resumes on page five. Intent-before-submit still guards the last click.
- Trust earned, not assumed. Each ATS starts in review mode until a handful of employers go through cleanly, then moves to auto. One ATS at a time, never all at once.
- What doesn't change. A CAPTCHA still means skip, with no solving services. The same caps apply (10 a day, 2 per company a month), and every answer stays true.
Others have done versions of this, and I'm borrowing ideas, not code: ApplyPilot drives Workday portals with a coding agent and pauses for a human at logins and CAPTCHAs; LangHire keeps what it learns per ATS so one Workday lesson applies to every Workday site; and Simplify Copilot fills Workday forms in the browser for a person to submit. Estimated effort: about a day for Workday, then less for each ATS after it, since the account, inbox, checkpoint and answer-bank pieces are shared. Each is tested on a few real portals in review mode before it ever runs on its own.
Decisions, one page
- How does it start?a schedulerone command in my terminal
It runs when I'm at the laptop and can see it. Cost: I have to type it.
- Which database?Postgres + Redisthree SQLite files
One writer, zero setup. Cost: none at this size.
- Where do jobs come from?scrape every boardfeeds first, then the agent
Exact timestamps, and the least grey area. Cost: a company list to grow.
- How are forms filled?computer use for allmappings, then computer use
Fast and free where forms are known. Cost: mappings to maintain.
- Which inbox?my personal emailits own address
Full access, small blast radius. Cost: one more account.
- How many applications?every match10 a day, 2 per company a month
Better replies, no flooded inboxes. Cost: some matches wait.
- Workday and CAPTCHAs?fight through themskip them
Nothing manual, nothing bypassed. Cost: a few roles a week.
- Replies to people?auto-responda "to respond" list
People get a person. Cost: ten minutes a day.
- Which model?the best for everythingsmall to find, best to apply
Spend where it acts in my name. Cost: two models to manage.
The well-earned drink
Every Brew Miles trip ends with one:
- Read the sources that want to be read, and you get exact times for free.
- Remember everything in two keys, because one job wears five IDs.
- Rules first, models last, and cache what the model says.
- Write the intent before the click, so a crash never applies twice.
- Cap yourself before anyone has to, send only what's true, and never auto-reply to a person.
- Give the agent its own inbox, and keep everything on your laptop.
Show the docs and research this leans on
- Greenhouse Job Board API, Lever Postings API and Ashby public job posting API.
- Playwright persistent browser contexts, and computer use (Anthropic docs).
- SQLite write-ahead logging, and keyring for the OS keychain.
- LinkedIn User Agreement (section 8.2 on automation) and Google Terms of Service.
- On applying early: Business Insider on applying within minutes and TopResume on the first 72 hours.