# LinkedIn Job Market Report — Demand, Top Hiring Companies (`northbell/linkedin-job-market-report`) Actor

A LinkedIn job market report per keyword and location: roles read, how many LinkedIn shows, top hiring companies, job titles and places, remote share and fresh postings. Run it again for the change since the last run: new companies and rank moves. $0.03 per report. No login.

- **URL**: https://apify.com/northbell/linkedin-job-market-report.md
- **Developed by:** [Northbell](https://apify.com/northbell) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $24.00 / 1,000 market reports

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## LinkedIn Job Market Report — Demand, Top Hiring Companies

Give it a list of **markets** — a keyword in a place, like `react developer | Berlin, Germany` — and get **one report row per market**: how many roles it read, how many LinkedIn says exist, **who is hiring** (top 15 companies), the **title mix**, the **places**, the **remote share** and how many postings are fresh. **$0.03 per report** ($30 per 1,000), no login, no cookies.

Run it again — or put it on a weekly schedule — and every report also tells you **what changed since the last run**: which companies are **new** in the market, how the top companies **moved up or down** (`previousRank`, `rankChange`, positive = moved up), and how many more or fewer roles it read. LinkedIn only shows you today's market; this keeps the history.

### What you get

One row per market (`type: "market"`):

| Field | What it is |
|---|---|
| `keywords`, `location`, `datePosted`, `searchUrl` | the market and the LinkedIn search page it comes from |
| `jobsRead`, `maxJobsPerMarket`, `stoppedBy`, `note` | how many distinct postings were read. `stoppedBy` is `cap` (your limit), `end` (LinkedIn's public list had no more to give — **this can be far fewer than the total it prints**, see Notes) `error` (then `note` says what happened) or `time` (the run was about to reach its time limit; `note` says so) |
| `totalShownByLinkedIn`, `totalShownText`, `totalShownIsLowerBound`, `totalShownPlace`, `totalShownNote` | the number LinkedIn prints above the results. Big markets show as a rounded floor such as `1,000+` or `10,000+` (`totalShownIsLowerBound: true`). `totalShownNote` appears only when the number is missing or answers for another place, and says why |
| `shareOfTotalRead` | `jobsRead` as a share of the exact total LinkedIn prints (`0.147` = you read 15% of it); `null` when the total is missing or a floor like `10,000+` |
| `postedLast24h`, `postedLast7d` | postings by posting date: dated today or yesterday (UTC), and dated within the last 7 days |
| `remoteShare`, `activelyHiringShare` | share of postings whose title or place says "Remote", and share with LinkedIn's "Actively Hiring" badge |
| `distinctCompanies`, `top5CompaniesShare` | how many companies are hiring, and how much of the market the five biggest hold |
| `topCompanies` | top 15: `company`, `slug` (LinkedIn's company page name, `null` when the posting links none), `jobs`, `rank`, `previousRank`, `rankChange`, `jobsChange`, `isNew` |
| `topTitles` | top 15 job titles with `jobs` — normalised, so "Data Engineer (m/w/d)", "Data Engineer - Remote" and "Sr. Data Engineer" fall into the right groups |
| `topLocations` | top 10 places with `jobs` |
| `previousObservedAt`, `jobsReadChange`, `totalShownChange`, `postedLast7dChange` | the change since your previous run of the same market (`null` on the first run) |
| `newCompaniesCount`, `newCompaniesSinceLastRun` | companies hiring now that were not in the previous run (top 15 listed, with `jobs`; an empty list when nothing is new) |
| `requestsUsed` | HTTP requests this report took, retries included |
| `observedAt` | when it was read |

With **Include job details** on, the row also has: `detailsRead` (of `detailsAsked`), `seniorityMix`, `employmentTypeMix` (each `[{level or type, jobs, share}]`), `easyApplyShare`, and `detailsNote` when too few job pages could be read.

Error rows (`type: "error"`: `keywords`, `location`, `message`) are free.

### Use it for

- **Hiring and recruiting** — see which companies are competing for the same people in your city, and which just started
- **Career moves and salary talks** — how deep is the market for your title in a place, and how much of it is remote
- **Sales and lead generation** — companies hiring in a field are companies with budget for it; `newCompaniesSinceLastRun` is a weekly lead list
- **Market research and newsletters** — demand by role and place, tracked the same way every week

### Input

```json
{
  "markets": [
    { "keywords": "data engineer", "location": "United States" },
    "react developer | Berlin, Germany"
  ],
  "maxJobsPerMarket": 200,
  "datePosted": "past-week"
}
```

- `markets` — up to 100 per run. Objects with `keywords` and `location`, or the short string `"keywords | location"`. Put them all in one run and you pay the start fee once.
- `maxJobsPerMarket` — default 200, up to 1,000. Ten postings per request.
- `datePosted` — `past-week` (default), `past-24-hours`, `past-month` or `any`.
- `includeDetails` — default `false`. Opens a sample of job pages for the seniority mix, employment-type mix and Easy Apply share (see Pricing).
- `detailsPerMarket` — job pages to open per market when details are on: 30 by default, 10 to 100, spread evenly over the postings read.

The history is kept per market, period and size in a named key-value store in your Apify account, so the second run already shows changes. Schedule it (Apify → Schedules) for a weekly log. Changing `datePosted` or `maxJobsPerMarket` starts a new history for that market.

### Pricing

**$0.03 per market report**, plus $0.01 per run. A report that reads 200 postings takes about **21 requests** (20 pages of results and 1 for the total); at 1,000 postings about **101**; a search whose public list is shorter takes fewer. LinkedIn is read at 30 requests a minute, so a 200-posting report takes about 40 seconds: a run of 100 markets at that size needs more than the default 1-hour run timeout, so lower `maxJobsPerMarket` or raise the timeout. Each report is saved as soon as it is done. Error rows (markets that could not be read) are free. The $0.01 run-start fee is the one charge every run pays, even one that returns nothing.

With **Include job details** on, a report costs **$0.10** instead ($100 per 1,000) and takes up to `detailsPerMarket` more requests (30 by default). If too few job pages could be read to give a mix, the report is kept and charged as a plain one.

### Notes

- **Public data only**: what anyone sees on LinkedIn without signing in. No login, no cookies; the Actor cannot send either. No people are read — only job postings, company names and places.
- **The lists come from the postings read, which can be a small part of the market.** The company, title and place lists are ranked by the number of postings among the postings read. LinkedIn serves anonymous visitors at most the first 1,000 postings of a search, and for many searches the public list ends much earlier: in October 2026 a search that printed "4,000+" listed 100 postings and one that printed 680 listed 100 (`stoppedBy: "end"`). So `stoppedBy: "end"` means LinkedIn had nothing more to show, **not** that you have the whole market. Compare `jobsRead` with `totalShownByLinkedIn` (`shareOfTotalRead`) to see how much of it a report covers, and compare markets with each other by shares (`remoteShare`, `top5CompaniesShare`) rather than by raw counts.
- **"New since last run" compares what each run could read.** A company shows as new when it was not in the previous run's list; with a list that is a ranked slice of a bigger market (`stoppedBy` is `cap`, or `end` well short of the total) it can also mean it moved into the slice. Narrow the market (one city, `past-24-hours`) for steadier changes. A run cut short by an error (`stoppedBy: "error"`) is not saved, so the next run is compared with the last complete one.
- **`totalShownByLinkedIn` can be empty.** It comes from LinkedIn's public search page, which sometimes answers for a different place than the one you asked for (we saw "Berlin, Germany" answered as the district "Köpenick"). Then the number is not used, it is `null`, and `totalShownNote` says what happened; the same field is also empty when that page could not be read. It never fails the report. `totalShownChange` is only filled when both totals are exact (not `10,000+`). A place LinkedIn does not recognise is widened to a bigger one (a made-up town came back as the whole United States): check `topLocations` against the place you asked for.
- **Dates have no time of day.** LinkedIn publishes the posting date, so `postedLast24h` counts postings dated today or yesterday (UTC), and `postedLast7d` those dated within 7 days. Choosing `past-week` makes `postedLast7d` about equal to `jobsRead`; choose `any` or `past-month` to see the age mix.
- **`remoteShare` is a floor.** The public job list has no workplace-type field, so it counts postings that say "Remote" in the title or place ("Remote Sensing" excepted).
- **Job details are a sample.** `seniorityMix`, `employmentTypeMix` and `easyApplyShare` are computed over the job pages read (`detailsRead`), not the whole market. `easyApplyShare` is the share of postings where you apply on LinkedIn itself (LinkedIn's public page shows an Apply button without the external-link arrow); the rest send you to the company's own site. Postings that state no seniority or employment type are left out of that mix. `seniorityMix` uses LinkedIn's own levels, including "Not Applicable".
- **Not included:** salary (see the salary Actor below), job descriptions, applicant counts. Fields from job pages are empty unless **Include job details** is on.
- If LinkedIn stops answering part-way, a report built from at least 30 postings is kept and marked `stoppedBy: "error"` with a `note`; with fewer you get a free error row. After three markets in a row that LinkedIn refuses, the rest of the run is skipped instead of waiting for each. Two minutes before the run's time limit the market in progress stops the same way (`stoppedBy: "time"`), and the markets not started come back as free `kind: "time-limit"` rows, so nothing read is lost to a timeout.
- `rank` is shared between companies with the same number of postings (1, 1, 1, 4), so a new company does not push the tied ones down.

Need the postings themselves, pay or one company's openings?

- [Fast LinkedIn Jobs Scraper](https://apify.com/northbell/linkedin-jobs-fast-scraper) — the job rows behind a market, cheap and quick
- [LinkedIn Salary Benchmark](https://apify.com/northbell/linkedin-salary-benchmark) — pay ranges for the same roles
- [LinkedIn Company Jobs Scraper](https://apify.com/northbell/linkedin-company-jobs-scraper) — every open role at a company from `topCompanies`

### For AI agents

`{"markets": ["data engineer | United States"], "maxJobsPerMarket": 100}` returns one row with `jobsRead`, `totalShownByLinkedIn`, `topCompanies`, `topTitles`, `remoteShare` and the change since the last run. Rows have `type`: `market` or `error`. Pass several markets in one run to compare them. `jobsRead` is the size of the sample behind the lists (LinkedIn's public list often ends at 100); for demand use `totalShownByLinkedIn` when it is present and not a floor (`totalShownIsLowerBound: false`), and compare the lists by shares (`remoteShare`, `top5CompaniesShare`).

# Actor input Schema

## `markets` (type: `array`):

One report per market. Each market is a keyword search in a place: {"keywords": "react developer", "location": "Berlin, Germany"}. The short form "react developer | Berlin, Germany" works too. Up to 100 markets per run; put them all in one run and you pay the start fee once.

## `maxJobsPerMarket` (type: `integer`):

How many postings to read for each market (10 per request, so 200 is 20 requests). More postings make the company, title and place lists more complete. The report is the same price at any size.

## `datePosted` (type: `string`):

Only count postings from this period. Past week is the usual choice for a weekly market check.

## `includeDetails` (type: `boolean`):

Also open a sample of the job pages to add the seniority mix, the employment-type mix and the share of postings you apply for on LinkedIn. Charged at the higher per-report price and adds one request per job page read.

## `detailsPerMarket` (type: `integer`):

Only used when Include job details is on. How many job pages to open for each market, spread evenly over the postings read. 30 gives a rough mix, 100 a steadier one.

## Actor input object example

```json
{
  "markets": [
    {
      "keywords": "data engineer",
      "location": "United States"
    },
    {
      "keywords": "react developer",
      "location": "Berlin, Germany"
    }
  ],
  "maxJobsPerMarket": 200,
  "datePosted": "past-week",
  "includeDetails": false,
  "detailsPerMarket": 30
}
```

# Actor output Schema

## `markets` (type: `string`):

Roles read, total shown by LinkedIn, fresh postings, remote share, top hiring companies and the change since the last run.

## `all` (type: `string`):

Every row: market reports and any errors.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "markets": [
        {
            "keywords": "data engineer",
            "location": "United States"
        },
        {
            "keywords": "react developer",
            "location": "Berlin, Germany"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/linkedin-job-market-report").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "markets": [
        {
            "keywords": "data engineer",
            "location": "United States",
        },
        {
            "keywords": "react developer",
            "location": "Berlin, Germany",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("northbell/linkedin-job-market-report").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "markets": [
    {
      "keywords": "data engineer",
      "location": "United States"
    },
    {
      "keywords": "react developer",
      "location": "Berlin, Germany"
    }
  ]
}' |
apify call northbell/linkedin-job-market-report --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,northbell/linkedin-job-market-report"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/4XBn0ENv9xIt9hQSq/builds/Gao7cEPbV6d4fgr8f/openapi.json
