A Markdown-based operating system for running a job search — designed for both humans and AI agents.
A serious job search has two halves, and most tools only handle one.
The outbound half is pipeline management: a dozen companies at different stages, each with its own recruiter, conversation history, follow-up clock, tailored resume, and next action. Most of that state lives in scattered emails, browser tabs, and memory — which is exactly where it gets dropped.
The inbound half is where opportunities come from. Left alone it is accidental: a recruiter emails, a job alert fires, you happen to be browsing. Roles you would have wanted are simply never seen, and the funnel quietly narrows to whatever inbound produces.
Job Search OS covers both. Radar scans public job boards every day and hands you a ranked, reasoned review queue. The pipeline takes it from there. All of it is plain Markdown you can read in any editor, browse as an Obsidian vault, and hand to any AI agent as a shared source of truth.
This repository is a public starter template. It contains only fictional placeholder content. A real job search involves recruiter names, compensation data, and interview details — keep your copy in a private repository.
Three ideas carry the whole system:
- One file per company is the source of truth. Everything about a company lives in
Companies/[Company].md: role details, contacts, the full conversation log, follow-ups, resume notes, and a status history. Nothing about a company is stored anywhere else — so neither you nor an agent ever has to reconcile conflicting copies. - The pipeline is a status board, not a database.
PIPELINE.mdis a single Markdown table with one row per active track: status, last contact, next action, due date. Closed tracks move to a## Closedsection so the live table only ever shows open work. It is glanceable in any editor and trivially parseable by tools. - Agents follow written protocols.
AGENTS.mdtells any AI agent how to log a conversation, update the pipeline, surface overdue follow-ups, and close out a track — so every agent session leaves the system more organized, not less. - Discovery is a daily job, not a mood. Radar scans public job boards on a schedule and writes a ranked record you triage in a couple of minutes, so which opportunities you see stops depending on who happened to email you.
RADAR (inbound) PIPELINE (outbound)
───────────────── ───────────────────
public ATS boards Companies/*.md
↓ scan daily ↓
title + location filter PIPELINE.md (generated)
↓ seen-set dedupe ↓
score vs the 10-dimension rubric follow-up + report sweeps
↓ ↓
Radar/YYYY-MM-DD.md ──you tick──▶ resume fork ──▶ you apply
(ranked, with reasoning) Promote
One rubric spans both halves. Radar scores a posting on the same ten dimensions
job_search_os.py scores a company record on, so a discovered role and a
hand-entered one are directly comparable and a promoted role carries its score
across without translation.
Nothing is automated past the point where judgment matters. Radar finds and ranks; you decide; the tooling drafts. It never submits an application.
The repository includes a dependency-free automation layer that keeps the Markdown system auditable:
python3 tools/job_search_os.py check
python3 tools/job_search_os.py pipeline --write
python3 tools/job_search_os.py report --write
python3 tools/job_search_os.py resume-manifest --company "Acme Robotics" --write
python3 tools/job_search_os.py application-draft --company "Acme Robotics" --writeIt adds the CareerOps-inspired capabilities this template is meant to demonstrate:
- opportunity scoring with a visible 10-dimension rubric;
- pipeline generation from company frontmatter;
- duplicate, metadata, and pipeline-drift checks;
- optional posting URL liveness checks;
- follow-up and open-loop reporting;
- funnel analytics by status and source;
- ATS/posting scan ranking without submitting applications;
- resume edit manifests that preserve your preferred external document renderer;
- review-only application drafts grounded in the story bank.
The generated job-search-os-report.md, resume manifests, and application drafts are safe sample artifacts when run against the fictional data in this public template.
Radar scans a configured list of company job boards every day, keeps only roles matching your targets, scores what's new, and writes a dated Markdown record you triage with checkboxes.
python3 tools/job_search_os.py radar verify # check every board resolves
python3 tools/job_search_os.py radar collect --dry-run # preview; creates nothing
python3 tools/job_search_os.py radar collect --write # write today's record
python3 tools/job_search_os.py radar collect --score --write # add model reasoning
python3 tools/job_search_os.py radar process --write # act on ticked boxesEvery source is a public, unauthenticated ATS endpoint — Greenhouse, Ashby, Lever, and Workday. No API keys, no proxies, no scraping, and no LinkedIn, whose terms prohibit automated access and whose postings mostly originate from these same boards anyway. Collection, filtering, and deduplication cost nothing.
Everything personal lives in radar.config.yml: work authorization, location
tiers, role families, exclusions, the company list, and the scoring model chain.
No location or job title is hardcoded anywhere in the code.
Location scoring is remote-first by default and encodes something worth stating plainly: a posting reading "Canada (Remote)" means remote within Canada and still requires local work rights. It is scored below a domestic on-site role rather than as remote, and flagged 🛂, so the queue never implies a role is more reachable than it is. Postings that explicitly offer visa sponsorship get a bonus and a different callout.
Radar always produces a deterministic keyword score. Set OPENROUTER_API_KEY and
it additionally scores each new role with a model against the same ten rubric
dimensions, writing a short rationale per role — which is what makes the record
reviewable in two minutes instead of twenty.
The model chain is tried in order and falls back to the deterministic score if
every model fails, so a bad model day degrades the record but never fails the run.
location_fit is always computed deterministically and the model's value
discarded: it encodes work authorization the model cannot know.
Treat model rationale as a directional read, not a citable claim. Free models confabulate specifics; the posting link is one click away.
Three checks run on the record you actually read, not on a function's return value. Every one of them exists because a fully green unit suite shipped the bug anyway.
Attribution. Models rarely invent an employer. They take a real one from your background and pair it with an achievement that belongs to a different one — "proven experience deploying ML at <somewhere you worked for six weeks>" — written fluently enough to read as evidence. No allowlist of company names catches that, because the company name is right; the pairing is wrong.
So the scorer must emit a structured evidence array of employer-and-claim
pairs beside its prose, and every pair is checked against the scoring.evidence
ledger in radar.config.yml. Prose naming an employer without declaring a claim
for it is rejected too, since that is the obvious way around the check. A
violation is treated like a parse failure: the run advances to the next model in
the chain, which is a free reroll. Leave the ledger out and scoring runs
unchecked, exactly as before.
An employer listed with no claims can be named as somewhere you worked but never cited as proof — useful for a role too short to have delivered anything you would defend in an interview.
Metadata. Boards return placeholder employers: Adzuna sends the literal
string UNAVAILABLE when a listing hides its company. Those postings are held
before scoring, listed separately, and refused by promote — otherwise the
tracker gains a company file named UNAVAILABLE.md and treats it as real
forever.
Duplicates. The seen-set keys on ats:company:job_id, which cannot tell
that "Globex Corporation, N.A.", "G.L.O.B.E.X.", and "Globex" are one employer
posting one role three times. Identity is rebuilt from the normalized name and
the destination URL. Company matching is deliberately loose and will also fold a
parent into a subsidiary, so the surviving row lists every key it absorbed and
the folded copies stay promotable.
A fourth check is advisory: a rationale that flatly denies evidence your ledger holds ("no documented ML work") raises a review flag in the day record without changing the score. Whether that denial is wrong or is a fair statement about a specific posting's bar is a judgment call, and a rule strict enough to catch it also fails rationales that name a real gap well.
Each day's record is a scannable table linking down to a block per role with its score, per-dimension reasoning, and two checkboxes:
- [ ] Promote `greenhouse:Acme:12345`
- [ ] Dismiss `greenhouse:Acme:12345`Ticking a box does nothing on its own — markdown checkboxes are inert. radar process --write reads them, acts, and rewrites each handled line as plain text
so a second run finds nothing to do. Promoting writes a Companies/*.md file
that passes check with zero errors, regenerates PIPELINE.md, and produces a
resume manifest.
There is no rolling inbox — per-day records are the queue. An earlier design kept both, and the inbox was regenerated each run, so un-actioned roles silently vanished: exactly the failure it existed to prevent. Day records give the same queue plus the daily delta, and miss a day and its file is still there with live checkboxes.
.github/workflows/radar.yml runs Radar on a weekday cron, commits the record,
and optionally posts a short digest to Telegram. It installs nothing — Radar is
standard-library only. Set OPENROUTER_API_KEY, and TELEGRAM_BOT_TOKEN plus
TELEGRAM_CHAT_ID if you want the push.
The cron is commented out in this template — the workflow is manual-only here, and you uncomment the schedule in your private instance. Run it from the Actions tab to watch it work end to end.
This is worth doing deliberately rather than by default. The workflow commits its own output back to the repository, so on a public repo a daily run publishes which boards you watch, which roles you were shown, and the model's written assessment of your fit for each one. None of that is secret individually; together it is a detailed picture of your job search, accumulating on a schedule, under your name. Enable the cron where the repository is private.
The repository ships with a visual dashboard that renders PIPELINE.md as a Kanban board in your browser:
python3 tools/pipeline_board.py --open- Columns by stage: Prospect → Outreach → Applied → Screen → Interview → Offer, plus Paused.
- Company radar: one health card per company (At Risk / Active / Waiting / Clear, open roles, nearest due date). Click to filter the board to that company.
- Cards show the role, a priority badge derived from stage, due dates with overdue and due-soon highlighting, a stale flag when a track has gone quiet too long, and the next action — with the card title deep-linking to the company file in Obsidian.
- Filters: priority chips and live search, combinable with the company filter.
The board is read-only by design: the Markdown stays the single source of truth, and the generated tools/dashboard.html is a gitignored build artifact. The script is dependency-free Python 3 and the HTML is fully self-contained (no network requests), so it works on a fresh clone with zero setup. Run the tests with python3 -m unittest tools.tests.test_pipeline_board.
-
Click Use this template on GitHub and create a new private repository.
-
Clone it and open the folder in your editor (or as an Obsidian vault).
-
Run setup:
python3 setup.py
It asks what roles you're after, where you can work, which companies to watch, and how you want scoring configured, then writes
radar.config.yml, seedsGOALS.md, and clears out the fictional example data. Nothing is written until you confirm, and re-running it is safe. You can skip it and editradar.config.ymlby hand. -
Check every board resolves. A wrong slug returns nothing and looks exactly like a company with no open roles, so this step is worth doing before you trust an empty result:
python3 tools/job_search_os.py radar verify
-
Preview your first scan without writing anything:
python3 tools/job_search_os.py radar collect --dry-run
-
Fill in the Positioning section of
GOALS.md. Radar sends it to the scoring model as context, so concrete beats aspirational. -
Point your AI agent at the repo and say:
Read AGENTS.md completely, then look at PIPELINE.md and tell me my most urgent follow-ups.
job-search-os/
├── setup.py # Interactive first-run setup
├── AGENTS.md # Agent protocols: logging, pipeline upkeep, follow-ups
├── GOALS.md # Target roles, positioning, and priority rules
├── PIPELINE.md # Live status board — every active track at a glance
├── radar.config.yml # Radar's only config: geography, roles, boards, evidence ledger
├── Companies/ # One file per company: the source of truth
├── Radar/ # One dated record per scan — the inbound review queue
│ # (ships with one example day; delete it after your first run)
├── Interview Prep/ # Story bank and question prep that work across companies
├── Resumes/ # Baselines, fork specs, and tailored resume tracking
├── data/ # Radar state: the seen-set and the candidate store
├── .github/workflows/ # Daily Radar run
└── tools/
├── job_search_os.py # One CLI entry point for everything below
├── pipeline_board.py # Generates the visual pipeline dashboard
├── resume_fork.py # Forks a baseline resume via the Google Docs API
└── radar/ # Providers, filters, scoring, integrity gates, triage
data/radar-seen.tsv is what stops a role reappearing every day, so it must
persist between runs — commit it, or the queue repeats itself.
Open the newest file in Radar/, scan the table, click through to a role, and tick
Promote or Dismiss. Then python3 tools/job_search_os.py radar process --write
(or ask your agent to process the day). Promoting creates the company file, updates the
pipeline, and writes a resume manifest — from there it is an ordinary track.
After any call, email, or interview, tell your agent what happened. It appends a dated entry to the company's Conversation Log, extracts follow-ups as checkboxes with owners and due dates, updates the frontmatter (last_contact, next_action, next_action_date), and refreshes the generated PIPELINE.md.
Ask "what are my job search follow-ups?" The agent scans every company file for open checkboxes and overdue next_action_dates, groups them by company, and flags anything that has gone quiet. python3 tools/job_search_os.py report --write creates the same view as Markdown.
Keep a baseline per track and fork a copy per company. python3 tools/job_search_os.py resume-manifest --company "Acme Robotics" --write creates a reviewable edit manifest before you touch the final document.
If your baselines live in Google Docs, resume-fork does the fork itself — copying the baseline so every style survives, then swapping text with the Docs API and verifying each replacement changed exactly one occurrence. Resumes/README.md covers the spec format, credentials, and why this is safer than driving Find & Replace by hand.
For an agent workflow that maps the job description to verified candidate evidence, proposes each material change for approval, and checks the rendered result, use the companion Generate Resume skill. Job Search OS owns role and pipeline state; Generate Resume owns the evidence-backed resume transformation. The skill writes the final document link or path back to the company record's resume_ref, then Job Search OS regenerates the pipeline.
When a company rejects, ghosts, or you withdraw: set the company file's status, add a Status Log entry with the outcome, and move the pipeline row to the ## Closed section. History is preserved; the live board stays clean.
- Markdown is the database. Everything is portable, diffable, and human-readable.
- One canonical location per fact. Company files own company facts; the pipeline is a view.
- Agents operate under written protocols, not improvisation.
- Follow-up discipline is the whole game — the system exists to make dropped loops visible.
- Dashboards are generated artifacts, never sources of truth.
- Private data belongs only in a private instance.
- Drafts are review-only. The tooling never sends messages or submits applications.
- Discovery is deterministic first. Filtering and deduplication are free and reproducible; a model only ever sees what survived, and never overrides a fact it cannot know.
- Every automated edit is verified. A step that silently did nothing is the failure mode worth engineering against, so the tooling checks its own work and aborts loudly.
Job Search OS is a companion to Personal OS, a general-purpose Markdown knowledge system for humans and AI agents. They share the same philosophy: shared context, durable memory, and auditable workflows. Run them separately or drop this structure into a Personal OS vault as a subsystem.
Generate Resume is the optional resume-tailoring companion. It can run independently with any baseline and evidence bank, or consume the company context and resume manifest produced here.
This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.
You may share and adapt it with attribution for noncommercial purposes. Adaptations must use the same license.
Copyright © 2026 [Your Name].