A job-search pipeline: scrape → track in Notion → tailor → apply.
Every morning it reads company job boards (and optionally LinkedIn and Indeed),
scores each posting against the person described in user/profile.md, and writes the
ones that pass into a Notion tracker. For a role you pick, it drafts a CV, a cover
letter and answers to the application questions, all tailored to the posting and all
traceable to what you actually wrote. You submit.
No dependencies: Python 3.9+ and the standard library.
Start with SETUP.md.
./job run # stage 1: scan, score, update the tracker
./job tailor 42 # stage 2: tailored CV, cover letter and draft answers for tracker row 42
./job apply 42 # stage 3: tailor, review and edit, PDF; Claude's browser for Greenhouse/Lever/Ashby, a tab otherwise
./job edit 42 # chat with Claude to edit that application's CV and answers
./job build 42 # rebuild that application's PDF and review after edits
./job open --top 10 # open the best LinkedIn / Workday / Indeed / Amazon roles as tabs
./job status 42 applied # after you submit; also sets Status in the tracker
./job remember 42 # keep what that application confirmed: its facts -> your profile
./job show 42 # why a role got its Fit
./job doctor # is everything set up
./job --help # everything elseAn application confirms things: a date that was wrong, a job that is still going, a responsibility the CV never mentioned. Without somewhere to put them, the next application asks again.
| File | What it holds |
|---|---|
user/profile.md → Facts about you |
Facts that outlive one application. ./job remember <ID> asks, one at a time, before moving a confirmed fact out of an application's review.md and into here. tailor reads them, so later applications start from them |
user/LOG.md |
What happened and when: applications prepared and sent, base CV and cover-letter versions. Newest first. Written by status and remember |
user/cv-history/, user/cover-letter-history/ |
A dated copy of the base CV or letter from before each change you approve |
Nothing is promoted without a yes. A fact that is true of one posting is not
automatically a fact about you, so remember asks per fact, and the edit chat asks before
it touches your base CV or base letter. It is all plain markdown in user/, which git ignores: no
database, no service, and nothing that needs a second brain to work.
Salary floor, locations and their order, visa situation, the roles you want (core
and adjacent), your industries, target companies, your CV, where the tracker
lives. The code carries only generic vocabulary (jayohbee/conditions.py) and
default weights. profile.example.md is a made-up person and the test fixture.
user/ is gitignored, and a test reads its Private words and fails if
any appears in a published file.
| Source | How | Cost |
|---|---|---|
| Greenhouse, Lever, Ashby | The public JSON behind each company's own careers page | Free |
| Workday | The JSON a company's myworkdayjobs.com page loads — the same thing Apify's Workday actors read. Searched, titles filtered locally, a description fetched only for a matching non-senior title | Free |
| Amazon | amazon.jobs' own search JSON | Free |
| LinkedIn, Indeed | Through Apify, which runs the scraper on its own accounts | Apify free plan covers light daily use — see SETUP.md |
./job company verify checks every board still answers. Companies with no
readable board are listed under Watch by hand, because "cannot read this
board" and "this company has no jobs" are different facts.
Fit is 0–100. 0 means excluded; 1–100 maps the worst posting that can still pass to the best reachable one, both computed from your weights. The components: pay against your floor; visa runway (E-Verify, H-1B history, what the posting says) for countries where you need sponsorship; how technical the role reads; your location order; your industries; whether it says entry-level in words, and on top of that whether it is a structured early-career program (an APM rotation, a graduate scheme), which accepts new graduates where an open req does not; core versus adjacent role.
Hard excludes: a senior or people-manager title, an internship, a title that
is none of your roles, "will not sponsor" where you need sponsorship, a
citizenship or clearance requirement, and — with entry_level_only — more years
of experience than you allow.
Every component is written into the tracker's Why column as a sentence, so a Fit can always be read back.
- A row is created once, keyed on
Job Key, and refreshed on later runs. - Status is yours. A scan never writes it. Only commands you run for one job write it:
tailorsets Drafted,statussets what you tell it.statusalso stamps Status Changed, and Applied with the day you applied, so the tracker is a record and not just a state. - Nothing is deleted. A posting the board pulled has Still Open unticked.
- Last Checked is stamped on every row on every run — if it is not today, the run did not happen.
Applications ask whether you will need sponsorship. A bot answering that wrong is
a misrepresentation that cannot be withdrawn. Automated submission also breaks
every major board's terms, and puts at risk the account you need for networking.
tailor writes the drafts under a zero-fabrication rule — every CV bullet traced
to a line in your real CV — and stops there.
job python3 -m jayohbee, with .env loaded
profile.example.md the template / test fixture
cv.example.md a CV in CV markdown; cover-letter.example.md a letter in the same spirit
SETUP.md setup, for a person or a Claude Code session
jayohbee/
profile.py reads profile.md
conditions.py generic vocabulary and default weights
scoring.py Fit, and the reasons behind it
companies.py the target list, from the profile
sources/ greenhouse, lever, ashby, workday, amazon, apify
comp.py salary ranges out of posting text
notion_sync.py the tracker: create it, push to it
tailor.py stage two
latex.py CV markdown -> one-page PDF with Tectonic; the cover letter too
apply.py stage three: PDF, form answers, routing, tabs
review.py checks a tailored CV against the base CV
edit.py the edit chat and the yes/no suggested edits
memory.py what carries between applications: facts, the log, versions
writing/ avoid-ai-writing rules (MIT, vendored) the tailor and the review use
db.py SQLite: postings, and what you did with them
scripts/
daily.sh the scheduled run
install_launchd.sh macOS schedule
tests/ python3 -m unittest discover -s tests -t .
All three are the same mistake — a phrase read without the words around it — and all three were silent.
itaris insidemil-itar-y. Every US EEO statement names military service, so a rule for defence-export roles excluded 163 ordinary jobs.apmis Application Performance Monitoring at Datadog, so an engineering manager passed the product-title filter.h-1b sponsorshipwas read as an offer when Roblox writes "the Company may not be able to employ candidates who have United States work authorization related to certain U.S. visa categories, or support future H-1B sponsorship at this time". The negation sits 21 words in front of the phrase, so no list of negative phrases reaches it: 245 open postings matched a WILL-sponsor phrase for refusing to sponsor, and one of them — Roblox's own Associate Product Manager, the single posting in the file not already excluded for some other reason — scored a +15 sponsorship bonus and Fit 77.
The first two were fixed by matching on word boundaries. The third is fixed by looking back from the phrase for a negation, as far as the start of the sentence, and letting the negation win. All three are pinned by tests.