Skip to content
Career
Command

/rank

You are batch-scoring the jobs that `/scrape` has collected, so the user can decide where to spend `/apply` effort. `/scrape` finds and dedupes postings; `/apply` evaluates one at a time in depth. `/rank` is the bridge: it scores every new posting against the fit framework and

From plugin
ai-job-search
68k12 skills3 agents12 commands
$ npx -y skills add ai-job-search-dev/ai-job-search --agent claude-code

Command definition

rank.md

/rank - Triage Scraped Jobs into a Ranked Shortlist

You are batch-scoring the jobs that `/scrape` has collected, so the user can decide where to spend `/apply` effort. `/scrape` finds and dedupes postings; `/apply` evaluates one at a time in depth. `/rank` is the bridge: it scores every new posting against the fit framework and returns a ranked shortlist.

`/rank` produces **triage scores**, not final evaluations. It scores from the posting text and the candidate profile only - no company research, no reviewer agent. `/apply`'s Step 1 evaluation (which adds company research) remains authoritative and always re-runs when the user applies.

Follow these steps **in order**.

---

Step 0: Parse Input

`$ARGUMENTS` may contain:

  • Nothing → rank up to 10 jobs with status `new` or `unreachable` (a fetch that failed on an earlier run, retried) in `job_scraper/seen_jobs.json`
  • A focus area (e.g. `/rank data science`) → rank only jobs whose title or stored fit-notes match the focus
  • `--all` → re-rank every job that has not been applied to, including previously ranked ones (useful after the profile changes)
  • `--limit <N>` → maximum number of jobs to score this run (default 10)
  • `--top <N>` → shortlist size (default 5)

`--limit` bounds the expensive fetch-and-score work; `--top` only bounds how many scored jobs appear in the shortlist. They are independent: jobs beyond `--limit` are deferred, not silently discarded.

---

Step 1: Load State

Never read `job_scraper/seen_jobs.json` into the conversation. It holds every job the workspace has ever seen - most of it `skipped` - while a run only ever touches the handful of entries being scored, so a manual read costs the whole backlog on every run and grows for the life of the workspace. Selecting candidates is a query, so run the query:

> **Windows:** run these tools as `python`, not `python3`. A python.org install ships no `python3`, so that name reaches the Microsoft Store alias, which either exits 49 with no output (#669) or starts a separate Store Python without the packages `pip` installed (such as pypdf). `.claude/settings.json` allows both spellings.

python3 tools/rank_state.py candidates --limit 10          # add --all / --focus "<text>" per Step 0

It applies the status filter (`new` and `unreachable`, or any status with `--all`), the tracker exclusion (any company+role already in `job_search_tracker.csv` is out of scope regardless of flags - it has been applied to or consciously tracked), the focus filter, and `--limit`, then prints one compact object per candidate (`key`, `title`, `company`, `url`, `portal`, `deadline`, `posted_date`) plus the counts: `eligible`, `deferred` (eligible beyond the limit, kept at their current status so a later run continues the backlog), `excluded_by_tracker`.

If it exits with "not found", tell the user to run `/scrape` first and stop.

If it reports no candidates, there is nothing to score, but already-ranked jobs still need Step 3's rule 6 expiry sweep. Run:

python3 tools/rank_state.py sweep --write

Skip profile loading and Steps 2-4 on this path: no jobs need fetching or scoring. Present a sweep-only Step 5 summary: say "Nothing new to rank", report `swept` and `newly_expired`, list `closing_soon` with deadlines and posting URLs, and report any `unparseable_deadlines` with their portals. Apply rule 6's same reporting and defensive-date rules. Then suggest `/scrape` for fresh postings and stop. An empty candidate batch, including one caused by a focus filter or tracker exclusion, never skips this sweep.

Then read the scoring framework and profile **once**:

  • `.claude/skills/job-application-assistant/04-job-evaluation.md`
  • `.claude/skills/job-application-assistant/01-candidate-profile.md`

State how many jobs will be ranked and how many are deferred before proceeding.

---

Step 2: Batch-Fetch and Score

Dispatch parallel **`posting-scorer`** agents via the **Agent tool** (`subagent_type: posting-scorer`, the shipped definition in `.claude/agents/posting-scorer.md`: Read, WebFetch, WebSearch and the allowlisted Bash only - no Edit, no Write, because these agents hold untrusted posting text), ~5 jobs per agent (a single agent is fine for ≤5 jobs). Token-efficiency rules, consistent with `/apply`:

  • Pass each agent everything it needs **inline in the prompt** - the job list (`key`, title, company, URL - the `key` exactly as Step 1's `candidates` output printed it, because the agent must echo it back and cannot derive it: it is a company+title slug from `tools/job_key.py`, not the URL) and a compact scoring rubric extracted from the files you read in Step 1: the strong/moderate/weak skill match areas, direct/adjacent experience domains, behavioral thrive/drain factors, career goals, deal-breakers, and the location constraints. Do **not** make agents re-read the profile files.
  • Agents fetch each posting URL following the escalation order in `.claude/skills/job-application-assistant/09-web-research.md` (using the installed portal CLI's `detail` command when the URL belongs to a portal in `.agents/skills/`, or `WebFetch` otherwise) and score **only from actually fetched content** - never from the title alone, never from fabricated posting content. A job that is not scored gets one of two statuses, and they mean different things:
  • `expired` - **positive evidence the posting is closed**: the page says it is no longer available or the deadline has passed, the server answers 410 Gone, or the URL redirects to a listing page instead of the posting.
  • `unreachable` - **retrieval failed, so nothing is known about the posting**: 401/403 or a login wall, a 404, a timeout, a 5xx, or an empty or unparseable page. The agent adds `"fetch_error"`, one short plain-text line naming the failure (e.g. `"403 after curl retry"`). A 404 is `unreachable`, not `expired`: links break while postings stay live (#538 was one).
  • **Before returning `unreachable` or `expired`, the agent must
Read more
@madslorentzen
Ships withai-job-search

The job search that runs on your machine. AI job application framework built on Claude Code: evaluate postings, tailor CVs, write cover letters, prep interviews. Fork it and own it.

68k3 skills3 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
68.5k
Star history of MadsLorentzen/ai-job-search on GitHub, a chart by star-history.com

Other commands on ai-job-search.

add-portal

add-portal

Command

You are helping the user build a job-portal search skill for a job board in their market. The repo ships worked examples of the…

add-template

add-template

Command

You are helping the user register their own CV or cover letter template with the AI Job Search framework — LaTeX, Typst, or any…

apply

apply

Command

You are orchestrating a two-agent job application workflow. The job posting is provided below as `$ARGUMENTS` (either a URL or…

expand

expand

Command

You are enriching the candidate profile by discovering competencies hidden in documents and public online presence. This command…

gmail-sync

gmail-sync

Command

You are scanning the user's Gmail for status signals on tracked job applications (interview invites, assessment links, offers,…

More commands from other plugins.

setup

setup

Command

Setup wizard for lockedin. Walks the user through: (1) wiring the HUD into Claude Code's statusLine, (2) default Q&A interview…

lockedin5mo127
@daypunk@daypunkInstall Command