Versions follow MAJOR.MINOR.PATCH (src/version.py); Apify shows MAJOR.MINOR from .actor/actor.json.
Every run logs its version and records it in the RUN_STATS key-value record.
About future failures: this scraper reads Lever's public job-board API. Lever can change it without
notice. If a company that used to work starts failing, or RUN_STATS shows skippedRecords, suspect a
format change first, not a regression in this code. The run log names the company and says what didn't parse.
One broken company never affects the others in a run.
- Input descriptions written for AI agents (Apify's MCP server shows values, not option titles, and no
limits): option values, ranges and defaults are now in the text.
- Big boards are read again: a board's whole-job feed may now be up to 32 MB (was 10 MB, which turned away e.g.
OpenAI's Ashby board at 14.5 MB).
- Closed and changed jobs, as in Company Career Page Jobs Scraper (
ats-jobs) 1.1: a new option, Also report
closed and changed jobs (includeClosedAndChanged, off by default), turns a scheduled run into a job-change
feed. Besides new jobs, each run returns, once, every job it returned before that has since left the company's
Lever board (changeType: "closed", with its last known title, locations, department, employment type, salary
text and link, and closedDetectedAt) or whose title, locations, department, employment type or salary changed
(changeType: "changed", with changedFields old and new). New jobs are marked changeType: "new", and every row
has firstSeenAt. These rows are charged like any job; with the option off, output and charges are as before. The
option implies Only jobs new since my last run. A board that fails, is blocked, has records that don't parse,
or suddenly lists far fewer jobs gets no closed-job check that run (RUN_STATS.closureChecksHeld says why); a
sudden drop is reported only if the next run sees it too. Run counts by type are in RUN_STATS.changes. A memory
from an earlier version is read as a baseline, so nothing false is reported on the first run with the option.
- Three new filters, applied before you're charged, so a job they leave out is never charged: Departments
(department or team contains any of the words, case-insensitive), Employment types, and Minimum salary (the
top of the job's pay range, from Lever's own salary range, or else pay text in the description; per year, month,
week, day or hour, compared as a full-time year; currencies are never converted; jobs with no comparable pay kept
unless Keep jobs with no comparable salary is off). With a minimum salary, descriptions are read to find pay
even when Include full job description is off; they're still left out of the results. Each company's log line
and
RUN_STATS.boards[...].notes say how many jobs a filter left out or kept for lack of data.
- "Only jobs new since my last run" is remembered per search, the new filters included, so changing one starts a
fresh search and hides nothing. A saved task that doesn't use them keeps its memory.
- Job titles, departments, teams, locations, countries and company names are trimmed: leading, trailing and
repeated whitespace is removed.
- Dataset: new nullable fields
changeType, changedFields, closedDetectedAt, firstSeenAt, and a "Job changes"
view. RUN_STATS has two new keys, changes and closureChecksHeld.
- Shared code update: a pasted URL on a platform whose terms don't allow automated reading (Workday, Workable,
Breezy, Rippling, SmartRecruiters) is refused with that reason instead of a pointer to another actor. No change to
results for this actor's own platform.
First release. Lever-only edition of Company Career Page Jobs Scraper (ats-jobs), sharing its code
(common/mms_ats) and output format.
- Shared safety code (
mms_common, the same in every actor of this portfolio): robots.txt per RFC 9309 (byte-order
marks, product-token matching, percent-encoding, a 500 KiB cap counted while streaming), a Crawl-delay above 30 s
reports that site instead of stalling the run, Retry-After honoured (seconds or a date; above 30 s reported),
the private-network guard on every request and redirect hop (resolve-and-check, then connect to the checked
address; ports 80 and 443 only; IPv6 forms that wrap an address, 6to4 and site-local addresses refused), a time
limit per attempt and one for the whole request (redirects, retries and robots.txt included), and decompression in
small steps with a capped size. robots.txt rules are matched in linear time, so a hostile file can't stall a run;
a robots.txt cut at the size cap loses its partial last line, and its retries honour Retry-After. A malformed
address or redirect target is reported for that company instead of crashing the run.
- Company names: a guess that fails for robots.txt or network reasons (e.g. an unknown Personio name redirecting to
Personio's site, whose robots.txt answered 429) just means "not on that platform", with no retries and no
robots.txt message; an unknown name is answered in seconds.
- A careers domain that doesn't exist is reported as such, not as a robots.txt problem.
- Jobs reserved against the run's limit for a company whose job details robots.txt then disallows are given back to
the other companies in the run.
- Companies as a name (looked up on Lever only, and used only when the board's own company name agrees), a
Lever board URL or
lever:slug, or a careers page that links to a Lever board. Boards on other platforms are
refused with a pointer to ats-jobs; the rest of the run continues.
- Filters: title keywords, exclude keywords, locations, remote only, posted within N days, max jobs per company;
"only jobs new since my last run", remembered per company and filter combination in the user's own
lever-jobs-seen store.
- Charged per job returned ($2.00 per 1,000, plus $0.00005 per run start), through Apify's standard
apify-default-dataset-item event.
- "Max jobs per run" input, and the maximum cost per run is honoured: once either is covered, companies still
being fetched are cancelled and the rest are skipped (listed as
boardsSkipped in RUN_STATS).
- Failure isolation: a failing company, a malformed job record or a failed detail request only affects itself.
- A run where no company is on Lever (or none can be found) ends successfully with 0 jobs and a status
message saying so;
RUN_STATS (unresolved) and the log say why for each company. A malformed company line
(e.g. http://[bad) is reported for that line and never crashes the run.
- "Only jobs new since my last run" remembers a job only once it's in your dataset, so a run that stops early never
hides jobs from the next one.
RUN_STATS has the same keys on every exit path. No proxy input: Lever's public API needs none.
- Identifies itself honestly (User-Agent
HumbleEchidnaApify/1.0 (+https://apify.com/humble-echidna)) and
checks every host's robots.txt first, careers pages included: anything a site disallows is never requested and the
company is reported with the reason; Crawl-delay is respected.