Skip to content

feat(discovery): Lever, Ashby, and Workday ATS adapters - #32

Merged
Harikeshav-R merged 5 commits into
mainfrom
feat/more-ats-adapters
Aug 5, 2026
Merged

feat(discovery): Lever, Ashby, and Workday ATS adapters#32
Harikeshav-R merged 5 commits into
mainfrom
feat/more-ats-adapters

Conversation

@Harikeshav-R

Copy link
Copy Markdown
Owner

What & why

Broadens Phase-2 discovery from Greenhouse-only to all four ATS providers the roadmap
names
— Lever, Ashby, and Workday (STATUS.md "Next up" #1, PROJECT.md §5.4-A) — so the
watchlist and daemon cover far more companies. Each is a new adapter dropped onto the existing
atlas.discovery.ats registry; the poller, watchlist service, and atlas company add are
already generic over the ats_type string, so no downstream code changed. detect(url)
accepts both the public board URL and the raw API URL for each provider.

Key commits

  1. feat(scrape): support POST + JSON body on the Fetcher seam — the one shared change: optional,
    GET-defaulted method / json_body / headers on Fetcher/BrowserFetcher/default_fetcher
    so Workday can POST over the same boundary. Backward compatible (existing GET callers
    unchanged); the test FakeFetcher records them and can replay a page sequence.
  2. feat(ats): add lever board adapterGET api.lever.co/v0/postings/<site>?mode=json, a raw
    JSON array
    ; maps id/text/hostedUrl/categories/workplaceType/descriptionPlain/createdAt.
  3. feat(ats): add ashby board adapterGET api.ashbyhq.com/posting-api/job-board/<name>; Ashby
    jobs carry no id, so the external id is derived from the job URL, and unlisted postings are
    skipped.
  4. feat(ats): add workday board adapter — the per-tenant CxS API, a POST that paginates;
    a compound <tenant>:<wd>:<site> board reference (detect emits it, list_postings parses it),
    offset paging to a _MAX_PAGES cap with a warning if capped (no silent truncation).
  5. docs: record lever/ashby/workday ATS discovery — STATUS / PROJECT §15 / CHANGELOG / README.

Each adapter's registry entry lands in its own commit (a module not in _ADAPTERS is dead code),
keeping every commit self-contained and green.

Testing

Fully hermetic (AGENTS.md §6.2): inline recorded-JSON fixtures replayed through FakeFetcher
(no real network). Per-adapter detection tables (board + API URL → token; cross-provider + non-ATS
None), happy-path normalization, empty board, malformed JSON, missing/wrong array,
skipped-malformed-job, and FetchError propagation — plus each adapter's quirks: Lever raw-array

  • description fallback, Ashby id-from-URL + isListed filter, and Workday POST body/headers,
    pagination (offsets 0/20), cap warning (caplog), empty-page early stop, malformed-board-ref /
    unexpected-shape errors, and JR-id parsing. Gates green: ruff format --check, ruff check,
    mypy --strict (incl. --platform win32), and 959 tests at 100% line+branch coverage. No
    new dependency (httpx + stdlib).

Design notes for review

  • Fetcher extension was the one structural change; kept backward-compatible with defaulted
    kwargs, so the two existing GET call sites and the poller's local sequenced fake needed only a
    signature widening (no behavior change).
  • Workday's compound board_ref (<tenant>:<wd>:<site>) is how a single registry string
    carries the tenant + datacenter + site the CxS endpoint needs; atlas company list renders it
    literally (a --name overrides the display).
  • Known limitations (documented, with clean follow-ups): Lever polls the US base only (EU-only
    boards may 404 — encode region in board_ref); Workday apply URLs omit any locale segment (some
    tenants 404 — carry an optional locale in board_ref).
  • smartrecruiters replaced lever as the "still-unknown provider" in the registry/poller
    negative tests once Lever registered.

Extend the Fetcher/BrowserFetcher protocols and default_fetcher with three
optional, GET-defaulted keyword params — method, json_body, headers — so an ATS
adapter can issue a JSON POST (Workday's CxS API) over the same seam. Backward
compatible by construction: the existing GET call sites pass none of these and
are unchanged, and runtime_checkable checks method presence not signature.
default_fetcher gains an httpx.request branch (merging headers over the default
User-Agent) and stays # pragma: no cover.

The test FakeFetcher now records method/json_body/headers on each FetchCall and
accepts an optional results sequence (replayed in order, raising when exhausted)
so the paginated Workday poll can script one page per offset — the fake carries
the whole test load. Full suite green at 100% line+branch, mypy --strict clean
(incl. win32).
The second ATS discovery source (PROJECT.md §5.4-A): Lever's public Postings
API. A drop-in on the existing registry — the poller/service/CLI are generic
over the ats_type string, so only the adapter + one _ADAPTERS entry are new.

- LeverAdapter.detect recognizes both the public board URL (jobs.lever.co/<site>,
  jobs.eu.lever.co) and the raw API URL (api.lever.co/v0/postings/<site>,
  api.eu.lever.co) — token = the site.
- list_postings hits GET api.lever.co/v0/postings/<site>?mode=json, which returns
  a RAW JSON ARRAY (so the shape guard is 'not a list' rather than a missing
  'jobs' key), and maps id/text/hostedUrl(or applyUrl)/categories/workplaceType/
  descriptionPlain(or stripped HTML)/createdAt onto a DiscoveredPosting, skipping
  malformed postings and propagating FetchError.

Registered in _ADAPTERS (ATS_TYPES auto-derives to greenhouse+lever). The
registry and poller tests that hard-coded 'lever' as the unknown provider now
use 'smartrecruiters' (documented but unregistered). Tests mirror the Greenhouse
suite; 100% line+branch, mypy --strict clean (incl. win32).

Known limitation: list_postings always polls the US base; an EU-only board may
404 — encoding the region in board_ref is a follow-up.
The third ATS discovery source (PROJECT.md §5.4-A): Ashby's public Job Posting
API — another drop-in on the registry.

- AshbyAdapter.detect recognizes the public board URL (jobs.ashbyhq.com/<name>)
  and the raw API URL (api.ashbyhq.com/posting-api/job-board/<name>).
- list_postings hits GET api.ashbyhq.com/posting-api/job-board/<name>?includeCompensation=false
  ({apiVersion, jobs: [...]}), and maps title/location/employmentType/workplaceType/
  descriptionPlain(or stripped descriptionHtml)/publishedAt onto a DiscoveredPosting.
- Ashby jobs carry NO top-level id, so the external id is derived from jobUrl's
  last path segment (a UUID), falling back to applyUrl — isolated in a helper with
  its own branch coverage. Unlisted postings (isListed false) are skipped; apply_url
  prefers applyUrl then jobUrl.

Registered in _ADAPTERS (ATS_TYPES → ashby+greenhouse+lever); registry test
extended. Tests mirror the Greenhouse suite plus the id-derivation and
isListed-filter branches. 100% line+branch, mypy --strict clean (incl. win32).
The fourth ATS discovery source (PROJECT.md §5.4-A): Workday's per-tenant CxS
API — the first adapter to use the POST Fetcher seam.

- WorkdayAdapter.detect recognizes the public board URL
  (<tenant>.<wdN>.myworkdayjobs.com/<locale?>/<site>) and the raw CxS API URL
  (.../wday/cxs/<tenant>/<site>/jobs), validating the wd-datacenter label and
  skipping a leading locale segment. Because the registry stores one board_ref
  string but Workday needs three values, detect emits a compound
  '<tenant>:<wd>:<site>' token that list_postings parses back apart (guarding the
  part count).
- list_postings POSTs {appliedFacets, limit, offset, searchText} with
  Accept: application/json and paginates offset by _LIMIT until len >= total, an
  empty page, or a _MAX_PAGES cap (≤200) — logging a warning when capped (no
  silent truncation, AGENTS.md). Maps title/externalPath/locationsText/postedOn,
  deriving external_id from the trailing _JR id and the apply URL from the base +
  externalPath.

Registered in _ADAPTERS (ATS_TYPES → ashby+greenhouse+lever+workday). Tests cover
detection (board + CxS URL + rejections), the POST body/headers, pagination
(offsets 0,20), the cap warning (caplog), empty-page early stop, malformed
board_ref / non-JSON / unexpected-shape errors, JR-id parsing, and skipped
malformed jobs. 100% line+branch, mypy --strict clean (incl. win32).

Known limitation: the apply URL omits any locale; a tenant that requires one may
404 — carrying an optional locale in board_ref is a follow-up.
Update the docs for the three new ATS adapters (Definition of Done, AGENTS.md §8):

- STATUS.md: bump 'Last updated' + 'Current phase'; add the 'What has landed'
  entry for the Lever/Ashby/Workday adapters + Fetcher POST extension; drop the
  'More ATS adapters' item from 'Next up' (add small ATS follow-ups) and re-order
  the rest (aggregators, multiple profiles, IPC); tick the Phase-2 table row.
- PROJECT.md §15: check off the company-watchlist + ATS-adapters item (all four).
- CHANGELOG.md (Unreleased/Added): the three adapters + the Fetcher POST seam.
- README.md: list all four supported providers (board + API URL forms).
@Harikeshav-R
Harikeshav-R merged commit 96ab958 into main Aug 5, 2026
10 checks passed
@Harikeshav-R
Harikeshav-R deleted the feat/more-ats-adapters branch August 5, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant