# Changelog of LinkedIn Jobs Scraper — Job Postings, LinkedIn Salary (`afanasenko/linkedin-jobs-scraper`) Actor

- **URL**: https://apify.com/afanasenko/linkedin-jobs-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/afanasenko/linkedin-jobs-scraper.md

## Changelog

All notable changes to the LinkedIn Jobs Scraper are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### \[0.0.40] - 2026-09-24

#### Fixed

- **A request that runs into more than one kind of trouble in a row — a busy moment, a dropped
  connection, an answer that is not finished yet — no longer gets a fresh round of waiting each
  time.** Each job lookup and search page keeps one waiting budget from its first attempt to its
  last, so a stuck one is reported as not checked on time instead of after a second full round of
  waiting.

### \[0.0.39] - 2026-09-24

#### Changed

- **Docs:** the related-actors table links the new LinkedIn People Search — find employees and
  decision makers by job title, company, country and job function.

### \[0.0.38] - 2026-09-23

#### Fixed

- **When several of your runs start at the same moment, a job link now waits its turn** (up to a few
  minutes) instead of coming back empty.
- **A lookup that was accepted but never finished is now reported as not checked and not charged** —
  never as a closed posting or an empty search.

### \[0.0.37] - 2026-09-23

#### Fixed

- **A job link that could not be checked is no longer reported as a closed posting.** When the
  lookup for a link was turned away on every attempt, the run used to finish with the "no results"
  advice, which suggests the posting has probably closed. It now says the link could not be checked
  this time, that nothing was charged for it, and to re-run it in a few minutes. When only some of
  your links could not be checked, you get the jobs that were checked plus a line saying how many
  links to re-run.
- **A search that could not be run no longer reports that nothing matched.** When a job search, or
  the company lookup in Search Company Jobs, was turned away on every attempt, the run now says it
  could not be run this time and that nothing was charged.
- **If a run has to stop early because of a limit on our side, it now stops cleanly and keeps what
  it fetched.** It used to keep trying and then finish as if your input had found nothing. You are
  charged only for the jobs already in your dataset, and the message says your input was fine and
  that the same input will work when you run it again later.
- The "Welcome to the paid plan" message now appears only on your first paid run instead of on
  every paid run, and the check-in after your third paid run can now appear. The count starts with
  this version, so if you already ran this actor on a paid plan you see the welcome once more.

#### Changed

- `RUN_SUMMARY.stopReason` now says how a run that returned no jobs ended — `no_results`,
  `all_filtered` or `Targets temporarily unavailable` — where it used to say the run completed.
  `RUN_SUMMARY.skippedUnchecked` counts the job links that could not be checked this time.

### \[0.0.36] - 2026-09-10

#### Changed

- Clarified wording in the README, this changelog, the input form, the output column titles and
  the live status page description. Nothing changes in how the actor runs, what it returns or what
  it costs.

#### Fixed

- When a run fails for a reason on our side, the error message now stays short and plain.

### \[0.0.35] - 2026-09-07

#### Fixed

- **When a run stops early and explains why, the explanation is now in the run log instead of only
  in the Storage tab.** The log used to show one line — the heading of the message — and told you
  to open Storage to read the rest. The rest is where the useful part lives: which link shape Mode 2
  accepts, and that Mode 1 finds the same jobs from a title and a location. One customer pasted
  company-careers links 33 times over eight days and never saw it. Nothing had been charged for
  those runs, and nothing is charged now — the only thing that changes is that the answer arrives
  where you are already looking.

### \[0.0.34] - 2026-09-01

#### Fixed

- **A run that ran alongside your other runs could come back empty-handed, and now waits instead.**
  When several of your runs are working at the same time, requests start being turned
  away. The run used to try twice more, five and ten seconds apart, and then give up. A run now
  waits longer and up to five times, and each run picks its own waiting time rather than all of
  them counting to five together and arriving back at the same instant. If the response says how long
  to wait, that is what the run waits.
- The extra waiting is capped: at most 30 seconds at a time and 75 seconds in total for any one
  piece of data, after which the run moves on rather than holding your run open. **A busy run may
  therefore finish a little slower than before — it is waiting where it used to fail.**

### \[0.0.33] - 2026-08-26

#### Fixed

- **A run that named its own cause was being filed as a clean completion.** The zero-row
  fallback stop reason (`no_results`) overwrote whatever the mode branch had already set, so
  the off-LinkedIn run shipped in 0.0.32 explained itself in `USER_MESSAGE` and then recorded
  itself as `completed`. `no_results` now means what it says — nothing came out and nothing
  said why — and only replaces the untouched default.

### \[0.0.32] - 2026-08-26

#### Added

- **Mode 2 now tells you when a link is not a LinkedIn posting, by name.** A link on a
  company careers site (Workday, Lever, UltiPro, Oracle Cloud, Indeed, a company's own
  careers page…) is recognised before any lookup is made: the run skips it, charges nothing
  for it, and the new `not_linkedin_urls_v1` message quotes the host it saw and names the
  next step — find the same role on LinkedIn, or use Mode 1 (Search jobs) or Mode 3
  (Search company jobs). Previously such a run spent a lookup, returned nothing, and was
  handed a list of three possible causes to choose between.
- `RUN_SUMMARY` carries `skippedForeignHosts` — links skipped before any lookup. Kept apart
  from `skippedEmpty`, which counts links that were tried and came back empty: a dead
  LinkedIn posting and a careers-site URL are different outcomes with different fixes.
- A run whose links are all off-LinkedIn now reports a stop reason that says so, instead of
  being indistinguishable from a search that legitimately found nothing.
- A partially off-LinkedIn list is handled too: the LinkedIn links do their work and the
  rest are named in a line appended to whatever the run had to say.

#### Changed

- **The job id is now read correctly from the canonical LinkedIn URL form.** LinkedIn writes
  `…/jobs/view/<role-slug>-<id>`; the id was only recognised when it followed `/jobs/view/`
  immediately, so for those links a truncated URL was used in its place — in the run's target
  label and, when the fetched job data omitted the id, in the `job_urn` column of a withheld
  preview row. Both now carry the id.
- The "Job posting URLs" field says that links must be on linkedin.com and what to use
  instead when they are not.

### \[0.0.31] - 2026-08-25

#### Added

- **Two documented surfaces that already shipped but were never written down.**
  `📡 Live status & storage records` names the live status page and the three key-value
  records any run writes (`RUN_SUMMARY`, `USER_MESSAGE`, `FREE_LIMITS_APPLIED`), so an
  unattended pipeline can branch on them instead of parsing the log.
  `🤖 Programmatic / API use` shows the actual calls: run-and-collect in one request,
  fire-and-poll for large company sweeps, reading a run's records, scheduling via a Task,
  and webhooks.
- A `🧭 When to use it` section, with every bullet naming a field or filter the Actor
  already ships.
- An opening paragraph saying what LinkedIn's own job APIs do not give you, and what this
  Actor returns instead.

#### Changed

- **The detail-column count was wrong on every surface that stated it.** The README claimed
  "36 detail fields" in three places and the input schema said "36 fields per row"; the
  Actor returns **33** detail columns. Corrected, and the total is now stated once and
  honestly: 46 columns — 10 on every search row, 33 more on an enriched row, plus
  `row_status` / `why_withheld` and the `input` echo.
- The payload is described by what each column decides rather than by how many there are.
- The output sample now matches the dataset schema exactly (46 keys): `row_status`,
  `why_withheld` and `hq_address_line2` were missing from it.
- The output section states what a free run held back, and the FAQ answer about the per-run
  limit now mentions the preview rows instead of only the `USER_MESSAGE` record.
- The README's H1 matches the Actor's Store title.
- `🔑 Use cases` became `🧭 When to use it` and moved up, ahead of the example tasks.

### \[0.0.30] - 2026-08-25

#### Added

- **Withheld free-plan preview rows.** When a free run hits the 20-job cap with matches
  remaining, up to 5 already-fetched jobs from the same page are now published at the end of
  the dataset as preview rows: `row_status = 'withheld_free_plan'`, title/company/location
  real, links/salary/details locked, and a `why_withheld` sentence naming exactly what was
  held back. Preview rows are never charged and never counted in run totals. Every normal
  row now carries `row_status = 'delivered'` / `why_withheld = 'N/A'`, so previews are
  filterable. Two new dataset columns appended last; both dataset views updated.
- Mode 2 (Fetch Job Details): links the cap left unprocessed are echoed back as preview rows
  (your own URLs, nothing else), so the skipped list is visible in the dataset itself.

#### Changed

- A free run truncated by the cap now reports `stopReason = 'free_cap_reached'`
  (category `free_limit`) instead of a plain completion.
- The free-cap message counts Mode-2 withheld links honestly: only links never attempted are
  "not processed" — dead links that returned nothing are no longer counted as withheld.
- README: the Free plan section is now a Free-vs-paid table naming exactly what a paid plan
  unlocks.

### \[0.0.29] - 2026-08-17

Maintenance build — no user-facing changes. Input schema, output dataset
columns, KVS records, console output, error messages, defaults, and pricing
are all unchanged from 0.0.28. Saved tasks continue to work identically.

### \[0.0.28] - 2026-08-17

Maintenance build — no user-facing changes. Input schema, output dataset
columns, KVS records, console output, error messages, defaults, and pricing
are all unchanged from 0.0.27. Saved tasks continue to work identically.

### \[0.0.27] - 2026-08-12

Maintenance build — no user-facing changes. Input schema, output dataset
columns, KVS records, console output, error messages, defaults, and pricing
are all unchanged from 0.0.26. Saved tasks continue to work identically.

### \[0.0.26] - 2026-08-12

Maintenance build — no user-facing changes. Input schema, output dataset
columns, KVS records, console output, error messages, defaults, and pricing
are all unchanged from 0.0.25. Saved tasks continue to work identically.

### \[0.0.25] - 2026-08-10

#### Fixed

- **A failed run no longer names the exact request that failed.** When a request was refused, the
  run's failure reason spelled out the technical address that had been called, and that reason is
  stored in `RUN_SUMMARY` and shown with the run. It now says only that the request was refused.

### \[0.0.24] - 2026-08-10

#### Changed

- The free-plan limit ids listed in the run's `FREE_LIMITS_APPLIED` storage record now use one
  shared vocabulary across all our actors, so the same limit reads the same wherever you meet it.
  The human-readable message on each entry is unchanged.

### \[0.0.23] - 2026-07-30

Maintenance build — no change to input, output or pricing.

### \[0.0.22] - 2026-07-29

#### Changed

- **The Storage tab now names the records the run writes** — the run summary, the plain-language
  explanation of what happened, and the free-plan caps applied — instead of showing bare keys.
- **The Store listing now says what this actor returns** — full description, salary, required
  skills, seniority and hiring-team contacts on every posting — and the price line shows the
  per-job price instead of a generic label.

### \[0.0.21] - 2026-07-28

#### Fixed

- **The status page is saved at all now.** Its saved copy was being rejected on every single run,
  so there was nothing to fall back on once the run's container went away.
- **The status page now keeps up while a run is going.** Its saved copy was only written twice —
  once at the start and once at the end — so anyone opening it mid-run saw the run as it looked at
  second zero. It now refreshes every ten seconds.
- **The "Live status" link now works after a run has finished.** It pointed at the run's own
  container, which the platform shuts down as soon as the run ends, so opening it later showed
  nothing. It now opens the saved copy of the page, which stays available for as long as the run's
  storage does.
- **Removed a storage record from the actor's schema that this actor never writes.** `SKIPPED_ITEMS`
  was declared but never produced, so tools reading the schema expected a record that was never
  there.

### \[0.0.20] - 2026-07-06

#### Changed

- Every "Upgrade" link in run messages now says what's behind it: paid plans start at $29/mo with $29 in platform credits, and the link opens your plan page in Apify Console.

### \[0.0.19] - 2026-07-06

#### Fixed

- The "Upgrade" link in run summaries now opens your Apify plan page directly (the old link form could land on the wrong billing tab).

### \[0.0.18] - 2026-07-02

#### Added

- **When some job links return nothing, the run now says how many.** A Fetch Job Details run with a mix of live and expired links previously reported only the rows it saved — now the run report and the completed screen show "N of your M job links returned nothing — likely closed, filled, or removed postings", with a reminder that empty links are not charged. The run summary also carries the count.

### \[0.0.17] - 2026-07-02

#### Changed

- README "Example tasks" now links the new "Scrape jobs by company on LinkedIn" ready-to-run task (six one-click examples total). No code changes.

### \[0.0.16] - 2026-07-02

#### Fixed

- **Mode 3 (Search Company Jobs) now returns what companies actually list.** Two bugs made most company searches come back empty:
  - Leaving the keyword field blank silently searched a default query instead of returning every open role. Blank keywords now return the company's complete job listing, as the field always promised.
  - Results were silently limited to the United States unless you changed the location. Mode 3 now searches all locations by default; pick a Mode 1 location field to narrow.

#### Added

- **Empty company searches now explain themselves** — the run report and live status screen show each company's live open-roles count, so you can tell "this company lists no jobs on LinkedIn" apart from "my keywords filtered everything out".
- New "Scrape jobs by company" example task; FAQ entry on getting every open role at a company.

#### Changed

- Mode 3 documentation rewritten around the fixed behavior: blank keywords = complete listing, worldwide by default, cost guidance for large companies.

### \[0.0.15] - 2026-07-02

#### Added

- **Example tasks** — five ready-to-run, pre-configured searches linked from the README (by keyword, Data Analyst, Product Manager, remote jobs, Software Engineer). One click, no setup.
- **The run tells you what you're missing** — when a free run hits the plan cap, the run report now shows exactly how many jobs matched your search and how many more are waiting, with a one-step path to the full result set.
- **Recovery screen for zero-result runs** — instead of a generic completion screen, a run that finds nothing now shows the most likely cause, the top fix, and a starter search that always returns jobs. The no-results report also leads with that starter search.
- **New FAQ entries** — no LinkedIn account/cookies/login needed; how many jobs per run; bulk-enriching job URL lists; scheduling a daily search.
- **Tips & Best Practices and Disclaimer sections** in the README; clearer in-form guidance on both search-keyword fields.

#### Changed

- **Free plan now returns up to 20 jobs per run** (was 50). When a search matches more, the run report says exactly how many — upgrade and re-run the same input for the full set.
- Store title and description now lead with what the data is for: salary, skills, and hiring-team contacts for recruiters, sourcers, and lead-gen teams.
- Mode 3 (Search Company Jobs) documentation now sets honest expectations — many companies have no open roles at any given moment.

#### Fixed

- Truncated Store description (ended mid-sentence with "…").
- Welcome message wrongly said scheduling was paid-only — free plans can schedule tasks with saved input too.

### \[0.0.14] - 2026-05-28

#### Fixed

- **Helpful message when Fetch Job Details returns nothing** — if a job link can't be pulled, the run now explains the likely cause (the posting was closed, filled, or removed; or the link isn't a job-view URL like `https://www.linkedin.com/jobs/view/<id>`) and how to copy a valid one, instead of showing the generic welcome message. Search mode already did this — Fetch Job Details now does too. Empty detail pulls are not charged.

### \[0.0.13] - 2026-05-21

#### Added

- **Helpful message when a search returns no jobs** — if Search Jobs or Search Company Jobs finds nothing, the run now explains the most likely reasons and the highest-impact fixes (trim your keywords to the core job title, widen the date range, turn off Easy Apply / Under 10 applicants, broaden experience levels) instead of finishing silently with an empty dataset. Empty results are not charged.

#### Changed

- The Job keywords field now notes that extra words (seniority, degree, e.g. "fresher") sharply narrow results and can return nothing.

### \[0.0.12] - 2026-05-21

#### Fixed

- **Multiple job titles in one search** — a comma-separated keyword list (e.g. `Risk Analyst, Data Analyst, Financial Analyst`) is now treated as an OR search that returns jobs matching **any** of the titles. Previously the whole comma string was sent as a single literal phrase, which matched nothing and returned zero rows. Applies to Mode 1 (Search Jobs) and Mode 3 (Search Company Jobs).
- **Custom Location with several places** — a comma list in Custom Location (e.g. `Poznań, Łódź`) now resolves to the first place LinkedIn recognises instead of silently falling back to the whole country.

### \[0.0.11] - 2026-05-18

#### Added

- **State / province / region dropdown** — narrow Mode 1 to a single subdivision (~1500 entries covering 70 top markets, sourced from LinkedIn's own canonical ADMIN\_DIVISION\_1 IDs).
- **Major city dropdown** — narrow Mode 1 to a single city (top ~1000 global cities by population, sourced from LinkedIn's canonical CITY IDs).
- **Custom Location** free-text field — type ANY LinkedIn-indexed location (city / town / neighborhood / region) and the actor resolves it live. Power users can paste a numeric LinkedIn geoId directly. Closes the long-tail gap for places not in the curated dropdowns (Cluj-Napoca, Pune, Tartu, etc.).

#### Changed

- Geo resolution now cascades through four fields. Priority: **Custom Location → City → State → Country**. The most specific non-empty filter wins. Country dropdown remains the always-default fallback.

### \[0.0.10] - 2026-05-18

Maintenance build — no user-facing changes.

### \[0.0.9] - 2026-05-18

#### Changed

- Country dropdown expanded to **198 entries** — full ISO-3166-1 sovereign-state coverage. GeoIds resolved canonically against LinkedIn's own public typeahead API (the same source that powers the autocomplete on linkedin.com/jobs). No more "your country isn't in the list" gaps.

### \[0.0.8] - 2026-05-18

#### Changed

- Country dropdown expanded from 23 → 28 entries after cross-referencing a community-curated mapping. Added: China, Italy, Russia, South Korea, United Arab Emirates. Two prior probe-rejected geoIds were re-investigated and revealed to be these countries (initial probes misread the top-row locations).

### \[0.0.7] - 2026-05-18

#### Changed

- "Location (LinkedIn geo code)" is now a **Country dropdown** with 23 probe-verified top markets. Previously a numeric input that asked customers to extract the geoId from a linkedin.com URL — now they pick a country name from a list. Default is United States.
- Default country geoId updated to the real United States value (probe-verified). The previous default returned ~500K mixed-region results dominated by Vietnam — it was a supra-regional bucket, not a country filter.

#### Notes

- LinkedIn's geo filter is loose; even with a country selected, some rows from adjacent markets may appear. The field description discloses this.

### \[0.0.5] - 2026-05-18

#### Added

- Industries multi-select dropdown on Mode 1 (Search Jobs) and Mode 3 (Search Company Jobs), sourced from the 434-entry canonical LinkedIn V2 industry taxonomy. Pick one or more industries to narrow the search to a specific sector.

#### Notes

- Verified with live searches that the industry filter accepts industry names end-to-end (e.g. `Software Development` narrowed the broad "engineer" pool from 499,684 → 95,375 jobs with software companies up top; `Accounting` narrowed to 4,017 with PwC / James Moore; `Higher Education` to 1,332 with Penn State / Duke).

### \[0.0.4] - 2026-05-18

#### Fixed

- Dataset schema validation no longer rejects Mode 2 rows. Three type mismatches reconciled: `expired`, `closed`, `remote_allow`, and `hiring_team` now accept every value type they actually arrive in.

### \[0.0.1] - 2026-05-18

#### Added

- Initial private build with three operation modes — Search Jobs, Fetch Job Details, and Search Company Jobs.
- PAY\_PER\_EVENT pricing with a single `JOB_ANALYZED` event ($0.030/job) across all modes.
- Seven post-fetch filters — title contains / NOT contains, company name allow-list, location contains, minimum salary, posted-within-N-days, remote-only.
- Per-mode cost-control caps (`maxItemsMode1`, `maxItemsMode2`, `maxItemsMode3`).
- Empty-input demo fallback (10-row cap) so first-touch runs always produce sample output.
- Live status page during the run + `RUN_SUMMARY` / `USER_MESSAGE` / `FREE_LIMITS_APPLIED` records in the Key-Value Store.
