Greenhouse, Lever, Ashby, Workable, Workday & Recruitee
Pricing
from $1.50 / 1,000 jobs
Greenhouse, Lever, Ashby, Workable, Workday & Recruitee
Collect and filter current job listings from Greenhouse, Lever, Ashby, Workable, Workday and Recruitee career pages. Export normalized titles, locations, descriptions, salary, remote status, posting dates, and application links.
Pricing
from $1.50 / 1,000 jobs
Rating
0.0
(0)
Developer
Mohammad Salman Farsi Khan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Share
Greenhouse, Lever, Ashby, Workable, Workday & Recruitee Jobs Scraper
Collect open roles from public company career pages and normalize them into a searchable dataset. This career page scraper fetches job listings from six supported applicant tracking systems and can inspect an ordinary company website for a linked board as a best-effort fallback.
Search for a greenhouse jobs api, lever jobs scraper, ashby jobs, workable jobs, or workday careers: this Actor covers all six with one normalized output.
What it does
- Fetches public job listings from 6 supported ATS platforms: Greenhouse, Lever, Ashby, Workable, Workday and Recruitee.
- Detects the board from a direct career URL, or checks up to three company website pages for a supported career-board link (best effort).
- Tries all supported boards when you enter only a company name.
- Normalizes titles, locations, departments, employment types, remote status, salary, dates, descriptions, and application links.
- Adds rule-based seniority, job category, country code, and days-since-posted fields without AI calls.
- Supports filters for title keywords, location text, remote-only jobs, and posting age.
- Writes job rows to the dataset. Per-company summaries and run totals are stored separately under
SUMMARYin the key-value store. - Assigns stable job IDs and removes duplicates within a run.
Who it is for
- Recruiters collecting roles across career pages.
- Sales teams tracking hiring as a company signal.
- Job boards gathering and standardizing public job listings.
Supported boards
| Board | Example public career URL |
|---|---|
| Greenhouse | https://boards.greenhouse.io/stripe |
| Lever | https://jobs.lever.co/aircall |
| Ashby | https://jobs.ashbyhq.com/openai |
| Workable | https://apply.workable.com/getresponse |
| Workday | https://illumina.wd1.myworkdayjobs.com/illumina-careers |
| Recruitee | https://natilik.recruitee.com |
This Actor is not affiliated with, endorsed by, or sponsored by the ATS vendors whose boards it supports. It reads public career-board pages and Workday's internal JSON endpoints only.
How to use it in 3 steps
- Enter direct career-board URLs, ordinary company website URLs, or company names in the career pages list. A normal Workday careers URL such as
https://illumina.wd1.myworkdayjobs.com/illumina-careersis enough: the tenant, thewdserver and the site are read from the URL. The oldertenant|wd1|site|localepipe form still works. - Set an optional job limit or filters.
maxJobsPerBoard: 0means no limit. - Start the Actor and review or export the job rows in the dataset.
Input example
{"companies": ["https://boards.greenhouse.io/stripe","https://jobs.lever.co/aircall","https://jobs.ashbyhq.com/openai","https://ramp.com"],"maxJobsPerBoard": 20,"titleKeyword": "Engineer","remoteOnly": false}
You can also enter a company name such as Stripe. Use locationContains, postedWithinDays, or includeSalaryDetails to refine the run. Salary details are enabled by default, and includeRaw is off by default.
Output fields
Every dataset row is one job. Fields are additive: a field is populated only when the source actually provides the value, otherwise it is empty or null.
| Field | Type | Description |
|---|---|---|
| id | string | Stable job identifier used for deduplication within a run. |
| company | string | Board slug, for example ramp. |
| company_name | string or null | Human-readable company name where the source or input provides one. For Greenhouse this is the response company_name when present; otherwise it falls back to the name typed in companies, or the slug. Null for Workday when no name is available. |
| title | string | Job title. |
| location | string | Human location string from the source. |
| is_remote | boolean or null | Remote marking from the source board, as implemented. Can disagree with workplace_type. |
| department | string | Department or team from the source. |
| salary_text | string | Salary text from the source, where provided. |
| posted_at | string | ISO posting date, where available. |
| posted_at_source | string or null | How posted_at was obtained: exact for an exact source date, estimated for a Workday relative label, unknown for a Workday Posted 30+ Days Ago label with no detail where posted_at is null. |
| job_url | string | Job link. The public posting URL, or null for a Workday posting with no public link. |
| seniority | string | Rule-based seniority from the title. Empty when no level is recognized. |
| job_category | string | Rule-based job category from the title. |
| country_code | string | Country code resolved from location/source data. |
| days_since_posted | integer or null | Days from posted_at to the run date. Empty when posted_at is empty. |
| description_text | string or null | Plain text from the job description, where the adapter provides one. Null for boards without a description and for Workday postings fetched without detail. |
| description_html | string or null | Raw HTML job description from the source, where the adapter provides one. Null when the source has no description. |
| employment_type | string or null | Source employment type where provided, for example full-time or contract. Null when the source does not expose it. |
| workplace_type | string or null | Source workplace label, for example remote, hybrid, or on-site, where the board exposes one. Null when unknown. |
description_text, description_html, employment_type, posted_at_source, workplace_type and company_name are populated only where the code already has the value. They are null when the source does not provide the value.
The dataset holds job rows only. The per-company summary and run totals are stored in the run's key-value store under SUMMARY. Each job row is one billable job event; the SUMMARY record and per-company/board state records are not billed as job events.
Pricing
$1.50 per 1,000 jobs. See the Pricing tab for current rates. Each job row is one billable job event. Company summaries and run totals in SUMMARY are not charged as job events.
When a run is on the Apify free plan and is not the configured owner account, the Actor still collects and normalizes jobs the same way, but it stops pushing job rows after the first 50 billable job rows for that run. That cap applies to pushed and billed job rows only, not to the number of postings the Actor may find, filter, or summarize internally. The cap is only on Apify's free plan; it is not a feature of the scraper itself and is not applied outside the free plan. For current limits and pricing, see the Pricing tab.
FAQ
Can I enter an ordinary company website?
Yes, as a best-effort fallback. The Actor checks the supplied page and up to two more career-page locations, for at most three page fetches per company. It follows linked career pages and looks for links or embedded scripts that point to a supported board. If it finds no board, it logs the supported URL formats and continues the run.
What happens when I enter only a company name?
The Actor tries the name against supported boards that use company identifiers. A Workday careers URL carries the tenant and site, so enter it directly or provide the ordinary company website that links to it. A direct career-board URL is the clearest input when a company uses more than one board.
Will every job have salary and a posting date?
Only when the board provides the field. The Actor leaves unavailable values empty. Greenhouse dates, departments, offices, and descriptions come from the main jobs response. When includeSalaryDetails is true, capped parallel detail requests are used for Greenhouse pay ranges only; turn it off to skip those requests.
What is different about Workday rows?
Workday detail fetching is on by default through the includeWorkdayDetail input (default true). When it is on, every Workday posting gets one detail request, and detailMax does not limit those requests. The detail record contributes:
description_htmlfrom the detailjobDescriptiondescription_textfrom the same HTML via the plain-text routineposted_atfrom the detailstartDate, withposted_at_source = "exact"employment_typefrom the detailtimeTypejob_urlfrom the detailexternalUrlwhen present, otherwise from the listing path
When includeWorkdayDetail is off, or when a detail request fails after retries, the row is still kept. In that case description_text, description_html, and employment_type may be empty, and posted_at may come only from the listing's relative label or be null. A relative label such as Posted 30+ Days Ago has no day count, so posted_at stays null and posted_at_source is unknown for that posting when no detail is available.
Workday listings can exceed the verified 2,000-result cap on a single list query. When the offset-0 total is at or above 2,000, the Actor splits the board by the first usable facet and fetches each slice separately. If the unique postings found equal the sum of that facet's counts in the base response, the board is complete. If they do not, the board is incomplete, and SUMMARY records both numbers. A slice that still reaches the cap is capped, and a posting with no public link is still emitted with job_url null.
How are the derived fields decided?
They use fixed keyword and location rules, with no AI calls. Seniority is assigned only when a recognized level appears in the title. Executive-level seniority is reserved for C-level, VP, President, and Head of titles. Job category uses title keywords and falls back to other when a title is present but no listed category matches. Country codes come from country data, major city names, or an unambiguous location value. Days since posted is calculated from the ISO posting date.
How accurate are the derived fields?
They are not yet measured in this version. A labelled fixture file exists for the owner to fill; until it has rows and a test prints a result, the Actor does not publish an accuracy number.
How are remote status and workplace type related?
is_remote follows the source board's remote marking as implemented. workplace_type is the source's raw label. The two can disagree. For example, an Ashby posting can show workplaceType "Hybrid" with isRemote true when a secondary location is remote. The remoteOnly filter uses is_remote, not workplace_type.
Where are run summaries stored?
The dataset contains job rows only. The per-company summary and run totals are stored in the run's key-value store under SUMMARY.
Workday terms and robots.txt
Workday's site terms (https://www.workday.com/en-us/legal/site-terms.html) prohibit scraping and bypassing robots.txt. Workday's End User Agreement, section 4.3(c), prohibits scraping Website content. This Actor calls Workday's internal /wday/cxs/ JSON endpoints, which are not a documented public API. Before each Workday list or detail request the Actor fetches robots.txt once per host with Accept: text/plain, */* and checks the URL against the parsed rules using the same User-Agent it sends. Status 404 means allowed; status 200 is parsed; any other status stops that board and reports it. The Actor also blocks any path containing /refreshFacet/.
The Actor does not use a browser-like User-Agent and does not add proxy rotation, fingerprint spoofing, CAPTCHA solving, or headless browsers to get past blocks. If a Workday board is blocked, the Actor stops that board and reports it. Users are responsible for complying with Workday's terms.
Coverage
The supported-boards table above lists the boards this version handles. Field coverage is
measured, never written by hand: every local run writes column_fill_*.csv under
runs/reports/, with one row per board and the filled/total count for each output field.
The current numbers are in “Measured output coverage” under Live results above.
Watch mode
Watch mode is not enabled in this version. When it is enabled later, set onlyChanges to true and the Actor will keep state in a named key-value store called job-tracker-state. In watch mode, only new and changed rows are emitted and billed; unchanged rows are neither emitted nor charged. The first run emits and charges every posting. Changes to descriptions and dates are not tracked in the first watch-mode version.
Live results
Measured runs, recorded as they were executed (Python 3.14.7, no Apify SDK installed). Reproduce any of them with the command shown.
2026-10-13 — offline run, five boards, no Apify
Command: python tools/run_local.py --input examples/sample_actor_input.json --max 10
Result: 5/5 boards returned data — greenhouse/stripe 722 postings, lever/aircall 77,
ashby/openai 800, workable/getresponse 7, recruitee/natilik 6; 43 rows written for
maxJobsPerBoard: 10; 15 HTTP requests; 8.4 s. Writes runs/reports/join_*.json, one
CSV per platform, and column_fill_*.csv.
2026-10-13 — the same input through the Actor code path
Command: python -m src.main with INPUT.json in the Actor folder. With no Apify SDK
installed this uses the local runtime and writes dataset.jsonl and summary.json.
Result: boards_found 5, boards_failed 0, jobs_found 1612, jobs_pushed 43,
rows_filtered_out 0, rows_without_link 0, elapsed_seconds 8.0. The 43 dataset rows
carry all 25 columns, including company_name and posted_at_source.
2026-10-13 — Workday detail against a live tenant
Input:
{"companies": ["https://illumina.wd1.myworkdayjobs.com/illumina-careers"], "maxJobsPerBoard": 3, "includeWorkdayDetail": true}Result: 165 unique postings listed. Every listed row came back with description_text,
description_html, employment_type, an exact posted_at from the detail record, and a
resolvable job_url. The board is reported incomplete because its unique posting count
is below the sum of the base facet counts; that is the documented completeness rule, not a
request failure.
2026-10-13 — per-ATS smoke test
Command: python tools/smoke_test.py — RESULT: all four boards returned a sample job row.
Measured output coverage
From the offline run's column_fill_*.csv (43 rows across five boards, share of rows with
a non-empty value): id, title, company, location, job_url, apply_url,
posted_at, description_text and description_html 100%; employment_type 76.7%;
is_remote 58.1%; workplace_type 53.5%; salary_currency 34.9%; salary_min,
salary_max and salary_text 30.2%; office 23.3%. A field stays empty when the source
does not publish it.
Not yet measured
- NVIDIA Workday list-only run,
includeWorkdayDetail: false - A run on Apify itself, to confirm the platform entrypoint and billing events
- Derived-field accuracy against the labelled fixture (see the accuracy caveat above)