ATS Job Watch — Greenhouse & Lever job change monitor avatar

ATS Job Watch — Greenhouse & Lever job change monitor

Pricing

from $2.00 / 1,000 board checkeds

Go to Apify Store
ATS Job Watch — Greenhouse & Lever job change monitor

ATS Job Watch — Greenhouse & Lever job change monitor

Monitor Greenhouse and Lever career boards through their public job-board APIs. By default each run returns only NEW, CHANGED and CLOSED postings since the previous run. No login, no proxies, no personal contact data.

Pricing

from $2.00 / 1,000 board checkeds

Rating

0.0

(0)

Developer

Yorozuya Neko Labs

Yorozuya Neko Labs

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Security

Limited permissions

Share

ATS Job Watch: Greenhouse & Lever job change monitor

ATS Job Watch checks company career boards hosted on Greenhouse and Lever through their public job-board APIs. In the default changes mode, each run returns only the postings that are NEW, CHANGED or CLOSED since the previous run of the same watchlist (full mode also returns unchanged jobs).

Put it on a schedule (for example once a day) to follow hiring at competitors, customers or companies you want to work for.

How it works / first run

  • The first run saves a baseline and reports no changes. It records the jobs that are open now, outputs one free BASELINE_SAVED row per board, and its status says: "First run: saved a baseline for N boards (M open jobs). No changes are reported on the first run. Schedule this Actor (e.g. daily) with the same companies and watchlistId, and the next runs will output NEW / CHANGED / CLOSED jobs."

  • Every later run compares the boards with the saved baseline and outputs only NEW, CHANGED and CLOSED jobs.

  • Want the current jobs right away? Turn on emitBaseline (Output current jobs on the first run): the first run then also outputs every open job as a BASELINE row, charged as snapshot-job.

  • Uses the public job-board endpoints that Greenhouse and Lever provide for publishing jobs. No login, no cookies, no proxies.

  • Recruiter names, e-mails and other contact fields are not output.

  • Ashby, SmartRecruiters and Recruitee are deliberately not supported (their API terms do not allow, or do not clearly allow, this kind of third-party use). Such inputs are reported as input errors and never requested. Workday and custom career pages are not supported either.

Quick start

  1. Add boards to Companies, for example greenhouse:gitlab, lever:palantir, or a board URL such as https://job-boards.greenhouse.io/gitlab or https://jobs.lever.co/palantir.
  2. Run once. By default (mode: changes), the first run saves a baseline and outputs no job rows — you get one free BASELINE_SAVED row per board and the status message says how many jobs were saved. (Want the current jobs too? Turn on Output current jobs on the first run. In mode: full the first run always outputs them, charged as snapshot rows.)
  3. Create a schedule (Console → Schedules) that runs a saved task daily. Keep the same companies and the same watchlistId. From the second run on you get only changes.

Example input

The smallest useful input (this is also the default, so an empty input runs the same two boards):

{
"companies": ["greenhouse:gitlab", "lever:palantir"]
}

A typical daily scheduled task — engineering roles only, interns excluded, remote or Tokyo, with a fixed watchlist name so history is kept when you add boards later:

{
"companies": ["greenhouse:gitlab", "lever:palantir"],
"mode": "changes",
"titleKeywords": ["engineer", "developer"],
"excludeTitleKeywords": ["intern"],
"locations": ["Remote", "Tokyo"],
"watchlistId": "daily-engineering-watch"
}

Input

FieldDescription
companies (required)greenhouse:<slug>, lever:<slug> or a public board URL. Default when omitted: greenhouse:gitlab, lever:palantir. Up to 500 entries, 300 characters each. A board that does not exist (404) is charged as a board check. EU Lever boards (jobs.eu.lever.co/<slug>) are read from Lever's EU API (api.eu.lever.co) and appear as board <slug>@eu. EU-hosted Greenhouse boards (*.eu.greenhouse.io) are not supported yet: Greenhouse documents no EU job-board API host, so they are reported as a free input error and never requested or charged.
modechanges (default): only NEW / CHANGED / CLOSED. full: also every open job as UNCHANGED. In full mode a board's first check always outputs its current jobs as BASELINE (charged as snapshot rows), whatever emitBaseline is. Only jobs that match your filters are output and charged (the counts in OUTPUT are before filtering).
emitBaselineDefault off. On: in changes mode the first run also outputs current jobs as BASELINE (charged as snapshot rows). (full mode always does.)
emitStatusRowsDefault on. Writes free status rows (BASELINE_SAVED, NO_CHANGES, RUN_SKIPPED) so a run where boards were checked but nothing changed still leaves one row. A NO_CHANGES row is written only when at least one board was checked successfully and no other row was written; if every board failed (or the input had only unsupported ATSs), the dataset can be empty — see OUTPUT for the reason.
titleKeywords / excludeTitleKeywordsTitle must / must not contain (case-insensitive). Up to 100 entries, 200 characters each.
locations / remoteOnlyLocation substring filter (up to 100 entries, 200 characters each), remote jobs only.
includeDescriptionAdds the job description as plain text (at most 50,000 characters per job, then [description truncated]; at most 200,000 characters of source text are read per field).
escapeFormulasDefault on: text values that look like a spreadsheet formula get a leading ' so spreadsheets don't run them (see Spreadsheet exports). Ordinary text such as - Bullet point or -Remote is not changed. Turn off for the exact original text.
watchlistIdName of the saved history the changes are compared against. Derived from the company list if empty; set it explicitly for scheduled tasks.
maxJobsPerBoard / delayBetweenBoardsSecsSafety limit per board; wait between boards (minimum and default 1 s, maximum 5 s).

Output

One dataset row per job change, plus free status rows. The run summary (per-board counts, errors) is in the OUTPUT record of the default key-value store.

changeTypeMeaningCharged
NEW / CHANGED / CLOSEDChange since the previous run1 × change-event
BASELINECurrent job on a board's first check, only with emitBaseline on or in mode: full. Never written after lost history1 × snapshot-job
UNCHANGEDUnchanged job, only in mode: full1 × snapshot-job
BASELINE_SAVEDStatus row: first check of a board (changes mode, emitBaseline off), baseline saved, no job rowsfree
BASELINE_RESETStatus row: the saved history for this board was missing or unreadable; a new baseline was saved and existing jobs were not re-sent (no NEW, BASELINE or UNCHANGED rows for that board in that run, in any mode)free
NO_CHANGESStatus row: at least one board was checked and nothing changed (boards that could not be checked are counted in boardsFailed)free
RUN_SKIPPEDStatus row: another run of the same watchlist was still in progress, so this run checked nothingfree

Inputs over these limits (more than 500 companies, more than 100 entries in a filter list, a longer entry, or a non-text entry) are rejected before anything is checked: the run fails with an explanation and nothing is charged by this Actor (only Apify's standard start event).

Job rows contain: jobKey, ats, board, company, jobId, title, department, team, locations, remote, workplaceType, employmentType, salary (min, max, currency, interval, raw), postedAt, updatedAt, url, applyUrl, firstSeenAt, checkedAt, isBaseline, contentHash, optional description. Status rows have jobKey: null.

Example (real public postings, change simulated):

{"changeType": "CHANGED", "jobKey": "greenhouse:gitlab:8716179002", "ats": "greenhouse", "board": "gitlab",
"company": "GitLab", "title": "AI Transformation Owner, Product & Design (Senior)", "department": "AI",
"locations": ["Remote, Canada; Remote, United Kingdom; Remote, United States"], "remote": true,
"url": "https://job-boards.greenhouse.io/gitlab/jobs/8716179002", "firstSeenAt": "2026-10-31T08:42:28Z",
"checkedAt": "2026-10-31T08:42:30Z", "isBaseline": false}
{"changeType": "NO_CHANGES", "jobKey": null, "boardsChecked": 2, "boardsFailed": 0, "checkedAt": "2026-10-31T08:42:33Z"}

Spreadsheet exports (CSV / Excel)

Job fields are text written by the companies that publish the postings. With escapeFormulas on (default), a text value that starts with = or @, a tab or a carriage return, or starts with + / - followed by a digit, (, =, +, -, @, . or a function-call shape such as -SUM( is written with a leading apostrophe (for example =HYPERLINK(...) becomes '=HYPERLINK(...) and -SUM(1,2) becomes '-SUM(1,2)), so Excel, LibreOffice and Google Sheets show it as text instead of running it as a formula. Ordinary text is left alone: - Bullet point, -Remote, + benefits and Senior - Platform are not changed. This applies to title, company, department, team, locations, workplaceType, employmentType, salary.raw / currency / interval, url, applyUrl, description and jobId. All other values are unchanged. Change detection and contentHash always use the original text, so switching the option never creates CHANGED rows. If you read the JSON directly and need the exact text, turn the option off (or remove one leading ' from these fields). Even with escaping on, treat exported job text as untrusted content.

Pricing (pay per event, public beta)

EventPrice
Board checked (board-checked) — one board the ATS API answered, including a board that does not exist (404)$0.002
Change event (change-event) — one NEW / CHANGED / CLOSED row$0.001
Snapshot row (snapshot-job) — BASELINE / UNCHANGED, only with emitBaseline on or mode: full (including the first full run). Never for lost history$0.001
Actor start (Apify's standard start event)$0.00005 per run
  • A board check is charged whenever the ATS API answered, including boards with 0 jobs, boards where nothing changed, and boards that do not exist (404) — a real check was made. 404 board checks count toward your max total charge.
  • Not charged: network errors, timeouts, server errors (5xx), 429 Too Many Requests, redirects (3xx, never followed), responses over 32 MB, boards that hit the 90 s per-board limit, unexpected responses, inputs rejected for size limits, EU-hosted Greenhouse URLs, boards not checked because of a stop below, unsupported-ATS inputs, skipped runs, and all status rows (BASELINE_SAVED, BASELINE_RESET, NO_CHANGES, RUN_SKIPPED).
  • Lost history: a board whose saved history was lost or unreadable outputs no job rows in that run, in any mode, so it costs only its board check ($0.002) — no change or snapshot charges.
  • Each row is charged at most once. There is no additional per-dataset-item charge.
  • The run stops at your max total charge and never exceeds it. Changes that did not fit are delivered on the next run and are not charged twice.
  • Examples: 10 boards checked daily with no changes ≈ $0.02 per day; 50 boards ≈ $0.10 per day, plus $0.001 per change.

How changes are detected, and when one can be missed

  • A job is identified by its ATS job ID. CHANGED means one of: title, department, team, locations, remote, workplace type, employment type, salary, URL.
  • Partial or failed responses never produce CLOSED. If a board errors, returns an unexpected shape, or is cut off (job limit reached, or fewer jobs than Greenhouse's reported total), its saved history is kept and no closures are reported for that run.
  • Sudden large drops are confirmed first. If a board loses at least 3 jobs and falls below 80% of its previous count, CLOSED rows are held back until the next run confirms the drop (reported one run later).
  • Lost or unreadable history: existing jobs are never re-sent — not as NEW, and also not as BASELINE or UNCHANGED in mode: full or with emitBaseline on. You get one free BASELINE_RESET row per affected board, and no change or snapshot events are charged for it. From the next run on, the board behaves normally (in mode: full, one snapshot-job per open job again). Changes that happened between the last good run and the reset are not reported.
  • Deleting the state store (ats-job-watch-state) or changing watchlistId starts a new watchlist. It behaves like a first run (BASELINE_SAVED rows and no job rows by default; with mode: full or emitBaseline on, current jobs are output as BASELINE and charged as snapshot-job). Changes in that gap are not reported.
  • A job that appears and disappears between two runs is never seen. Run more often if that matters.
  • Lever does not report a total job count. If Lever returns a well-formed but shortened list on two runs in a row, it cannot be told apart from real removals.
  • A failed board is retried on the next run; the history from the last good check is used for comparison.

Safety limits

  • One run per watchlist at a time. A lock (lease of 300 s, renewed at the start of a board when more than 60 s have passed since the last renewal) is kept in the state store. A second run that overlaps exits without checking or charging anything and writes a free RUN_SKIPPED row. The lock is a best-effort lease, not an atomic lock (the key-value store has no compare-and-swap): a run writes it, waits 1 s and reads it back. In our tests with the production settings, overlapping runs did not report or charge the same change twice, but a duplicate is still possible in rare cases, e.g. when a run takes more than the 1 s wait between reading the lock and writing it, when the store returns a stale value on the read-back, or when one board's processing outlasts the lease window (the lease is renewed only when a board starts, after 60 s; since 0.1.11 fetching one board is limited to 90 s, so this needs a very large board). The lease of a crashed run expires after at most 300 s; until then, runs of that watchlist are skipped (free).
  • 20 failed boards in a row (including 404s) stop the run; the remaining boards are not checked and not charged (OUTPUT.stoppedEarly).
  • 429 Too Many Requests: each board is retried at most twice. If 3 boards in a row on the same ATS end in 429, that ATS is not called again in this run (those boards are not charged).
  • Requests: only boards-api.greenhouse.io, api.lever.co and api.eu.lever.co over HTTPS. Redirects are not followed (a 3xx answer is a failed, uncharged check). One response may be at most 32 MB (decoded); one board, including retries and waits, at most 90 s. A board over either limit is a failed check and is not charged.
  • OUTPUT.stats lists requests, retries, 429s, 5xx responses, network errors, 404 boards, stopped ATSs and, when they happen, refused redirects, oversized responses and boards over the time limit.
  • Delay between boards: 1–5 s. Memory: 256 MB by default, 1024 MB at most.
  • On an unexpected internal error the run fails with the error type only (no stack trace). Boards checked and rows output before that were charged once and are not charged again.

Testing and limits

  • Tested on labeled synthetic scenarios (9 boards × 29 runs, 4 random seeds, including failures, truncation, lost and corrupted history, budget stops and crashes): 0 wrong NEW / CHANGED / CLOSED rows, and charged change events equal output change rows.
  • Real boards: cloud runs on 28 boards (about 4,400 jobs) and a side-by-side check of 3 other Actors on the same boards — 3 products × 2 runs only. That does not prove long-term reliability, and no real job changes happened during that check.
  • Salary is filled only when the ATS provides it in structured form (Lever) or the posting states an explicit amount range. Nothing is guessed.
  • Please do not schedule more often than needed (every minute is not recommended). Job content belongs to the posting companies; check their terms before redistributing it.

Support

Report problems in the Issues tab. Please include the run ID and the board (for example greenhouse:gitlab).