# LinkedIn Skills Demand — Top Skills in Job Postings (`northbell/linkedin-skills-demand`) Actor

Which skills do LinkedIn job postings ask for? Give a job title and a place, get the top skills with their share of postings, by category (languages, cloud, data, security, business tools, certifications, soft skills) and which skills rose or fell since your last run. No login.

- **URL**: https://apify.com/northbell/linkedin-skills-demand.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 $40.00 / 1,000 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 Skills Demand — Top Skills in Job Postings

Give it job titles and places — `data engineer | Germany`, or `{"title": "accountant", "location": "United Kingdom"}` — and get one row per report: the **top 40 skills that live LinkedIn postings ask for, each with the share of postings that name it**, the skills **by category** (programming languages, frameworks, cloud, data and AI, DevOps, security, business tools, methods, certifications, soft skills, spoken languages), how many skills a typical posting names, and five typical postings to check the numbers against. It reads up to hundreds of public postings per report and looks for about 450 skills in their usual spellings — `Go` the language, not "go to market"; `Java`, not `JavaScript`. **$50 per 1,000 reports ($0.05 each)**, no login, no API key.

Run it again — or put it on a monthly schedule — and every row also tells you **what moved since the last run**: each top skill's previous share, change and rank (`rankChange`, positive = moved up), whether it is new, and the lists of **rising** and **falling** skills — only moves that sampling noise does not explain. LinkedIn only shows the postings that are open today; this keeps the history for you. The example input (`data engineer`, United States, 20 postings) took 65 s on 17 Oct 2026.

**Then:** the `sampleJobUrls` of a report go straight into the **Jobs** field of [LinkedIn Salary Scraper](https://apify.com/northbell/linkedin-jobs-salary-scraper), which returns each of those postings with its pay as numbers when the posting states it.

### Call it from a script or a schedule

```bash
curl -X POST "https://api.apify.com/v2/acts/northbell~linkedin-skills-demand/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reports":[{"title":"data engineer","location":"United States"}]}'
```

The response is the rows themselves (a JSON array). To run it on a timetable instead, save the input as a task in Apify Console and add a Schedule; n8n, Make and Zapier run it through their Apify app with the same input.

### What you get

One row per job title and location (`type`: `skills-report`). A share is `jobs` divided by `jobsWithDescription`, from 0 to 1.

| Field | What it is |
|---|---|
| `title`, `location`, `datePosted`, `titleMatch` | the report you asked for |
| `jobsListed`, `jobsOffTitle`, `jobsRead`, `jobsClosed`, `jobsFailed` | how many postings LinkedIn listed, how many were left out because the title lacked your words, how many were opened and read, how many had closed since, how many could not be read |
| `jobsWithDescription` | postings with description text to take skills from: the denominator of every share |
| `skillsFound`, `skillsPerJobMedian` | how many different skills turned up, and how many a typical posting names |
| `topSkills` | the 40 most-named skills: `skill`, `category`, `jobs`, `share`, `rank`, and the comparison with your previous run: `previousShare`, `shareChange`, `previousRank`, `rankChange` (positive = moved up), `isNew` (not named by any posting last time) |
| `byCategory` | for each category the top 10 skills as `{skill, share}`: `Programming Languages`, `Frameworks`, `Cloud`, `Data & AI`, `DevOps & Infrastructure`, `Security`, `Business Tools`, `Methods & Domain`, `Certifications`, `Soft Skills`, `Spoken Languages` |
| `risingSkills`, `fallingSkills` | skills whose share moved since your previous run by more than chance explains: `{skill, category, shareNow, shareBefore, change}`, up to 10 each (see Notes) |
| `sampleJobUrls` | five postings that name about as many skills as the typical one: open them to check the numbers |
| `lowSample`, `note` | `lowSample` is `true` when fewer than 15 postings had a description; `note` says why a row should be read with care |
| `previousObservedAt`, `previousJobsWithDescription`, `isNew` | when the previous run of the same title, location, posting age and title rule was made and how many descriptions it had (`null` on the first run, `isNew` is `true`) |
| `observedAt` | when the row was made |

Searches that fail come back as free rows with `type: "error"` (`input`, `title`, `location`, `kind`, `message`).

**Long lists and the run timeout.** Two minutes before the run's time limit, the report in progress stops reading and returns what it read (its `note` says so), and the ones not started come back as free `kind: "time-limit"` rows — nothing read is lost to a timeout.

A real excerpt (60 *data engineer* postings in the United States, October 8, 2026):

```json
{ "type": "skills-report", "title": "data engineer", "location": "United States", "jobsWithDescription": 60, "skillsPerJobMedian": 20,
  "topSkills": [ { "skill": "SQL", "category": "Data & AI", "jobs": 50, "share": 0.833, "rank": 2 },
                 { "skill": "Python", "category": "Programming Languages", "jobs": 46, "share": 0.767, "rank": 4 } ] }
```

### Use it for

- **Choosing what to learn** — which languages, tools and certifications a role asks for in your city, and which are fading
- **Recruiters and hiring managers** — the skill list competitors write into the same role, and how long that list is (`skillsPerJobMedian`)
- **Bootcamps, universities and L\&D teams** — a syllabus checked against what employers ask for this month, with the movers
- **Market research** — compare roles and places (say, how often *Kubernetes* or *ACCA* is named in London and in Berlin), monthly, with the history kept

### Input

```json
{
  "reports": [
    { "title": "data engineer", "location": "United States" },
    "accountant | United Kingdom"
  ],
  "maxJobsPerReport": 100,
  "datePosted": "past-month"
}
```

- `reports` — up to 50 pairs per run, each `{"title", "location"}` or the text `title | location`. The location is a country, region or city as LinkedIn spells it; `Worldwide` works too.
- `maxJobsPerReport` — postings to read per report, 10 to 500 (default 100).
- `datePosted` — `past-month` (default), `past-week` or `any`.
- `titleMatch` — `all-words` (default) keeps postings whose title contains every word you typed, so a *data engineer* report is not filled up with software engineers. `any` keeps everything LinkedIn lists for those words.
- `categories` — leave empty for all; pick some (say Programming Languages, Frameworks, Cloud and Data & AI) to leave the rest, soft skills in particular, out of the top list and the category lists. Shares stay out of all postings with a description.
- `maxRequestsPerMinute` — how fast pages are requested (default 20).

The previous run is remembered per title, location, posting age and title rule in a named key-value store in your Apify account, so the second run already shows changes. Schedule it (Apify → Schedules) for a history. A run that LinkedIn cut short (a low sample after refusals) is not remembered, so the next run is still compared with your last proper report.

### Pricing

**$0.05 per report** ($50 per 1,000), plus $0.01 per run. One report is priced for the work behind it: it opens up to `maxJobsPerReport` postings, and each posting is one page request, plus one request per 10 listed. In live runs that was **1.2 to 1.4 requests per posting** (more when many of the postings LinkedIn lists are off your title), so the default 100 postings is roughly 120 to 140 requests and **about 6 to 7 minutes at the default pace of 20 requests a minute**. At 256 MB that is around 0.03 compute units of platform usage (about a cent), already included in the price; nothing is billed on top. The cost grows with the postings you ask for: 500 postings is five times that, about half an hour per report.

- **Free:** error rows (a place LinkedIn does not know, no posting with your title, LinkedIn refusing), a row where **no skill was found** (`skillsFound` is 0 and `topSkills` is empty: the postings name nothing from the list, or nothing in the categories you chose; there is nothing to pay for), and a row that LinkedIn cut short by refusing requests and that ended up a `lowSample`.
- A report read from **fewer than 5 descriptions is free** (`note` says so). From 5 to 14 it is a `lowSample` row, flagged and explained in `note`, and charged like any other report.
- The $0.01 run-start fee is the one charge every run pays, even one that returns nothing.

### Notes

- Public data only: what anyone sees on LinkedIn without signing in. Rows carry skill names, shares and posting links, nothing else from a posting: no descriptions, company names, recruiter names, emails or phone numbers.
- **What "asks for" means.** A skill counts when the posting's text names it anywhere — requirements, "nice to have" and the company's own description alike (so a vendor's own postings name its product) — and a posting counts once per skill however often it names it. So a share is the share of postings that *mention* the skill, not the share that *require* it. Measured on 60 postings each (October 8, 2026): a typical *data engineer* posting in the United States names 20 of the skills, an *accountant* posting in the United Kingdom 10, a *product designer* posting in Germany 7.
- **How a skill is recognised.** About 450 skills, each with its usual spellings (`Postgres`, `k8s`, `Amazon Web Services`, `.NET Core`, `C/C++`), matched on whole words: `SQL` is not found inside `MySQL`, `Java` not inside `JavaScript`, `React` not in "react quickly" or "reaction". Words that are also plain English or a name need a list around them: `Go`, `R`, `C`, `Swift`, `Spark`, `Chef`, `Spring` and `Express` count in "Python, Go, SQL" or a bullet that says only "Go", not in "go to market", "R\&D", "Series C", "spark creativity" or "Spring 2027 graduates". `Excel` the program, not "excel in a fast-paced team". Acronyms with a second meaning are cut too: `CPA` is the accountancy qualification, not an advertising metric; `GCP` is Google Cloud, not Good Clinical Practice (a line about trials or regulation is skipped); `PHP` is the language, not the peso in "PHP 68,000"; `C` is not the "C" of a credential like *FNP-C*. Web and e-mail addresses in a description are ignored. Soft skills are matched on the words and phrases that name them ("communication skills", "problem-solving", "stakeholder management", "mentor", "collaborate"), so one can come from a sentence about the team's culture as well as from a requirement (*Collaboration* is named in about half of all postings, in every role), and one worded another way is missed: read soft-skill shares as a rough guide, hard skills as the precise part.
- **Hiring-process notices are left out.** Many postings end with "we may use AI tools to screen applications". That is about the hiring process, not a skill, and it was putting "AI" into one posting in three; sentences like that are skipped. A sentence that sounds like a requirement ("experience with AI tools to screen resumes") stays.
- **English names.** Skills are looked up by their English names. In a posting written in German or Dutch the tools, languages and certifications (`Kubernetes`, `Python`, `CISSP`) are still found; soft skills and business terms written in the local language are not. Spoken languages count only with a word that says how well ("fluent in German", "business English", "German speaker").
- **Rising and falling.** With 100 postings in each run, a share of 30% moves by about 9 points from chance alone, so a skill is listed only if its share moved by at least 5 points **and** a two-proportion test puts the move beyond sampling noise (about 95% one-sided), and both runs read at least 15 descriptions. An empty list means nothing moved more than chance; the raw change is always in `topSkills[].shareChange`. When the skill list of this Actor is updated, the next run says so in `note` and starts a new comparison instead of calling every changed skill a mover.
- **Sample size.** `lowSample` is `true` below 15 postings with a description. Postings LinkedIn served without description text are counted in `jobsRead` but not in the shares. The first 30,000 characters of a description are read.
- **Title filter.** LinkedIn's search is loose, so by default only postings whose title has every word you typed are read (a word may continue by up to three letters: *engineer* matches *engineers* and *engineering*, but *java* does not match *JavaScript*). The same posting listed twice (same company, title and place) counts once. `jobsOffTitle` says how many were left out.
- Fields that are empty on purpose: `risingSkills`, `fallingSkills`, `previousObservedAt`, `previousJobsWithDescription` and the comparison fields inside `topSkills` are `null` on the first run of a title, location, posting age and title rule; `risingSkills` and `fallingSkills` are also `null` when either run had fewer than 15 descriptions, and `[]` when nothing moved beyond chance; `note` is `null` unless something needs saying; a category list in `byCategory` is `[]` when no skill of that category was named, and `topSkills` is `[]` (with `skillsFound` 0) when no skill at all was.
- LinkedIn lists at most about 1,000 postings per search, so 500 is the ceiling. If LinkedIn rate-limits a request the Actor waits 5 s, then 10 s, and tries again; if it keeps refusing, the run stops reading, keeps what it has and says so in `note`.

For the **pay** in the same postings, see [LinkedIn Salary Benchmark](https://apify.com/northbell/linkedin-salary-benchmark). For **who is hiring** in a market, see [LinkedIn Job Market Report](https://apify.com/northbell/linkedin-job-market-report). To get the **postings themselves** with their pay, see [LinkedIn Salary Scraper](https://apify.com/northbell/linkedin-jobs-salary-scraper).

### For AI agents

`{"reports": ["data engineer | United States"], "maxJobsPerReport": 100}` returns one row with `topSkills` (`skill`, `category`, `jobs`, `share`), `byCategory`, `skillsPerJobMedian`, `risingSkills` and `fallingSkills`. Rows have `type`: `skills-report` or `error`. `share` is the share of postings that name the skill (0 to 1). If `lowSample` is `true`, read `note` before quoting a number.

# Actor input Schema

## `reports` (type: `array`):

What to report on: a list of job title + location pairs, each as {"title": "data engineer", "location": "United States"} or as text, "data engineer | Germany". The location is a country, region or city as LinkedIn spells it ("Worldwide" works too). Each pair is one report.

## `maxJobsPerReport` (type: `integer`):

How many LinkedIn postings to open and read for each report (10 to 500). Skills live in each posting's description, so the run makes about one request per posting plus one per 10 listed. More postings give steadier shares; 100 is a good default.

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

Only postings from this period. The past month keeps the report current; choose any time for rare titles or small places. Each period keeps its own history for the since-last-run comparison.

## `titleMatch` (type: `string`):

All words in the title (default) keeps postings whose title contains every word you typed, so a data engineer report does not fill up with software engineers. Anything LinkedIn lists keeps everything the LinkedIn search returns for those words.

## `categories` (type: `array`):

Leave empty for all categories. Pick some to leave the rest out of the top list and the by-category lists, for example only Programming Languages, Frameworks, Cloud and Data & AI for a tech-only view. Shares are always out of all postings with a description, whatever you pick.

## `maxRequestsPerMinute` (type: `integer`):

How fast the run asks LinkedIn for pages. Lower it if the log shows many rate-limit retries. The pace is shared by all runs of this Actor in your account.

## Actor input object example

```json
{
  "reports": [
    {
      "title": "data engineer",
      "location": "United States"
    }
  ],
  "maxJobsPerReport": 20,
  "datePosted": "past-month",
  "titleMatch": "all-words",
  "maxRequestsPerMinute": 20
}
```

# Actor output Schema

## `reports` (type: `string`):

Top skills with their share of postings, skills by category, and rising and falling skills since the last run.

# 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 = {
    "reports": [
        {
            "title": "data engineer",
            "location": "United States"
        }
    ],
    "maxJobsPerReport": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/linkedin-skills-demand").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 = {
    "reports": [{
            "title": "data engineer",
            "location": "United States",
        }],
    "maxJobsPerReport": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("northbell/linkedin-skills-demand").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 '{
  "reports": [
    {
      "title": "data engineer",
      "location": "United States"
    }
  ],
  "maxJobsPerReport": 20
}' |
apify call northbell/linkedin-skills-demand --silent --output-dataset

```

## MCP server setup

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

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/weEg9Yj2tCtUtAncS/builds/iCQxIsmcEvVqvBmm8/openapi.json
