# Changelog of Lever Jobs Scraper (`humble-echidna/lever-jobs`) Actor

- **URL**: https://apify.com/humble-echidna/lever-jobs/changelog.md
- **Full Actor documentation**: https://apify.com/humble-echidna/lever-jobs.md

## Changelog

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.

### 1.1.1 (2026-09-26)

- 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.

### 1.1.0 (2026-09-25)

- 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`.

### 1.0.1 (2026-09-24)

- 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.

### 1.0.0 (2026-09-24)

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.
