Greenhouse, Lever & Ashby Jobs Scraper + Changes
Pricing
from $1.50 / 1,000 company checkeds
Greenhouse, Lever & Ashby Jobs Scraper + Changes
Get open jobs from companies on Greenhouse, Lever or Ashby and what changed since your last run: new and closed jobs, new jobs by function, new countries. $2 per 1,000 companies checked + $3 per 1,000 changed companies + $1 per 1,000 job rows. Not affiliated with Greenhouse, Lever or Ashby.
Pricing
from $1.50 / 1,000 company checkeds
Rating
0.0
(0)
Developer
Sumitsubo
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
9 days ago
Last modified
Categories
Share
Get the open jobs of any company that uses Greenhouse, Lever or Ashby — and what changed since your last run. Paste domains or careers URLs: the ATS is detected for you.
Every run returns one row per company: open jobs, new and closed roles since the last run, hiring growth, counts by function (Engineering, Sales, Marketing…), department and location, plus first jobs in new countries. By default you also get job rows: on the first run up to 50 open jobs per company (the most recently posted), then one row per new job.
Even the first run shows momentum: jobs opened in the last 7/30/90 days. $2 per 1,000 companies checked + $1 per 1,000 job rows ($3 per 1,000 changed companies; misses free). Not affiliated with Greenhouse, Lever or Ashby.
Try it in one minute
Press Start with the default input. It checks three public job boards (Airbnb on Greenhouse, Palantir on Lever, Ramp on Ashby) and returns three company rows like this:
| company | ats | openTotal | engineeringOpen | salesOpen | postedLast30d | longOpenCount | summary |
|---|---|---|---|---|---|---|---|
| Airbnb | greenhouse | 160 | 38 | 25 | 63 | 43 | Baseline: 160 open roles (Engineering 38, Sales 25, Customer Success 21). 63 posted in the last 30 days, 43 open for 90+ days. |
(Numbers from a test run on 2026-10-11; yours will differ.)
On this first run you also get up to 50 job rows per company, the most recently posted open jobs of each board (First-run job rows per company, default 50). The three boards list more than 50 open jobs each (160, 309 and 165 on 2026-10-11), so that is 3 company checks + 150 job rows = $0.006 + $0.150 ≈ $0.16; the jobs over the limit are counted in the company row (baselineJobRowsOmitted). Set Job rows to none for the three company rows only ($0.006).
Run it again later with the same Watchlist name and every row tells you what changed: +7 (9 new, 2 closed), New: Engineering 7, Sales 2, First job in DE, New department: AI Platform.
Who it is for
| You are | Look at |
|---|---|
| SDR / RevOps / founder doing outbound (Clay, HubSpot, n8n) | salesOpen, marketingOpen, gtmOpen, newJobs[] (titles + links as a reason to reach out), signalTags (gtm_push, hiring_up) |
| Investor / analyst | openTotal, growth, delta, trend12 (last 12 runs), newCountries, hiring_down, suspectedFetchAnomaly |
| Competitive intelligence | newDepartments, newLocations, newCountries, countsBy.department, weekly Slack digest |
| Recruiting agency | hiring_up, longOpenCount / long_open_heavy (roles that stay open), newJobs[], job rows |
Input
- Companies — one per line: a domain (
stripe.com), a careers page (https://stripe.com/jobs) or a board URL (https://jobs.lever.co/palantir,https://boards.greenhouse.io/airbnb,https://job-boards.greenhouse.io/embed/job_app?for=airbnb,https://jobs.ashbyhq.com/ramp, Lever EUhttps://jobs.eu.lever.co/...). Up to 5,000. Lines that are exactly the same are merged (the log says how many). Greenhouse EU boards (job-boards.eu.greenhouse.io) are not supported yet and are returned asunsupported_url(orboard_not_foundwhen a careers page links one) with that reason. Domains and careers URLs on ports other than 80/443 are not accepted. - Watchlist name (
stateKey) — changes are computed against the previous run with the same name. Use several names for several lists (every name is kept apart, including non-English names). One run per watchlist at a time: a second run started while the first is still working waits for it (up to 4 minutes, nothing is charged while waiting) and then runs; if the watchlist is still busy after that, it stops without charging. Starting runs in parallel (Clay, n8n, several schedules): give each parallel run its own watchlist name. - Job rows (
outputJobs) —new(default: on the first run of a board one row per open job up to First-run job rows per company, the most recently posted first (change: "baseline"), then one row per job that is new since the last run; $0.001 each),all(every open job, every run, no limit) ornone(company rows only, no job-row charges). A first run therefore costs up to 50 job rows per company at the default: 30 companies with 50 or more open jobs each ≈ $0.06 in company checks + $1.50 in job rows = ~$1.56. Changing filters starts a new baseline, which outputs the open jobs again (up to the same limit). - First-run job rows per company (
baselineJobRowsPerCompany) — default 50, 0–1,000. WithoutputJobs: "new"the first run of a board (and a new baseline after the filters change) outputs at most this many job rows for it, the most recently posted first; 0 outputs none. The jobs over the limit are not output (and not charged): the company row counts them inbaselineJobRowsOmittedandstatusDetailsays so. The baseline itself saves every open job, so none of them is reported as new later. Not used withall. - Count breakdowns (
groupBy) — any offunction,department,location,country,team(other values are ignored with a warning in the log). - Filters — track only some jobs:
titleIncludes(whole words; Japanese, Chinese, Korean and Thai terms match inside words, soエンジニアfindsシニアエンジニア),functions,countries(ISO codes such asUS,GB,DE;UK,USAand English country names are converted),departmentIncludes. Values that cannot be read are ignored with a warning in the log. With acountriesfilter, jobs whose location names no recognisable country (countrynull, e.g. "Remote - EMEA") are left out. Changing filters starts a new baseline. - Overrides — fix detection by hand:
[{"company": "acme.com", "ats": "lever", "token": "acme", "label": "Acme"}].companycan be written as the domain or as a URL of it (https://www.acme.com/careersandacme.commatch each other). An override that matches no line of Companies is reported in the log. - Digest — Slack Incoming Webhook URL and/or any public HTTPS webhook URL (both stored as secrets), and whether to send only when something changed. Webhooks to private or internal addresses, URLs with a user name or password, and redirects are refused.
- Domains detected per run (
maxDetectionsPerRun, Advanced) — default 1,000, at most 5,000. A domain without a saved detection costs up to 2 careers pages (the careers URL you gave and/careers, or/careersand/jobs) and up to 6 board-name guesses (free). Past this many in one run the rest aredetection_deferred(free, no request) and later runs detect them; saved detections do not count, board URLs and overrides never do. - Spreadsheet-safe text (
spreadsheetSafeText, Advanced) — off by default. Turn on if you open the dataset in Excel or Google Sheets: text that starts with=,+,-or@(job titles, departments and locations are written by each employer) gets a leading apostrophe, so it is shown as text instead of being run as a formula.
Output
Company row (one per input, every run)
| Field | Meaning |
|---|---|
company, input, domain, ats, token, boardUrl | Who and where (company is filled on every row: the company or board name, else the domain, else the input line) |
status, statusDetail | baseline (first run), ok, board_not_found, needs_review, fetch_failed, detection_deferred, anomaly, ats_switched, too_large, unsupported_url, duplicate — see below; statusDetail says why |
atsDetectedBy, confidence, detectionNote | How the board was found: url, careers-page, slug-probe, override (plus (cached)) |
openTotal, prevTotal, delta, deltaPct | Open jobs the board lists now, what it listed last run, difference. Jobs missing for the first time are not in openTotal (they are in missingOnceCount) |
listedTotal, missingOnceCount | Jobs the board lists right now (the same as openTotal); jobs listed last run that are missing this run for the first time — closed only if they are still missing on the next run (see "Two misses to close") |
prevOpenEstimate | Jobs open at the previous check as far as anyone can tell: listed then, plus those missing once then. The base of hiring_up / hiring_down (one incomplete answer does not shrink it) |
newJobsCount, closedJobsCount, replacedCount, repostedCount | New jobs since last run; jobs closed this run (missing two runs in a row — each close is counted once, on the run that confirms it); jobs missing for the first time in the same run as new jobs appeared, each offsetting one new job (a replacement); reposts: a role closed and re-posted with the same title and location within 14 calendar days counts as reposted, not new — also when the close and the re-post fall in different runs |
closedOffsetCount, returnedReplacedCount | Of the closes, those that had already offset a new job (not counted again); jobs back with the same id that had offset a new job — they were never gone, so the new job they offset counts as growth now |
growth, changed | Hiring growth: new jobs minus replaced jobs minus confirmed closes that no new job offset, plus returnedReplacedCount (other returned jobs, reposts and filter moves do not count). One turnover — as many roles filled as posted — is growth 0. changed is true when the company counts (and is billed) as changed in this run: new jobs, confirmed closes that no earlier change already covered, or jobs back that had offset new ones (see Pricing) |
returnedCount, movedIntoFilterCount, movedOutOfFilterCount | Jobs back with the same id after being missing or closed (not new); jobs that entered or left your filters while staying on the board (e.g. a renamed department; not new, not closed) |
prevCheckedAt, daysSincePrev | When this board was last read successfully, and how many days ago |
changeWithheld | true when the board changed but the run's spending limit had no room for "Company changed": the row shows the current counts only (the change fields are empty) and the next run with room reports the change — see Pricing |
baselineJobRowsOmitted | First run with outputJobs: "new": open jobs not sent as job rows because of First-run job rows per company (0 when all were sent; empty on other runs) |
atsSwitchedTo, schemaWarnings | The new board when status is ats_switched; notes about the feed (e.g. duplicate job ids counted once) |
engineeringOpen … otherOpen, engineeringDelta … | Listed jobs per function and the difference from what was listed last run (10 functions: Engineering, Product, Design, Data, Sales, Marketing, Customer Success, Operations, G&A, Other) |
gtmOpen, engShare, topGrowingFunction | Sales + Marketing + CS, engineering share, the function with the most new jobs this run |
newDepartments, newLocations, newCountries | Never seen at this company before (also when an existing job moves there). After more than 1,000 distinct values on one axis the oldest are forgotten and seenHistoryCapped is true |
postedLast7d, postedLast30d, postedLast90d, longOpenCount, longOpenShare | From posting dates — available from the first run (dates in the future are ignored; on Lever the date is when the posting was created) |
signalTags | hiring_up (growth of at least +3 jobs and +10% of prevOpenEstimate, from 5 such jobs), hiring_down (growth of at least −3 jobs and −20% of prevOpenEstimate, from 5), drop_confirmed, new_country, new_department, gtm_push, engineering_push, fresh_postings, long_open_heavy |
summary | One sentence, rule-based (no AI): "164 open (+4 vs last run): 9 new, 5 closed. New: Engineering 7, Sales 2. Closed: G&A 3, Sales 2. First job in DE." — the functions of the new and the confirmed closed jobs |
countsBy | { "department": { "Payments": { "now": 12, "prev": 9, "delta": 3 } } } for each breakdown (listed counts) |
trend12 | Listed jobs over the last 12 runs |
newJobs[], closedJobs[] | Up to maxJobsInRow jobs each (default 20): title, function, department, location, country, link, posted date |
recentJobs[] | First run only: the most recently posted jobs, up to 10 |
Job row (on by default)
rowType: "job", company, ats, jobId, title, function, department, team, location, country, remote, publishedAt, url, change (new | open | baseline)
With the default outputJobs: "new" the first run of each board gives one job row per open job (change: "baseline"), at most baselineJobRowsPerCompany (default 50) per company, the most recently posted first; every later run gives one per new job (change: "new"). all gives every open job every run. Set none to get company rows only.
Run summary
The key-value store record OUTPUT holds the run summary: status counts, the 10 companies with the highest growth, detection (of the companies this run output: domains detected, domains deferred, and the limit), requests and bytes transferred, and the digest result (OUTPUT.digest.skippedReason says why no digest was sent). The run's status message shows the same counts and is updated after each group of (up to) 25 companies while the run works.
A run ends as failed when no company could be read at all (every one fetch_failed: ATS outage, blocking or network) or when another run kept the same watchlist busy for longer than the 4-minute wait; nothing is charged in these two cases (no event of this Actor, and there is no start fee). A run also ends as failed when it loses its watchlist lock while working: lock_lost (another run took the watchlist over) or lock_unconfirmed (the lock could not be read even after 3 tries: key-value store errors). These two can happen after earlier groups of companies were already output and charged: those charges stay (the rows were delivered), are recorded and are never billed again. The run then stops before outputting its next group — or, when the check right before saving fails, without saving the group it has just output and charged: the next run shows that group's changes again without charging them again. After a stop like this the run writes no shared state any more (a lock it could not remove expires after 10 minutes). A failed run sends no digest. If only some companies fail, the run succeeds and the status message says how many could not be read.
Long lists and the time limit. Before the run's timeout (2 hours by default) the Actor stops between two groups of 25 companies, saves where it stopped and ends as succeeded; the status message says from which company the next run continues. Inside a group it reads no further company in the last 3 minutes, and a group whose reading ran into the last minute is not output (nothing charged; the next run reads it). Start it again with the same companies and it picks up from there. The same happens when the run stops at its spending limit or is aborted: the next run with the same companies continues from the first company that was not output. A run that gets to the end of the list clears the saved position, so the run after it starts at the top again. There is one saved position per watchlist (OUTPUT.nextIndex shows it); a run of another list on the same watchlist that gets to its end leaves it in place. The position is also saved after every group, so a run that the platform ends anyway (for example while a very slow ATS answers) still leaves it, and requests to an ATS never wait into the last minute before the time limit (a board that has not answered by then is fetch_failed, free). If the platform moves (restarts) a run, the restarted run never outputs a second row for a company the first attempt already output, even when that company's result is different the second time, and also when the restart comes after the first attempt finished (it then outputs, charges and sends nothing). A change whose company row the first attempt may not have output is not charged by the restart; the next run shows it and charges it once. The first run on domains is the slowest because each domain's ATS is detected (up to 2 careers pages, then up to 6 board-name guesses): plan roughly 3 minutes per 100 domains at the default "Companies in parallel" = 4 when most domains have no board, about half that at 10 — an estimate from a simulated run, not a measurement on Apify. At most 1,000 domains are detected per run (maxDetectionsPerRun); the rest are detection_deferred and detected by the next runs. Board URLs need no detection, and detection results are cached, so later runs are much faster.
Monitoring: schedule it
- Save your input as a Task, with a Watchlist name such as
prospects-q4. - Add a Schedule — weekly (
0 7 * * 1) or daily. - Optional: paste a Slack Incoming Webhook URL to get a digest (fastest-growing companies, first jobs in new countries, new departments, sharp declines).
The digest is sent at the end of each run, so a weekly schedule gives a weekly digest. Failed sends never fail the run and are not charged. With "Only send when something changed" (the default) the digest is sent when at least one company changed, or got its first job in a new country or a new department; the first run of a watchlist only saves baselines, so the first digest then arrives after the second run. A run in which no company could be read sends no digest. A digest is only sent when the run's spending limit still has room for "Digest sent"; otherwise it is not sent (never delivered for free) and OUTPUT.digest.skippedReason says so. A restart of the same run does not send it a second time. "Growing" and "Sharp declines" are ranked by growth, and each line shows the parts that add up to it (new, replaced, closed, back); "Sharp declines" lists exactly the companies tagged hiring_down, so a turnover at a constant count is listed under other changes, not as growth or decline.
Pricing
Pay per event. Prices below are the Apify Free plan prices (the most you pay; paid Apify plans may get lower prices — the Pricing tab shows yours).
| Event | Price | When |
|---|---|---|
| Company checked | $0.002 | A board was read successfully (ok or baseline) |
| Company changed | $0.003 | From the second run, when new jobs were posted, jobs were confirmed closed that no earlier change already covered, or jobs that had offset new ones came back (changed: true). One turnover — some roles filled and as many posted — is one change, on the run that sees the new roles; the closes confirmed on the next run are not charged again |
| Job row | $0.001 | One row per job — on by default (outputJobs: "new"): the open jobs on a board's first run (and after the filters change), at most baselineJobRowsPerCompany (default 50) per company, then one per new job; every open job with all; never with none |
| Digest sent | $0.01 | A Slack or webhook digest was delivered (it is only sent when the spending limit has room for this event) |
There is no Actor start fee: nothing is charged when a run starts.
Free: detection, cached lookups, board_not_found, needs_review, fetch_failed, detection_deferred, anomaly, ats_switched, too_large, unsupported_url, duplicates, and failed digests.
| Example (Free-plan prices) | Assumptions | Approx. per month |
|---|---|---|
| First run of 30 companies | 50 or more open jobs each = 1,500 job rows (default outputJobs: "new", at most 50 per company) | ~$1.56 once |
| 30 companies, weekly, digest | 4.3 runs; 15% of companies change per run; 2 new-job rows per changed company | ~$0.40 |
| 300 companies, weekly, digest | same | ~$3.59 |
| 2,000 companies, daily | 30 runs = 60,000 checks; 9,000 changes; 21,500 new-job rows | ~$169 |
The monthly examples are after the first run. Your cost depends mostly on how many companies change and how many new jobs they post; set outputJobs to none to pay for company rows only.
Set a maximum cost per run in the run options to cap spending; the Actor stops cleanly at the limit, the companies it did not reach are not charged, and the next run with the same companies continues where it stopped. Each group of companies is planned before anything is output: every company read is charged as checked, then changes in input order. A company whose change no longer fits is output with its current counts and changeWithheld: true — what changed (new and closed jobs, deltas, growth, tags, summary) is not shown and no job rows are sent for it — and the next run with room reports that change and charges it once. Job rows are planned in the same step, before anything is output: a company whose job rows do not all fit says so in statusDetail (baseline waits: 4 of 50 job rows sent; the rest next run on a first run, changed since last run; 2 of 3 job rows sent, the rest next run on a change) and the status message counts them (N job rows wait for budget). The next run sends (and charges) the rest — rows already paid are sent again without a charge — and a board's first run is saved once all its rows are paid. OUTPUT.safety.changesWithheld counts the withheld changes, OUTPUT.safety.jobRowsWaiting the waiting job rows.
No false alarms
- Anomaly guard — if a board that had 20+ jobs suddenly lists none, or fewer than 20% of its previous count, the row is marked
anomaly, the previous snapshot is kept and nothing is charged. Jobs being replaced (high turnover, a long pause between runs) is not an anomaly: only the drop in the count is. A drop is accepted as real after 3 such runs in a row (the counts may differ), or at once when the previous check is 28+ days old and the board still lists some jobs: the row isokwithdropConfirmed: trueand the tagshiring_down+drop_confirmed, the closed jobs are reported (and charged as a change) and the digest lists it under sharp declines. A drop accepted at 0 jobs setsstaleBoard: true. - Two misses to close — a job that is missing for the first time is not reported as closed: it is shown in
missingOnceCount(it is no longer inopenTotal, which is what the board lists) and closed only if it is still missing on the next run. One incomplete answer from an ATS therefore reports no closures, nohiring_downand no change, and the jobs that come back arereturnedCount, not new (also after a real close, when the same job id is back within 14 days, or at the next check after the close however long after — e.g. a board that recovers after a drop accepted after a long pause). This also applies to small boards that return an empty list once.hiring_up/hiring_downare measured againstprevOpenEstimate(listed plus missing once at the previous check), so a run right after an incomplete answer is not measured against the shrunken count. - Replacements — when new jobs appear in the same run as others disappear, each disappearance offsets one new job at once (
replacedCount). One turnover at a constant count therefore showsdelta0,growth0 and nohiring_up, is one change (billed once), and the close confirmed on the next run is counted inclosedJobsCount(andclosedOffsetCount) but is not a second change and nohiring_down. A disappearance that no new job offsets counts when it is confirmed. If a job that offset a new one comes back instead (it was only missing once), the offset is taken back: that run'sgrowthrises by one per such job (returnedReplacedCount), so real growth that coincided with an incomplete answer is not lost. The same holds after a long pause between runs. - ATS switches — when a domain's board suddenly looks gone (404, newly empty, or a sudden drop) and the board came from a cached detection, the careers page is checked again. If it now links another board, the row is
ats_switched(free, not a drop) and the next run starts the new board with a baseline. Board URLs and overrides are taken as given. - Incomplete responses — if the ATS answers but the answer is broken (departments missing on Greenhouse, more than 10% of postings without an id or title), the company is
fetch_failed: nothing is reported as closed and nothing is charged. A careers page that cannot be read (timeouts, connection errors, server errors, "too many requests" on every page tried) is not taken as "no board": the domain isfetch_failed(free) and checked again on the next run. - Reposts — the same title and location closed and re-opened within 14 calendar days are not counted as new, also across runs. (If two runs are more than 14 days apart, a close and a re-post between them cannot be dated and count as closed + new.)
- Long-open roles — jobs posted more than 90 days ago are counted separately (
longOpenCount), so evergreen postings do not look like growth. - Same-name check — when a domain is matched by guessing a board name, the board is accepted only if its job links point to that domain (Greenhouse, at least half of the links) or the postings write that domain as a web or e-mail address (Lever, Ashby; at least 30% of postings). A careers subdomain such as
careers.acme.comis checked againstacme.com. A matching company name, a similar domain (notacme.com) or a capitalised word is never enough. Otherwise you getneeds_review(free) and can confirm it with Overrides. Guessed boards (and "not found" / "needs review" results) are re-checked after 7½ days, i.e. every second run on a weekly schedule. On Lever the guess first reads a sample of 25 postings, so a board of another company is not downloaded in full. - Duplicate job ids — the same job id listed twice in one feed counts once (one job row, one charge);
schemaWarningssays so.
Status values
| status | Meaning | Charged |
|---|---|---|
baseline | First check of this board (or filters changed) | Company checked (+ job rows) |
ok | Compared with the last run | Company checked (+ changed, + job rows) |
board_not_found | No Greenhouse/Lever/Ashby board for this domain, or the board returned 404 | No |
needs_review | A board with this name exists but may belong to another company, or the careers page links several boards | No |
fetch_failed | The ATS did not answer after 3 retries (or asked to wait more than 30 seconds), or answered with an incomplete list, or the careers page of a domain could not be read; snapshot kept | No |
detection_deferred | This run already detected maxDetectionsPerRun domains (default 1,000); detected by a later run | No |
anomaly | Sudden drop that looks like a data problem; snapshot kept | No |
ats_switched | The careers page now links another board; tracked from the next run | No |
too_large | The board's saved state would not fit in one key-value store record (tens of thousands of jobs); counts shown, changes not tracked — use filters to track a part of it | No |
unsupported_url | Not a domain or a supported board URL | No |
duplicate | Same board as an earlier line | No |
Scope and limits
- Greenhouse, Lever and Ashby only. Workday, LinkedIn and Indeed are not supported.
- Input must be a domain or URL; company names alone are not supported.
- Companies with several boards (regions, subsidiaries) need one line per board.
- Lever EU boards (
jobs.eu.lever.co) are found from a board URL or the careers page; guessing from the domain only tries the global Lever host. - Greenhouse EU boards (
job-boards.eu.greenhouse.io) are not supported yet (only the global Greenhouse job board API is read). - At most 1,000 domains without a saved detection are detected per run (
maxDetectionsPerRun, up to 5,000): a list of 5,000 new domains takes 5 runs to be fully detected at the default (the rest aredetection_deferred, free). Detection itself is free and costs the operator requests, which is why it is capped. - Countries come from the ATS's country field or from the location text (best effort: "Berlin, DE", "Austin, TX", "Remote (FR)"). A location that names no country, or a code that could mean two places ("Dublin, CA"), stays
null. After "Remote" a two-letter code counts only when it has one meaning: "Remote, TX" / "Remote, WA" are US (state codes only), "Remote (FR)" is France, but "Remote, CA" (California or Canada), "Remote, CO" (Colorado or Colombia) and "Remote (DE)" (Delaware or Germany) staynull. A location that lists several places ("Austin, TX; San Francisco, CA", "New York, NY | London", "Chicago, IL, Denver, CO") gets a country only when all of them are in the same one; otherwisenull. A list of countries in one place ("Remote - US, Canada", "Remote (DE, AT, CH)", "France, Spain") isnulltoo. Australian states: "Perth, WA", "Darwin, NT" and "Adelaide, SA" are Australia (WA / NT / SA only after an Australian city; "Vancouver, WA" stays US), NSW / VIC / QLD / TAS / ACT always. - Job functions are read from titles and departments written in English. Titles in other languages (e.g. Japanese 「ソフトウェアエンジニア」) mostly end up as
Other; usetitleIncludeswith your own terms to track them. - Greenhouse boards whose job links stay on greenhouse.io cannot be confirmed from a guess: paste the board URL or add an override (most companies link their board from their careers page, which is found automatically).
- Jobs are told apart by their id. When an ATS gives some jobs new ids for one run and the old ids come back the run after, that is read as real turnover: e.g. 10 jobs with new ids for one run show 10 new jobs replacing 10 (growth 0), then growth +10 on the run the old ids are back (10 back that had been counted as replaced) and −10 on the next run, when the 10 temporary ids are confirmed closed. Over the three runs the total is 0, but two of them show +10 and −10 (and can be tagged
hiring_up/hiring_down). - Job descriptions are never returned or stored — only titles, departments, locations, dates and links.
- No personal data: recruiter or hiring-manager names and custom fields are never read into the output.
- Data comes from each ATS's public job board API (no login). Not affiliated with Greenhouse, Lever or Ashby.
FAQ
How do I find a company's board name? Open their careers page and click a job: the link contains boards.greenhouse.io/<name>, jobs.lever.co/<name> or jobs.ashbyhq.com/<name>. Paste that URL.
A company shows needs_review. We found a board with a matching name but could not confirm it is theirs. If it is, add an override: {"company": "acme.com", "ats": "greenhouse", "token": "acme"}.
A company shows board_not_found. It does not use one of the three supported ATSs, or its careers page does not link the board. Paste the board URL if you have it.
Can I export to CSV / Clay / HubSpot? Yes — the dataset exports to CSV, JSON or Excel. The main numbers are plain columns (openTotal, growth, delta, engineeringOpen …); countsBy, trend12, newJobs[], closedJobs[] and recentJobs[] are nested (objects / lists), which CSV export flattens into many columns. Company rows and job rows share one dataset: the "Companies" view picks the company columns but does not remove job rows, so filter on rowType = company (or run with outputJobs: "none") when you want exactly one row per company. Opening the export in a spreadsheet? Turn on Spreadsheet-safe text (spreadsheetSafeText) so employer-written text that starts with =, +, - or @ cannot run as a formula.
Two runs on the same watchlist? Only one works at a time; the other waits for it up to 4 minutes (charging nothing), then runs — or, if the watchlist is still busy, stops without charging and says so in its status message. Use a different watchlist name for each run you start in parallel.
How it compares
| Per-job scrapers | Monthly hiring-data SaaS | This Actor | |
|---|---|---|---|
| You choose the companies | Yes | Search their database | Yes (domains or URLs) |
| What changed since last run | Usually not, or new jobs only | Yes | New and closed jobs, hiring growth, new jobs by function, counts by function / department / country |
| Billing | Per job, every run | Monthly credits | Per company and per job row (up to 50 jobs per company once, then new ones); changes cost extra; misses free |
| First run useful | Raw jobs | Yes | Yes (up to 50 job rows per company + 7/30/90-day posting counts of every open job) |
Looking for a TheirStack or PredictLeads alternative for a known list of companies on Greenhouse, Lever or Ashby? This Actor is built for exactly that list-based monitoring.