Academic Jobs Skill
The user-facing entry point for academic opportunity discovery. Broad postdoc
curation runs both board searches and official institution/group-direct discovery.
The user does not need another skill. The ajo CLI remains the board collector;
this integration changes the assistant workflow, not CLI source options.
Request routing
| Request |
Execution |
| Broad personal postdoc search, e.g. Physics×AI 포닥을 추려줘 |
Both boards plus bounded official institution/group discovery; one integrated result |
| Explicit AJO-only / Inspire-only / board postings only |
Only the requested board(s); no direct expansion |
| Named lab/institution, e.g. Tübingen AI postdocs |
Official research and recruitment pages first; board identity checks for duplicates, no global fetch |
| Changes since previous search |
Refresh the previous scope in both tracks and compare snapshots; preserve any explicit board-only restriction |
| Maintenance: config/list/show/enrich/mark-seen/prune, including “검색 preset 설정을 보여줘” |
Maintenance only: execute the requested CLI operation, no board search, direct discovery or snapshot comparison |
| Faculty or PhD opportunities |
Rank-appropriate board and employer sources; do not use the postdoc-only direct ledger for other ranks |
Integrated curation workflow
- Ground rank, research/method axes, target start and region policy in the current
profile, saved preset and user decisions. Use the same policy in both tracks.
- For broad postdoc searches, execute the board flow below AND
references/direct-discovery.md. Start with
2–3 relevant institutional ecosystems in preferred regions and declare the scope;
expand for promising leads or requested breadth. This is not a global census or
a quota of recommendations. Do not ask whether to add this track.
- Read official research, recruitment policy and linked calls. The direct reference
owns the evidence/status schema and uses the bundled
scripts/direct_ledger.py validator/diff. No managed or externally installed
companion skill is required. Reuse board results for identity checks rather
than recursively restarting this workflow.
- Deep-read both sources. Board candidates require
ajo show and the complete
stored body; direct candidates require the official page and linked call.
Follow the full-body extraction instructions in references/curation.md when
display truncation hides conditions. Targeted untyped searches can recover
blank/mixed-rank fellowships; verify the postdoc branch in the body.
- Deduplicate verified identical appointments by employer, group/PI, project,
reference number, dates and application URL. Preserve all source links and
(source,id) matches in one row. Different projects remain separate; a general
application route is not an extra vacancy. A local DB miss does not prove
absence from either live board.
- Deliver ONE result with four sections:
- Current applicant-facing vacancies: verified board and direct vacancies
together, preference tier then actual application urgency; no invented dates
for undated rolling calls.
- Standing application routes: ongoing applications, current funded capacity
not established.
- Research-fit leads: no verified hiring, ranked by fit.
- Conflicting/expired/closed or host-funding-only findings: reasons and
evidence, excluded from open-vacancy totals. Host support is not employment.
- Use the ten-field curation contract below. Separate method fit, scientific-domain
fit, transition cost and start compatibility. Fit is not selection probability.
Missing facts remain 명시 없음/null. An open banner against an expired linked PDF
is a conflict, not permission to ignore the deadline.
- Report board candidates/details/truncation, direct institutions/groups/calls
actually read, and deduplicated counts by status. Do not sum overlapping fetches
as unique jobs. Distinguish newly discovered records from newly posted calls.
- Store direct evidence separately under
~/.local/share/academic-direct-opportunities/,
never as fabricated valid rows in AJO jobs.db. Validate dated snapshots and
compare them with the bundled helper. Refresh sources before claiming a change;
unobserved records and failed URLs are not automatically closed.
Mail, applications, calendar tasks, scheduled watchers, pruning and mark-seen are
not implicit in discovery. Respect the user's authorization for each.
Board collector
The ajo CLI fetches valid postings from two boards into a local SQLite store:
- AJO — Academic Jobs Online (
academicjobsonline.org), HTML scraping, all fields.
- InspireHEP — the HEP/astro jobs board (
inspirehep.net/jobs), via its public JSON API.
By default ajo fetch searches both boards with the same keywords and merges the
results, deadline-sorted. Each posting carries a source (ajo or inspire); the two
boards use overlapping integer ids, so everything is keyed by (source, id).
Quick Reference
| Intent |
Command |
Reference |
| Integrated postdoc curation |
Board CLI + official-source research |
references/direct-discovery.md, references/curation.md |
| Validate / compare direct snapshots |
python3 scripts/direct_ledger.py validate FILE / diff BEFORE AFTER |
references/direct-discovery.md |
| Show / edit field presets |
ajo config [...] |
references/presets.md |
| Fetch current open postings |
ajo fetch [--preset N | --keyword K] [--source ajo|inspire|both] [--preferred TIERS] [--excluded LIST] [--detail-cap N] |
references/fetch.md |
| Show stored postings |
ajo list [--valid] [--new] [--source S] |
references/schema.md |
| Inspect one posting (stored-first) |
ajo show {id} [--source ajo|inspire] [--refresh] |
references/fetch.md |
| Fetch missing detail bodies |
ajo enrich [--source ajo|inspire] [--detail-cap N] [--include-expired] |
references/fetch.md |
| Emit curation skeleton |
ajo report [--source S] [--preferred TIERS] [--excluded LIST] [--out PATH] |
references/curation.md |
| Mark postings as seen |
ajo mark-seen --all |
references/fetch.md |
| Drop expired postings |
ajo prune |
references/schema.md |
Running the CLI
The CLI lives in this skill directory. Always invoke it through uv:
uv run --project <skill-dir> ajo <command> [...]
where <skill-dir> is the directory containing this file. Add --json to any command
when you (Claude) need to post-process the output; the default is a human table.
First run auto-creates the data dir, the SQLite DB, and a default physics-ml preset.
Curation rule (read before writing a report)
Deep-read every reported board posting with ajo show {id} --source S and its
complete stored body. For direct opportunities, read the official recruitment page
and linked call. Never judge either source from titles or keyword matches alone.
Every posting in a report must fill the mandatory 10-field schema:
- 직급/seniority
- 기관, 그룹, 국가
- 연구주제, PI
- 자격/eligibility
- 기간, 급여, 시작일
- 마감 체계 (hard/rolling/etc.)
- 지원 서류
- fit 근거 + 등급
- 신빙성/주의 플래그
- 출처 URL
Use ajo report only for the board skeleton, then add verified direct records and
the integrated result sections. The complete curation procedure is in
references/curation.md. Reports are saved to
~/Dropbox/AJO/AJO_YYYY-MM-DD.md in Korean.
Core behaviour you must understand
Two board sources, one CLI view
ajo fetch runs the preset's keywords against every board in the preset's sources
(default ["ajo", "inspire"]), merges, dedups within each board, and stores everything
keyed by (source, id).
- Override per run with
--source ajo (AJO only), --source inspire (InspireHEP only), or
--source both. With an ad-hoc --keyword, both boards are searched unless --source says
otherwise.
- The same preset filters apply to both boards:
position_types is matched against the AJO
"Position Type" and against the InspireHEP ranks (e.g. postdoc matches POSTDOC);
countries is matched against the institution string (plus InspireHEP regions).
AJO validity (HTML)
Validity is judged from the detail page, not the list. The AJO list page only shows a
deadline for some postings, and a missing list deadline does NOT mean "no deadline". So
ajo fetch fetches each AJO candidate's detail page by default and judges validity from the
effective deadline = firm Appl Deadline if present, else the listed until date.
--fast skips AJO detail pages (faster but deadlines are approximate and many valid postings
will be missed). Prefer the default detail mode for correctness. --fast does not affect
InspireHEP.
InspireHEP validity (API)
The InspireHEP API is queried with status=open (server-side), so closed postings never
arrive. The structured deadline_date is used directly, no detail fetch needed.
In both cases:
- valid → effective deadline is in the future
- expired → effective deadline has passed (excluded)
- rolling → no deadline (excluded unless
--include-rolling)
Common Rules
Base directory
Board CLI state lives under ~/.local/share/academic-jobs/ (override with AJO_DATA_DIR):
jobs.db — SQLite store of postings
config.toml — field presets
Board-track execution (not the whole integrated search)
ajo fetch --json [--preferred "KR,DE; JP,HK,GB,US"] [--excluded "IN,IL"] [--detail-cap 80]
(uses the default preset; fetches details up to --detail-cap; stores + flags new).
Pass --preferred/--excluded to override the preset for this run without saving.
If AJO has more candidates than --detail-cap, the run is truncated; run ajo enrich in
a follow-up pass to capture the remaining detail bodies politely.
- Render the returned
jobs as a table sorted by pref_tier then deadline. Surface postings
with "new": true first or in a separate "New since last check" group.
- After presenting, if the user has reviewed them, run
ajo mark-seen --all so the next
fetch only flags genuinely new postings.
- To inspect one posting:
ajo show {id} [--source ajo|inspire]. It reads the stored row
(including the cached description body) first; it only hits the network when the row is
missing, has no stored body, or --refresh is given. A live fetch is written back to the DB.
Output formatting
- For broad searches, use the integrated sections above. CLI fields below describe
board records; label direct sources and annotate country/preference tier using
the same active policy.
- Sort by deadline ascending; show source, deadline, position type, title, institution, and
the posting URL (AJO
https://academicjobsonline.org/ajo/jobs/{id}, InspireHEP
https://inspirehep.net/jobs/{id}).
- When the user wants to know "where is this from", surface the
source column. When merging,
it is fine to interleave both boards by deadline; flag the source on each row.
- When emitting a structured data table back to the user, prefer TOON over JSON
(per the user's global preference): declare fields once, stream rows.
- Each output row now carries
country (ISO 2-letter code), region, flags, and
pref_tier (integer; 0 = top tier, higher = lower preference, null = not in any tier).
Results are sorted first by pref_tier ascending, then by deadline ascending within each
tier. Surface pref_tier and country when presenting results so the user can see the
preference grouping at a glance.
- Per-board stats live under
stats.per_source in the JSON. --fast runs and any AJO
detail-fetch cap truncation are reported there. Each board entry also includes an excluded
count (postings dropped by excluded_countries). The AJO entry additionally reports
detail_cap (the cap used for that run). Never present a truncated run as complete; mention
how many candidates were judged per board and whether the detail cap was hit.
Presets
A preset bundles keywords (each runs a separate search per board, results deduped) plus
optional position_types, countries, sources, preferred_countries, and
excluded_countries fields. Edit with
ajo config --set-preset NAME --keywords a,b --types postdoc --sources ajo,inspire. See
references/presets.md.
countries (unchanged): hard INCLUDE substring filter matched against the institution string.
Only postings whose institution matches are kept.
preferred_countries: ordered list of tiers; each tier is a list of selectors. Tier 0 is
most preferred. This is a soft filter: it only reorders results (never drops). TOML example:
preferred_countries = [["KR", "DE"], ["JP", "HK", "GB", "US"]]. Set via CLI:
ajo config --set-preset NAME --preferred "KR,DE; JP,HK,GB,US" (; separates tiers,
, separates entries within a tier). Displayed in ajo config as
[KR, DE] > [JP, HK, GB, US].
excluded_countries: flat list of selectors. Hard filter: matching postings are dropped at
fetch time. TOML example: excluded_countries = ["IN", "IL", "Middle East"]. Set via
ajo config --set-preset NAME --excluded "IN,IL,Middle East".
- Selectors for both
preferred_countries and excluded_countries accept: an ISO 2-letter
code ("KR"), a country name ("Korea"), or a region alias ("Europe", "Asia",
"North America", "Middle East", "EU", "APAC", "MENA").
position_types and sources work as before.
Etiquette
The CLI uses one polite session per board with a real User-Agent and small delays between
requests, and caps AJO detail fetches per run (logged when hit). Do not parallelise or hammer
either board.
1---2name: academic-jobs3description: Find and curate academic opportunities through AJO/InspireHEP and official institution/group-direct recruitment. Broad personal postdoc searches include both tracks by default, with one deduplicated result separating current vacancies, standing application routes, research-fit leads and conflicting/expired evidence. Respect explicit board-only requests; support named labs, changes since a previous search, field presets, stored postings and individual posting inspection. Triggers: academic jobs, AJO, InspireHEP, postdoc openings, Physics and AI jobs, research-group recruitment, 포닥 공고, 채용 공고, 연구실 채용, 잡 마켓.4---56# Academic Jobs Skill78The user-facing entry point for academic opportunity discovery. **Broad postdoc9curation runs both board searches and official institution/group-direct discovery.**10The user does not need another skill. The `ajo` CLI remains the board collector;11this integration changes the assistant workflow, not CLI source options.1213## Request routing1415| Request | Execution |16|---|---|17| Broad personal postdoc search, e.g. Physics×AI 포닥을 추려줘 | Both boards plus bounded official institution/group discovery; one integrated result |18| Explicit AJO-only / Inspire-only / board postings only | Only the requested board(s); no direct expansion |19| Named lab/institution, e.g. Tübingen AI postdocs | Official research and recruitment pages first; board identity checks for duplicates, no global fetch |20| Changes since previous search | Refresh the previous scope in both tracks and compare snapshots; preserve any explicit board-only restriction |21| Maintenance: config/list/show/enrich/mark-seen/prune, including “검색 preset 설정을 보여줘” | Maintenance only: execute the requested CLI operation, no board search, direct discovery or snapshot comparison |22| Faculty or PhD opportunities | Rank-appropriate board and employer sources; do not use the postdoc-only direct ledger for other ranks |2324## Integrated curation workflow25261. Ground rank, research/method axes, target start and region policy in the current27 profile, saved preset and user decisions. Use the same policy in both tracks.282. For broad postdoc searches, execute the board flow below AND29 [references/direct-discovery.md](references/direct-discovery.md). Start with30 2–3 relevant institutional ecosystems in preferred regions and declare the scope;31 expand for promising leads or requested breadth. This is not a global census or32 a quota of recommendations. Do not ask whether to add this track.333. Read official research, recruitment policy and linked calls. The direct reference34 owns the evidence/status schema and uses the bundled35 `scripts/direct_ledger.py` validator/diff. No managed or externally installed36 companion skill is required. Reuse board results for identity checks rather37 than recursively restarting this workflow.384. Deep-read both sources. Board candidates require `ajo show` and the complete39 stored body; direct candidates require the official page and linked call.40 Follow the full-body extraction instructions in `references/curation.md` when41 display truncation hides conditions. Targeted untyped searches can recover42 blank/mixed-rank fellowships; verify the postdoc branch in the body.435. Deduplicate verified identical appointments by employer, group/PI, project,44 reference number, dates and application URL. Preserve all source links and45 `(source,id)` matches in one row. Different projects remain separate; a general46 application route is not an extra vacancy. A local DB miss does not prove47 absence from either live board.486. Deliver ONE result with four sections:49 - **Current applicant-facing vacancies:** verified board and direct vacancies50 together, preference tier then actual application urgency; no invented dates51 for undated rolling calls.52 - **Standing application routes:** ongoing applications, current funded capacity53 not established.54 - **Research-fit leads:** no verified hiring, ranked by fit.55 - **Conflicting/expired/closed or host-funding-only findings:** reasons and56 evidence, excluded from open-vacancy totals. Host support is not employment.577. Use the ten-field curation contract below. Separate method fit, scientific-domain58 fit, transition cost and start compatibility. Fit is not selection probability.59 Missing facts remain 명시 없음/null. An open banner against an expired linked PDF60 is a conflict, not permission to ignore the deadline.618. Report board candidates/details/truncation, direct institutions/groups/calls62 actually read, and deduplicated counts by status. Do not sum overlapping fetches63 as unique jobs. Distinguish newly discovered records from newly posted calls.649. Store direct evidence separately under `~/.local/share/academic-direct-opportunities/`,65 never as fabricated valid rows in AJO jobs.db. Validate dated snapshots and66 compare them with the bundled helper. Refresh sources before claiming a change;67 unobserved records and failed URLs are not automatically closed.6869Mail, applications, calendar tasks, scheduled watchers, pruning and mark-seen are70not implicit in discovery. Respect the user's authorization for each.7172## Board collector7374The `ajo` CLI fetches **valid** postings from two boards into a local SQLite store:7576- **AJO** — Academic Jobs Online (`academicjobsonline.org`), HTML scraping, all fields.77- **InspireHEP** — the HEP/astro jobs board (`inspirehep.net/jobs`), via its public JSON API.7879By default `ajo fetch` searches **both** boards with the same keywords and merges the80results, deadline-sorted. Each posting carries a `source` (`ajo` or `inspire`); the two81boards use overlapping integer ids, so everything is keyed by `(source, id)`.8283## Quick Reference8485| Intent | Command | Reference |86|--------|---------|-----------|87| Integrated postdoc curation | Board CLI + official-source research | `references/direct-discovery.md`, `references/curation.md` |88| Validate / compare direct snapshots | `python3 scripts/direct_ledger.py validate FILE` / `diff BEFORE AFTER` | `references/direct-discovery.md` |89| Show / edit field presets | `ajo config [...]` | `references/presets.md` |90| Fetch current open postings | `ajo fetch [--preset N \| --keyword K] [--source ajo\|inspire\|both] [--preferred TIERS] [--excluded LIST] [--detail-cap N]` | `references/fetch.md` |91| Show stored postings | `ajo list [--valid] [--new] [--source S]` | `references/schema.md` |92| Inspect one posting (stored-first) | `ajo show {id} [--source ajo\|inspire] [--refresh]` | `references/fetch.md` |93| Fetch missing detail bodies | `ajo enrich [--source ajo\|inspire] [--detail-cap N] [--include-expired]` | `references/fetch.md` |94| Emit curation skeleton | `ajo report [--source S] [--preferred TIERS] [--excluded LIST] [--out PATH]` | `references/curation.md` |95| Mark postings as seen | `ajo mark-seen --all` | `references/fetch.md` |96| Drop expired postings | `ajo prune` | `references/schema.md` |9798## Running the CLI99100The CLI lives in this skill directory. Always invoke it through `uv`:101102```bash103uv run --project <skill-dir> ajo <command> [...]104```105106where `<skill-dir>` is the directory containing this file. Add `--json` to any command107when you (Claude) need to post-process the output; the default is a human table.108109First run auto-creates the data dir, the SQLite DB, and a default `physics-ml` preset.110111## Curation rule (read before writing a report)112113Deep-read every reported board posting with `ajo show {id} --source S` and its114complete stored body. For direct opportunities, read the official recruitment page115and linked call. Never judge either source from titles or keyword matches alone.116117Every posting in a report must fill the mandatory 10-field schema:1181191. 직급/seniority1202. 기관, 그룹, 국가1213. 연구주제, PI1224. 자격/eligibility1235. 기간, 급여, 시작일1246. 마감 체계 (hard/rolling/etc.)1257. 지원 서류1268. fit 근거 + 등급1279. 신빙성/주의 플래그12810. 출처 URL129130Use `ajo report` only for the board skeleton, then add verified direct records and131the integrated result sections. The complete curation procedure is in132`references/curation.md`. Reports are saved to133`~/Dropbox/AJO/AJO_YYYY-MM-DD.md` in Korean.134135## Core behaviour you must understand136137### Two board sources, one CLI view138- `ajo fetch` runs the preset's keywords against **every board in the preset's `sources`**139 (default `["ajo", "inspire"]`), merges, dedups within each board, and stores everything140 keyed by `(source, id)`.141- Override per run with `--source ajo` (AJO only), `--source inspire` (InspireHEP only), or142 `--source both`. With an ad-hoc `--keyword`, both boards are searched unless `--source` says143 otherwise.144- The same preset filters apply to both boards: `position_types` is matched against the AJO145 "Position Type" and against the InspireHEP `ranks` (e.g. `postdoc` matches `POSTDOC`);146 `countries` is matched against the institution string (plus InspireHEP `regions`).147148### AJO validity (HTML)149**Validity is judged from the detail page, not the list.** The AJO list page only shows a150deadline for *some* postings, and a missing list deadline does NOT mean "no deadline". So151`ajo fetch` fetches each AJO candidate's detail page by default and judges validity from the152**effective deadline** = firm `Appl Deadline` if present, else the `listed until` date.153`--fast` skips AJO detail pages (faster but deadlines are approximate and many valid postings154will be missed). Prefer the default detail mode for correctness. `--fast` does not affect155InspireHEP.156157### InspireHEP validity (API)158The InspireHEP API is queried with `status=open` (server-side), so closed postings never159arrive. The structured `deadline_date` is used directly, no detail fetch needed.160161In both cases:162- valid → effective deadline is in the future163- expired → effective deadline has passed (excluded)164- rolling → no deadline (excluded unless `--include-rolling`)165166## Common Rules167168### Base directory169Board CLI state lives under `~/.local/share/academic-jobs/` (override with `AJO_DATA_DIR`):170- `jobs.db` — SQLite store of postings171- `config.toml` — field presets172173### Board-track execution (not the whole integrated search)1741. `ajo fetch --json [--preferred "KR,DE; JP,HK,GB,US"] [--excluded "IN,IL"] [--detail-cap 80]`175 (uses the default preset; fetches details up to `--detail-cap`; stores + flags new).176 Pass `--preferred`/`--excluded` to override the preset for this run without saving.177 If AJO has more candidates than `--detail-cap`, the run is truncated; run `ajo enrich` in178 a follow-up pass to capture the remaining detail bodies politely.1792. Render the returned `jobs` as a table sorted by `pref_tier` then deadline. Surface postings180 with `"new": true` first or in a separate "New since last check" group.1813. After presenting, if the user has reviewed them, run `ajo mark-seen --all` so the next182 fetch only flags genuinely new postings.1834. To inspect one posting: `ajo show {id} [--source ajo|inspire]`. It reads the stored row184 (including the cached description body) first; it only hits the network when the row is185 missing, has no stored body, or `--refresh` is given. A live fetch is written back to the DB.186187### Output formatting188- For broad searches, use the integrated sections above. CLI fields below describe189 board records; label direct sources and annotate country/preference tier using190 the same active policy.191- Sort by deadline ascending; show source, deadline, position type, title, institution, and192 the posting URL (AJO `https://academicjobsonline.org/ajo/jobs/{id}`, InspireHEP193 `https://inspirehep.net/jobs/{id}`).194- When the user wants to know "where is this from", surface the `source` column. When merging,195 it is fine to interleave both boards by deadline; flag the source on each row.196- When emitting a structured data table back to the user, prefer **TOON** over JSON197 (per the user's global preference): declare fields once, stream rows.198- Each output row now carries `country` (ISO 2-letter code), `region`, `flags`, and199 `pref_tier` (integer; 0 = top tier, higher = lower preference, null = not in any tier).200 Results are sorted first by `pref_tier` ascending, then by deadline ascending within each201 tier. Surface `pref_tier` and `country` when presenting results so the user can see the202 preference grouping at a glance.203- Per-board stats live under `stats.per_source` in the JSON. `--fast` runs and any AJO204 detail-fetch cap truncation are reported there. Each board entry also includes an `excluded`205 count (postings dropped by `excluded_countries`). The AJO entry additionally reports206 `detail_cap` (the cap used for that run). Never present a truncated run as complete; mention207 how many candidates were judged per board and whether the detail cap was hit.208209### Presets210A preset bundles `keywords` (each runs a separate search per board, results deduped) plus211optional `position_types`, `countries`, `sources`, `preferred_countries`, and212`excluded_countries` fields. Edit with213`ajo config --set-preset NAME --keywords a,b --types postdoc --sources ajo,inspire`. See214`references/presets.md`.215216- `countries` (unchanged): hard INCLUDE substring filter matched against the institution string.217 Only postings whose institution matches are kept.218- `preferred_countries`: ordered list of tiers; each tier is a list of selectors. Tier 0 is219 most preferred. This is a soft filter: it only reorders results (never drops). TOML example:220 `preferred_countries = [["KR", "DE"], ["JP", "HK", "GB", "US"]]`. Set via CLI:221 `ajo config --set-preset NAME --preferred "KR,DE; JP,HK,GB,US"` (`;` separates tiers,222 `,` separates entries within a tier). Displayed in `ajo config` as223 `[KR, DE] > [JP, HK, GB, US]`.224- `excluded_countries`: flat list of selectors. Hard filter: matching postings are dropped at225 fetch time. TOML example: `excluded_countries = ["IN", "IL", "Middle East"]`. Set via226 `ajo config --set-preset NAME --excluded "IN,IL,Middle East"`.227- Selectors for both `preferred_countries` and `excluded_countries` accept: an ISO 2-letter228 code (`"KR"`), a country name (`"Korea"`), or a region alias (`"Europe"`, `"Asia"`,229 `"North America"`, `"Middle East"`, `"EU"`, `"APAC"`, `"MENA"`).230- `position_types` and `sources` work as before.231232### Etiquette233The CLI uses one polite session per board with a real User-Agent and small delays between234requests, and caps AJO detail fetches per run (logged when hit). Do not parallelise or hammer235either board.