# Job Market Report: salary percentiles, skills, remote share (`ucchiii/job-market-report`) Actor

Turn normalized job datasets (input: dataset IDs from Job Postings Normalizer) into one job market report: salary percentiles p25/p50/p75 per currency, top skills, remote share, plus role and country segments. No scraping. Empty input = free demo. $0.10 per report + $5 per 1,000 segments.

- **URL**: https://apify.com/ucchiii/job-market-report.md
- **Developed by:** [Actor Factory](https://apify.com/ucchiii) (community)
- **Categories:** Jobs, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Job Market Report — salary percentiles, top skills and remote share from job scraper data

**For recruiters, HR and compensation analysts, career coaches, job-board and newsletter owners, and labour-market
researchers who already scrape jobs and now have to answer "what does this market actually pay, ask for and allow?"**
You have thousands of job postings in an Apify dataset but no time to turn them into numbers. This Actor reads the
**normalized** job dataset (the output of [Job Postings Normalizer](https://apify.com/ucchiii/job-postings-normalizer))
and writes **one market report plus one row per role or country** — no scraping, no proxies, no spreadsheets.
**$0.10 per report + $5 per 1,000 segment rows.** Reading and duplicates are free.

### What you get

- **Salary percentiles p25 / p50 / p75 per currency** — annualized (hour × 2,080, day × 260, week × 52, month × 12),
  using the midpoint of the salary range. **Currencies are never mixed or converted.**
- **Most requested skills** — the share of jobs that mention each of about 100 common skills (Python, SQL, AWS, Kubernetes,
  Salesforce, Figma, Excel …) in the title or description. Add your own tools with `extraSkills`.
- **Remote share** — remote / hybrid / onsite counts and `remoteShare` (remote ÷ jobs with a known workplace type).
- **Segments** — one row per **role family** (derived from the job title: Data Analysis, DevOps & Cloud, Sales …), per
  **country**, or per **role × country**, each with its own salary percentiles, remote share, top skills and top hiring companies.
- **Report row** — jobs analyzed, salary coverage, top roles, top countries, top 10 hiring companies, employment types,
  posting date range. The same report is saved as one JSON document in the key-value store (`REPORT`).

### Use cases

- Benchmark pay for a role in a country before you write an offer or negotiate
- Show candidates or clients which skills a market asks for and how many roles are remote
- Fill a weekly "jobs market" newsletter or dashboard with fresh numbers from your own scraper runs
- Compare countries or role families in a labour-market study

### How to use

1. Run a job scraper (LinkedIn Jobs, Indeed, Glassdoor, career pages …) and note the dataset ID.
2. Run **Job Postings Normalizer** on it (several datasets can be merged there). It turns salaries and countries into
   the fields this Actor reads.
3. Run this Actor with the normalizer's dataset ID in `datasetIds`. Open the first dataset row (`type: "report"`) and the
   segment rows, or fetch the `REPORT` record.

Leave `datasetIds` empty to see a **free demo** on clearly fictional sample jobs (every row is marked `"_demo": true`).

### Input

```json
{
  "datasetIds": ["<dataset ID of a Job Postings Normalizer run>"],
  "segmentBy": "roleAndCountry",
  "maxSegments": 30,
  "minSalarySample": 3,
  "topSkills": 20,
  "extraSkills": ["dbt", "Terraform"],
  "dedup": true
}
```

| Field | Default | Meaning |
|---|---|---|
| `segmentBy` | `role` | `role`, `country` or `roleAndCountry` |
| `maxSegments` | 30 (0–200) | Largest segments first. Also caps segment charges. 0 = report row only |
| `minSalarySample` | 3 | Percentiles are `null` when fewer salaries of one currency are available |
| `topSkills` | 20 (1–50) | Skills listed in the report row |
| `extraSkills` | none | Extra tools to count (whole-word, case-insensitive) |
| `dedup` | true | Same URL, then same company + title + location counted once |

Fields read from each job: `title`, `company`, `countryCode`, `workplaceType` (or `remote`), `salaryMin`, `salaryMax`,
`salaryCurrency`, `salaryInterval`, `postedAt`, `employmentType`, `description` (only to detect skills; it is not output).
A job counts for salary only when it has a **currency, an interval and an amount**. Datasets that were not normalized
still work but give sparse salary and country numbers.

### Output

First row: the report.

```json
{
  "type": "report", "jobsAnalyzed": 24, "companies": 7, "countries": 5, "salaryCoverage": 0.833, "salaryUnusable": 1,
  "salaryByCurrency": {
    "USD": {"sample": 9, "p25": 125000, "p50": 147500, "p75": 170000, "interval": "year"},
    "EUR": {"sample": 5, "p25": 75000, "p50": 79000, "p75": 80000, "interval": "year"}
  },
  "workplace": {"remote": 8, "hybrid": 10, "onsite": 6, "unknown": 0, "remoteShare": 0.333, "remoteOrHybridShare": 0.75},
  "topSkills": [{"skill": "SQL", "jobs": 10, "share": 0.417}, {"skill": "Python", "jobs": 7, "share": 0.292}],
  "topRoles": [{"role": "Software Engineering", "jobs": 5}], "topCountries": [{"countryCode": "US", "jobs": 10}],
  "topCompanies": [{"company": "Demo Systems (sample)", "jobs": 5}], "postedFrom": "2026-10-06", "postedTo": "2026-10-14"
}
```

Then one row per segment (`type: "segment"`):

```json
{
  "type": "segment", "segmentType": "country", "role": null, "countryCode": "US", "jobs": 10, "jobsWithSalary": 9,
  "salaryCurrency": "USD", "salarySample": 9, "salaryP25": 125000, "salaryP50": 147500, "salaryP75": 170000,
  "salaryInterval": "year", "remoteShare": 0.4, "remote": 4, "hybrid": 4, "onsite": 2,
  "salaryByCurrency": {"USD": {"sample": 9, "p25": 125000, "p50": 147500, "p75": 170000, "interval": "year"}},
  "topSkills": [{"skill": "SQL", "jobs": 4}], "topCompanies": [{"company": "Demo Systems (sample)", "jobs": 2}]
}
```

In a segment row `salaryCurrency` and the `salaryP*` columns show the currency with the most salaries in that segment;
all currencies are in `salaryByCurrency`. Run statistics are in the key-value store as `STATS`.

### Pricing

Pay per event: **$0.10 per report** (one per run; not charged when no jobs could be analyzed) and **$5 per 1,000
segments** ($0.005 per segment row). A typical run — one report and 30 segments — costs about **$0.25**, whatever the
number of jobs analyzed. `maxSegments` and the maximum cost per run in Apify cap the spend; the Actor stops cleanly when
the limit is reached. The demo is free.

### Notes and limits

- This Actor only reads datasets you already own; it does not access any job site. You are responsible for how the
  underlying data was collected and used.
- **The numbers describe your dataset, not the whole market.** A scrape of one job board, one search term or one week is a
  sample; small segments are noisy, so percentiles need `minSalarySample` salaries.
- Role families come from keyword rules on the job title (about 25 groups); unusual titles fall into "Other".
  Skills are keyword matches, so "Go" is counted only as "Golang" or "Go developer/programming" and a skill mentioned in a
  "nice to have" line counts too.
- Salaries without a currency or interval are left out of percentiles (and counted in `salaryUnusable`). Annualization uses
  fixed factors; no currency conversion and no cost-of-living adjustment.
- No personal data is output: no job texts, URLs, applicant or recruiter names. Company names appear only as hiring counts.

### More tools from the same developer

- [Job Postings Normalizer](https://apify.com/ucchiii/job-postings-normalizer) — unify fields, salaries and dates from any job scraper and remove duplicates
- [Job Listings API](https://apify.com/ucchiii/career-site-jobs) — every open job from Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee career sites
- [Dataset Merge & Dedup](https://apify.com/ucchiii/dataset-dedup-merge) — merge Apify datasets and remove duplicates by the fields you choose
- [Google Sheets Export](https://apify.com/ucchiii/google-sheets-export) — export an Apify dataset to Google Sheets

# Actor input Schema

## `datasetIds` (type: `array`):

Dataset IDs (the 'defaultDatasetId' of a finished run of Job Postings Normalizer, e.g. 'WkzbQMuFYuamGv3YF'). Fields read: title, company, countryCode, workplaceType, salaryMin/salaryMax/salaryCurrency/salaryInterval, postedAt, description. Several runs are merged and deduplicated. Leave empty to run a free demo on fictional sample jobs (not charged).

## `segmentBy` (type: `string`):

What the segment rows group jobs by. 'role' groups by a role family derived from the job title, 'country' by countryCode, 'roleAndCountry' by both. Each segment row is charged separately; the report row always covers all jobs.

## `maxSegments` (type: `integer`):

Write at most this many segment rows, largest segments first (by number of jobs). Minimum 0 (report row only), maximum 200. This also caps the segment charges of a run.

## `minSalarySample` (type: `integer`):

Salary percentiles (p25/p50/p75) are only shown when at least this many jobs of the same currency have a usable salary; smaller samples give null. Minimum 1, maximum 1000.

## `topSkills` (type: `integer`):

How many of the most frequent skills to list in the report row. Minimum 1, maximum 50.

## `extraSkills` (type: `array`):

Additional skills or tools to count in job titles and descriptions (e.g. \['Looker Studio', 'Terraform Cloud']), on top of the built-in list of about 100 common skills. Case-insensitive whole-word match. Empty = built-in list only.

## `dedup` (type: `boolean`):

Count each job once when it appears in several datasets: same URL first, then same company + title + location.

## Actor input object example

```json
{
  "segmentBy": "role",
  "maxSegments": 30,
  "minSalarySample": 3,
  "topSkills": 20,
  "dedup": true
}
```

# Actor output Schema

## `results` (type: `string`):

First row (type 'report'): jobsAnalyzed, salaryByCurrency (p25/p50/p75), workplace (remoteShare), topSkills, topRoles, topCountries, topCompanies. Then one row per segment (type 'segment'): jobs, salary percentiles, remoteShare, topSkills.

## `report` (type: `string`):

Record REPORT: the same report as the first dataset row, as a single JSON document.

## `stats` (type: `string`):

Record STATS: items read, skipped, duplicates, jobs analyzed, segments written.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("ucchiii/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 = {}

# Run the Actor and wait for it to finish
run = client.actor("ucchiii/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 '{}' |
apify call ucchiii/job-market-report --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ucchiii/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/375VeiVEoGgTkpW3i/builds/2bp5DvGWhprZuLI2P/openapi.json
