# Job Posting Classifier — remote, visa, salary · $0.5/1k (`leoworks/job-posting-classifier`) Actor

Filter LinkedIn jobs and Indeed or Glassdoor postings: remote/hybrid/on-site, visa sponsorship jobs, USD salary. Job posting classifier. Use for job data; for company sites use leoworks/company-website-classifier. datasetId = jobs scraper output, or jobs JSON. ~180 jobs/9 s, pay per job.

- **URL**: https://apify.com/leoworks/job-posting-classifier.md
- **Developed by:** [Leoworks](https://apify.com/leoworks) (community)
- **Categories:** Jobs, AI, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.43 / 1,000 job posting classifications

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 Posting Classifier — remote, experience, visa & salary

**Part of LeoWorks lead and hiring data tools:** places → leads ([Google Maps Lead Qualifier](https://apify.com/leoworks/google-maps-lead-qualifier)) · company websites → ICP fit ([Company Website Classifier](https://apify.com/leoworks/company-website-classifier)) · job postings → filters ([Job Posting Classifier](https://apify.com/leoworks/job-posting-classifier)).

**For recruiters, job boards, HR-tech, sales teams that sell to hiring companies, labour-market researchers and AI agents that already scrape job postings** — and need every posting tagged with how and where the work is done, how much experience it asks for, whether it is one of the visa sponsorship jobs and what it pays, for $0.50 per 1,000 postings, with no prompt writing or LLM key.

**LinkedIn removed its experience-level, job-type and remote search filters in August 2026** (as the most-used LinkedIn jobs scraper on Apify Store notes in its README). Scrape broadly, then filter LinkedIn jobs here — the answers come from the description, not from the board's own fields.

- **Work mode** — remote / hybrid / on-site / not stated
- **Minimum experience** — none (entry level) / 1–2 / 3–5 / 6+ years / not stated
- **Employment type** — full-time / part-time / contract / internship / temporary / not stated
- **Visa sponsorship** — offered / not offered (incl. "must be a US citizen or permanent resident") / not mentioned
- **Salary as numbers** — min, max, currency, period, and **annual USD** (hour × 2,080, day × 260, week × 52, month × 12) from the scraper's salary fields or the description
- **Skills** — the skills, tools or licences from your list that each posting mentions (matched by code, free)
- **Your own labels** — up to 10 yes/no criteria in plain language (e.g. "requires a security clearance")
- **Hiring companies** — `REPORT_COMPANIES.csv`: one row per company with open jobs, top titles, remote share, visa share, median salary, top skills and latest posting date

| You are | Use it to |
|---|---|
| Job seeker or job board | Keep only remote, entry-level or visa-sponsoring postings; add filters to a board without hand-tagging |
| Recruiter or HR-tech | Benchmark salary ranges and experience asks for a role; check pay transparency by company |
| Sales team selling to employers | `REPORT_COMPANIES.csv` = companies hiring right now, with what and where they hire |
| Labour-market researcher | Remote and hybrid shares, salary disclosure rates and skill demand by week, company or city |

**How it reads the posting:** each field is answered from the title, company, location and full description; the job board's own type field is used only when the description says nothing (it is often wrong — LinkedIn lists Plaid's $176k–$227k backend engineer role as "Volunteer"; this Actor answers full-time). Salary numbers, skills, dates, filters and the company report are code, not AI, and cost nothing extra. Our other classifiers: [Company Website Classifier](https://apify.com/leoworks/company-website-classifier), [Google Maps Lead Qualifier](https://apify.com/leoworks/google-maps-lead-qualifier), [Amazon Review Classifier](https://apify.com/leoworks/amazon-review-classifier), [Social Comment Classifier](https://apify.com/leoworks/social-comment-classifier).

### How to use

1. Scrape postings with a jobs scraper — for example [LinkedIn Jobs Scraper](https://apify.com/curious_coder/linkedin-jobs-scraper) or [Indeed Jobs Scraper](https://apify.com/valig/indeed-jobs-scraper) (more in **Works with**). Several searches and boards in one dataset are fine: duplicates are skipped.
2. Open this Actor and pick that dataset.
3. Optional: a list of **Skills to look for**, up to 10 **Custom labels**, and free filters — **Only postings from the last** (e.g. `14 days`), **Title must contain / must not contain**, **Max postings per company**.
4. Click **Start**. 180 postings take about 9 seconds.
5. Open the **Job postings** or **Salaries** view, or download `REPORT_COMPANIES.csv` (Output tab).

#### Run it automatically after every scrape

In the jobs scraper's Actor (or a saved task of it) → **Integrations** → **Connect Actor or task** → pick this Actor, event **Run succeeded**, and set the input to:

```json
{ "datasetId": "{{resource.defaultDatasetId}}", "skills": ["Python", "SQL"], "postedWithin": "7 days" }
```

Add an Apify **Schedule** on the scraper (e.g. every morning) and every new scrape is classified without you opening the console.

### Output sample

Real rows from run `MH5gM9XIKsNbxv9VR` (2026-10-11): 180 postings from eight searches — data scientists, accountants, sales representatives and warehouse associates from an Indeed jobs scraper; software engineers, registered nurses, marketing managers and data analyst internships from a LinkedIn jobs scraper — with 14 skills to look for.

| job | company | work mode | experience | type | visa | annual salary (USD) | skills |
|---|---|---|---|---|---|---|---|
| Strategic Planning Data Scientist | Corning | not stated | 3–5 (1.00) | full-time | **offered (1.00)** | 77,234 – 106,197 | Python, SQL, Excel, Power BI |
| Senior Data Scientist, RegLab | Stanford University | not stated | 3–5 (1.00) | **temporary (1.00)** | **offered (1.00)** | 117,134 – 130,006 | Python, SQL |
| Data Scientist II | ACT | **remote (1.00)** | 3–5 (1.00) | **temporary (1.00)** | **not offered (1.00)** | 95,000 – 105,000 | Python, SQL |
| Software Engineer II, Backend (Identity Decisioning) | Affirm | **remote (0.86)** | 1–2 (1.00) | full-time | not mentioned | 165,000 – 225,000 | Python, AWS, Kubernetes |
| Software Engineer, Backend | Plaid | not stated | 1–2 (1.00) | full-time (0.96) — *board says "Volunteer"* | not mentioned | 176,400 – 226,800 | — |
| Entry Level Sales Rep | EMBLM | remote (0.88) | **none (0.99)** | full-time | not mentioned | 50,000 – 70,000 | Salesforce |
| Experienced Warehouse Order Selector | Performance Foodservice | not stated | not stated | full-time | not mentioned | 44,928 *($21.60 per hr)* | Forklift |
| Registered Nurse | Betty Jean Kerr People's Health Centers | not stated | 1–2 (1.00) | full-time (0.67) | not mentioned | — | BLS |

Why those answers: Stanford's posting says "ONE-YEAR FIXED TERM position" and "Stanford may sponsor J-1 visas"; ACT's says "Fixed Term Position that will last 6 months" and "without the need for visa sponsorship"; Corning's says "This position supports immigration sponsorship". Each row also keeps the posting's `id`, `url`, `postedAt`, the board's seniority, applicant count, company size and industry when the scraper has them, and the probability and confidence of every answer.

### Input example

The form default — two pasted postings, no dataset needed (about $0.001, 2 seconds):

```json
{
  "jobs": [
    {
      "title": "Senior Data Analyst",
      "company": "Example Co",
      "location": "Chicago, IL",
      "description": "Hybrid role: 3 days a week in our Chicago office. 5+ years of SQL and BI experience required. Full-time. Salary $110,000–$130,000 per year. We are unable to sponsor work visas."
    },
    {
      "title": "Warehouse Associate",
      "company": "Example Logistics",
      "location": "Dallas, TX",
      "description": "Entry level — no experience needed, we train you. Part-time evening shifts, in person at our Dallas warehouse. $18/hour."
    }
  ]
}
```

To classify a jobs scraper's output, pass its dataset instead. Filters run before classification, so skipped postings are not charged:

```json
{
  "datasetId": "YOUR_JOBS_DATASET_ID",
  "skills": ["Python", "SQL", "AWS", "Go", "Kubernetes"],
  "titleIncludes": ["data scien", "engineer"],
  "titleExcludes": ["intern"],
  "postedWithin": "14 days",
  "maxPerCompany": 2
}
```

With exactly this input on 210 postings (the 180 above plus 30 repeated), run `OJGMvSRiUS8ADar5v` classified 38, skipped 28 duplicates and 144 postings outside the filters — **38 charged**.

### Pricing

Pay only for classified postings — no subscription.

| Event | Price | When |
|---|---|---|
| `job-classified` | $0.0005 | One job posting classified (work mode, required experience, employment type, visa sponsorship, salary disclosed and any custom labels). |

That is **$0.50 per 1,000 postings**. **First run with the form defaults: about $0.001** (2 postings, 2 seconds).

**Cost examples**

| Postings | Cost |
|---|---|
| 100 | $0.05 |
| 1,000 | $0.50 |
| 10,000 | $5.00 |
| 100,000 | $50.00 |

With the free $5 monthly Apify credit you can classify about **10,000 postings**.

Postings without a title, with a description shorter than **Minimum description length** (120 characters by default — some scrapers return titles only), duplicates and postings removed by your filters are skipped and **not charged**. Postings that fail after retries are reported with an `error` field and **not charged**. If you set a maximum cost per run, the Actor stops cleanly when it is reached.

### Works with

Field names are matched ignoring case, `_` and `-`, including nested fields (`company.name`, `description.text`, `salary.min`). Checked against real output (2026-10-10/11) and, for the others, a test row built from each scraper's published output schema.

| Scraper on Apify Store | Checked | Notes |
|---|---|---|
| [LinkedIn Jobs Scraper (curious_coder)](https://apify.com/curious_coder/linkedin-jobs-scraper) | real output | salary text → numbers; seniority, applicants, industry kept |
| [Indeed Jobs Scraper (valig)](https://apify.com/valig/indeed-jobs-scraper) | real output | `baseSalary`, `employer.name`, location object, company size, industry |
| [LinkedIn Jobs Scraper (valig)](https://apify.com/valig/linkedin-jobs-scraper), [bebity](https://apify.com/bebity/linkedin-jobs-scraper), [cheap_scraper](https://apify.com/cheap_scraper/linkedin-job-scraper) | test row | `contractType`; `salaryInfo` list |
| [Indeed Scraper (borderline)](https://apify.com/borderline/indeed-scraper), [misceres](https://apify.com/misceres/indeed-scraper), [kaix](https://apify.com/kaix/indeed-scraper), [curious_coder Indeed](https://apify.com/curious_coder/indeed-scraper) | test row | `jobType` lists, `salary` objects, `formattedAddressShort`, `urls.indeed`, `companyDetails.name` |
| [Advanced LinkedIn Job Search API (fantastic-jobs)](https://apify.com/fantastic-jobs/advanced-linkedin-job-search-api) | test row | `organization`, `description_text`, `locations_derived`, `ai_salary_*`, numeric `id` |
| Glassdoor scrapers ([valig](https://apify.com/valig/glassdoor-jobs-scraper), [bebity](https://apify.com/bebity/glassdoor-jobs-scraper), [cheap_scraper](https://apify.com/cheap_scraper/glassdoor-jobs-scraper-remove-duplicate-jobs)) | test row | `pay`, `job_salary`, `baseSalary_min/max/period`, `location.name`, `location_city` + `location_state` |
| Google Jobs and JobSpy-style scrapers (`job_title`, `company_name`, `job_url`, `detected_extensions`, `salary_min` / `salary_max` / `salary_interval`) | test row | "3 days ago" dates read |
| [hirebase](https://apify.com/hirebase/job-search) | test row | `salaryRange`, `applicationLink` |

A dataset whose postings have titles but no description (some scrapers return list pages only) is not classified from titles alone: those rows are skipped and not charged, and if nothing could be classified the run stops with the field names it found. **Also:** the Apify API and JavaScript/Python clients · Apify Schedules and Integrations (above) · Claude, Cursor and Claude Code through the Apify MCP server (below).

### Call it from code

```bash
curl -X POST "https://api.apify.com/v2/acts/leoworks~job-posting-classifier/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"datasetId": "YOUR_JOBS_DATASET_ID", "skills": ["Python", "SQL"]}'
```

JavaScript (`npm install apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('leoworks/job-posting-classifier').call({ datasetId: 'YOUR_JOBS_DATASET_ID', postedWithin: '7 days' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const remoteSponsoring = items.filter((j) => j.workMode === 'remote' && j.visa === 'offered');
```

Python (`pip install apify-client`):

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("leoworks/job-posting-classifier").call(run_input={"datasetId": "YOUR_JOBS_DATASET_ID"})
well_paid = [j for j in client.dataset(run["defaultDatasetId"]).list_items().items if (j.get("salaryAnnualMinUsd") or 0) >= 100000]
companies_csv = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("REPORT_COMPANIES.csv")
```

### Use with Claude, Cursor or Claude Code (MCP)

**Ask your AI agent.** Add the Apify MCP server with this Actor as a tool, then say, for example:

- *"Which of these jobs are remote and sponsor visas? Job 1: “Staff Engineer — fully remote (US time zones). We sponsor H-1B for the right candidate. $160k–$190k per year.” Job 2: “Office Manager — on-site in Denver. We are unable to sponsor work visas.”"*
- *"From dataset YOUR_DATASET_ID (my LinkedIn jobs scraper run), list the entry-level remote jobs that mention Python or SQL, with the annual salary in USD."*
- *"Tag this posting and give me the pay as an annual USD figure: “Warehouse Associate, part-time evening shifts, in person at our Dallas warehouse. $18/hour, no experience needed.”"*

The agent calls the tool `leoworks--job-posting-classifier` and reads the labels with `get-dataset-items`. Replace `YOUR_DATASET_ID` with the ID of your own dataset.

Claude Desktop or Cursor (`mcp.json`):

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=leoworks/job-posting-classifier",
      "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
    }
  }
}
```

Claude Code: `claude mcp add --transport http apify "https://mcp.apify.com?tools=leoworks/job-posting-classifier" --header "Authorization: Bearer YOUR_APIFY_TOKEN"`. Leave out the header to sign in with OAuth in the browser instead. We verified this setup with the Apify MCP server (v0.17.4) on 2026-10-10: the agent classified a pasted posting in 7 seconds end to end (run `0Xasgsc22J1HhTJi3`).

### Output (one row per posting)

```json
{
  "id": "2ee401cc411b433b",
  "url": "https://www.indeed.com/viewjob?jk=2ee401cc411b433b",
  "title": "Strategic Planning Data Scientist",
  "company": "Corning",
  "location": "Corning, NY, US",
  "postedAt": "2026-10-09T05:00:00.000Z",
  "companySize": "10,000+",
  "companyIndustry": "Industrial Manufacturing",
  "workMode": "not_stated",
  "experience": "3-5",
  "employmentType": "full_time",
  "visa": "offered",
  "salaryDisclosed": true,
  "salaryText": "$77,234 - $106,197 / year",
  "salaryMin": 77234,
  "salaryMax": 106197,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salaryAnnualMinUsd": 77234,
  "salaryAnnualMaxUsd": 106197,
  "skills": "Python, SQL, Excel, Power BI",
  "labels": {
    "workMode": { "label": "not_stated", "probability": 0.85, "confidence": 0.8 },
    "experience": { "label": "3-5", "probability": 1, "confidence": 1 },
    "employmentType": { "label": "full_time", "probability": 1, "confidence": 1 },
    "visa": { "label": "offered", "probability": 1, "confidence": 1 }
  },
  "model": "jev-1.13.0"
}
```

The flat columns (`workMode`, `experience`, `employmentType`, `visa`, `customLabelsMatched`) repeat the answers in `labels` for spreadsheets and filters; `labels` keeps every probability. `experience` is the **minimum** asked for, including preferred experience ("1 year preferred" counts as 1–2). Salary fields are code, not AI: a number in the scraper's salary field, or a dollar amount stated as pay in the description — company figures ("raised $1.4B") and benefits ("sign-on bonus up to $15,000", "$10 copays") do not count. `salaryAnnualMinUsd` / `MaxUsd` are filled only for US-dollar salaries with a known period. `postedAt`, `seniority`, `applicants`, `companySize` and `companyIndustry` are copied from the scraper when it has them.

#### Hiring companies (REPORT_COMPANIES.csv)

One row per company, most open jobs first (Output tab → *Hiring companies*). From the same run:

| company | openJobs | topTitles | onsiteShare | salaryDisclosedShare | medianAnnualSalaryUsd | topSkills | latestPostedAt |
|---|---|---|---|---|---|---|---|
| Performance Foodservice | 3 | Experienced Warehouse Order Selector; Order Picker; Sales Representative | 0 | 0.667 | 44,824 | Forklift | 2026-10-09 |
| CommonSpirit Health | 2 | Clinic RN Queen Creek; RN Medical | 0 | 1 | 118,753 | — | 2026-10-10 |
| Cleveland Clinic | 2 | RN - Orthopaedics; RN - Orthopedics | 0.5 | 1 | 73,840 | BLS | 2026-09-04 |

Columns: `company, openJobs, topTitles, remoteShare, hybridShare, onsiteShare, visaOfferedShare, salaryDisclosedShare, medianAnnualSalaryUsd, topSkills, topLocations, latestPostedAt, companySize, companyIndustry`.

#### Summary (REPORT)

The **REPORT** record has the same figures for all postings together and for every company, plus top titles, top locations and postings per week (last 26 weeks). From the same run:

```json
{
  "overall": {
    "jobs": 180,
    "salaryDisclosedShare": 0.667,
    "medianAnnualSalaryUsd": 82500,
    "workMode": { "not_stated": 0.472, "onsite": 0.294, "hybrid": 0.156, "remote": 0.078 },
    "experience": { "1-2": 0.311, "3-5": 0.267, "not_stated": 0.25, "none": 0.144, "6+": 0.028 },
    "visa": { "not_mentioned": 0.872, "not_offered": 0.111, "offered": 0.017 },
    "skills": [{ "skill": "Python", "jobs": 38, "share": 0.211 }, { "skill": "Excel", "jobs": 37, "share": 0.206 }, { "skill": "SQL", "jobs": 34, "share": 0.189 }]
  },
  "companies": [{ "company": "Performance Foodservice", "jobs": 3, "salaryDisclosedShare": 0.667, "medianAnnualSalaryUsd": 44824 }]
}
```

### Accuracy

Measured on 174 hand-labelled English US job postings (2026-10-10) across eight searches — data analyst internships, marketing managers, registered nurses and software engineers from LinkedIn; accountants, data scientists, sales representatives and warehouse associates from Indeed. The questions were adjusted on one part, then checked once on a separate part that was not used for adjusting.

| Set | Postings | Work mode | Experience | Employment type | Visa | All four right |
|---|---|---|---|---|---|---|
| **Check set — not used for adjusting** | 100 | **90%** | **97%** | **96%** | **100%** | **85%** |
| Adjusting set | 74 | 92% | 95% | 99% | 99% | 84% |

A field counts as right when it matches the hand label, or an alternative noted for postings that fit two answers (e.g. a nurse posting at a named hospital that never says "on-site" accepts both *not stated* and *on-site*). In the check set, 7 of 8 postings that refuse sponsorship were found. Few postings in the sample **offer** sponsorship (2 of 174), so treat that answer's accuracy as unmeasured. The most common miss is *on-site* for a posting that never says where the work is done (9 of 29 on-site answers in the check set were wrong) — when that matters, check on-site answers by hand; *remote* and *hybrid* answers were right in every case but one. Salary detection was checked by hand on all 180 scraped postings: no benefit or company figure was counted as salary. Automated labels can be wrong; check samples before making big decisions.

### Limits

| Item | Limit |
|---|---|
| Posting length | First 12,000 characters of the description are used; descriptions under 120 characters are skipped (adjustable) |
| Custom labels | Up to 10, each up to 200 characters |
| Skills | Up to 100. Whole words, not case-sensitive, except short skills typed with a capital (`Go`, `R`, `C`), which must appear as typed and not inside a hyphenated word ("Go-to-Market") |
| Dataset size | Any — datasets are read in pages of 1,000 |
| Language | English (measured). Other languages are accepted and usually work, but accuracy has not been measured — treat them as **beta** |
| Salary | Structured salary fields in any currency; salary text in $, £, €, ₹, ¥, C$, A$. Annual USD only for US-dollar salaries |
| Duplicates | Same job ID or URL, or same title + company + city (across boards). Turn off **Skip the same job posted twice** to keep them |
| Speed | 180 postings in about 9 seconds |
| Data | Only the title, company, location, description and the board's type field are sent for classification; recruiter contact details are not output |

### FAQ

**Which AI makes the judgments?** Jev, TypeSafe's decision model (version `jev-1.13.0`, pinned). Jev answers each field with a calibrated probability instead of generated text, so a 0.95 means the same thing across postings. Answers are stable but not fixed: in three runs on the same 180 postings, 2 to 5 of the 720 answers differed between runs — almost all between *not stated* and a definite answer, on postings that are vague about it. If one posting must never flip, filter on probability (e.g. keep `labels.workMode.probability` ≥ 0.9).

**Does it scrape LinkedIn or Indeed?** No. It classifies postings you already have — run a jobs scraper first (see **Works with**) or paste postings.

**LinkedIn removed its remote, experience and job-type filters — does this replace them?** Yes, after the scrape: search broadly (title and location), then keep `workMode = remote`, `experience = none` or `employmentType = internship` here. The answers come from the description, so they also catch postings whose board fields are wrong or empty.

**How is this different from job APIs with AI fields (e.g. fantastic-jobs)?** Those sell their own job database with AI fields included, at about $5–$12 per 1,000 postings. This Actor adds the fields to postings from any scraper or source you already use, for $0.50 per 1,000 (plus the scraper — valig's Indeed scraper costs about $0.10 per 1,000, so the pair is about $0.60). Choose a database API when you need their coverage; choose this when you have your own source or need visa, skills and custom labels on it.

**Why not ask ChatGPT?** You can for a handful. For thousands of postings you would write and test prompts, handle rate limits and parse free text, and the answers would not come with a probability you can filter on. Here each answer is one of a fixed set of labels with a probability, salary numbers are parsed by code, and the cost is $0.50 per 1,000.

**Why "not stated" so often for work mode?** Many postings never say whether the job is remote or on site. The Actor answers *not stated* rather than guessing from the job type, so a filter on *remote* returns postings that actually say remote.

**Which postings are skipped and not charged?** Postings without a title, with a description shorter than 120 characters, duplicates, postings older than **Only postings from the last**, postings outside your title filters, and postings over **Max postings per company**. The status message counts each reason.

### Disclaimer

> Independent tool — not affiliated with, endorsed by or sponsored by LinkedIn, Indeed, or the authors of the scrapers listed above. Names are used only to describe compatible data sources. The postings shown are public job postings used as examples.

### Reviews and support

If this Actor saved you time, a short review on Apify Store helps others find it. Questions or a dataset whose fields are not detected? Open an issue in the **Issues** tab — we answer within a day.

### Changelog

See the Changelog tab.

# Changelog

This Actor's version history is a separate document: https://apify.com/leoworks/job-posting-classifier/changelog.md

# Actor input Schema

## `datasetId` (type: `string`):

An Apify dataset of job postings — for example the output of a LinkedIn or Indeed jobs scraper. Use this OR “Job postings”.

## `jobs` (type: `array`):

Postings pasted as JSON objects (title, company, location, description) or plain description strings. Use this OR “Job postings dataset”.

## `skills` (type: `array`):

Up to 100 skills, tools or keywords (e.g. Python, SQL, Salesforce, CDL, RN license). Each posting gets a `skills` column with the ones it mentions — whole words, matched by code at no extra charge. Not case-sensitive, except short skills typed with a capital (`Go`, `R`, `C`): those must appear as typed, so the word "go" or "Go-to-Market" does not count as Go.

## `customLabels` (type: `array`):

Up to 10 extra yes/no checks (each up to 200 characters) in plain language, e.g. “requires a security clearance”, “mentions Python”, “offers a signing bonus”. Each gets a probability.

## `postedWithin` (type: `string`):

e.g. `7 days`, `2 weeks`, `1 month`. Older postings are skipped (not charged). Postings without a date are kept.

## `titleIncludes` (type: `array`):

Free filter on the job title, e.g. "engineer", "nurse". Not case-sensitive.

## `titleExcludes` (type: `array`):

Free filter, e.g. "intern", "senior", "manager".

## `maxPerCompany` (type: `integer`):

Classify at most this many postings per company. 0 = no limit.

## `dedupeAcrossBoards` (type: `boolean`):

The same job ID, or the same title + company + city (e.g. from LinkedIn and Indeed), is classified and charged once.

## `minDescriptionChars` (type: `integer`):

Dataset postings with a shorter description (or none — some scrapers only return titles) are skipped and not charged: a title alone is not enough to judge work mode, experience or visa.

## `labelThreshold` (type: `number`):

Minimum probability (0–1) for a custom label to be marked as matched.

## `maxItems` (type: `integer`):

Classify at most this many postings (0 = all).

## `maxConcurrency` (type: `integer`):

Postings classified in parallel.

## `healthCheck` (type: `boolean`):

Internal: fail the run when results look degraded (used by the developer's scheduled checks).

## Actor input object example

```json
{
  "jobs": [
    {
      "title": "Senior Data Analyst",
      "company": "Example Co",
      "location": "Chicago, IL",
      "description": "Hybrid role: 3 days a week in our Chicago office. 5+ years of SQL and BI experience required. Full-time. Salary $110,000–$130,000 per year. We are unable to sponsor work visas."
    },
    {
      "title": "Warehouse Associate",
      "company": "Example Logistics",
      "location": "Dallas, TX",
      "description": "Entry level — no experience needed, we train you. Part-time evening shifts, in person at our Dallas warehouse. $18/hour."
    }
  ],
  "skills": [
    "Python",
    "SQL",
    "AWS",
    "Excel",
    "Salesforce"
  ],
  "customLabels": [],
  "titleIncludes": [],
  "titleExcludes": [],
  "maxPerCompany": 0,
  "dedupeAcrossBoards": true,
  "minDescriptionChars": 120,
  "labelThreshold": 0.5,
  "maxItems": 0,
  "maxConcurrency": 10,
  "healthCheck": false
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

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

No description

## `companiesCsv` (type: `string`):

No description

# 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 = {
    "jobs": [
        {
            "title": "Senior Data Analyst",
            "company": "Example Co",
            "location": "Chicago, IL",
            "description": "Hybrid role: 3 days a week in our Chicago office. 5+ years of SQL and BI experience required. Full-time. Salary $110,000–$130,000 per year. We are unable to sponsor work visas."
        },
        {
            "title": "Warehouse Associate",
            "company": "Example Logistics",
            "location": "Dallas, TX",
            "description": "Entry level — no experience needed, we train you. Part-time evening shifts, in person at our Dallas warehouse. $18/hour."
        }
    ],
    "skills": [
        "Python",
        "SQL",
        "AWS",
        "Excel",
        "Salesforce"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("leoworks/job-posting-classifier").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 = {
    "jobs": [
        {
            "title": "Senior Data Analyst",
            "company": "Example Co",
            "location": "Chicago, IL",
            "description": "Hybrid role: 3 days a week in our Chicago office. 5+ years of SQL and BI experience required. Full-time. Salary $110,000–$130,000 per year. We are unable to sponsor work visas.",
        },
        {
            "title": "Warehouse Associate",
            "company": "Example Logistics",
            "location": "Dallas, TX",
            "description": "Entry level — no experience needed, we train you. Part-time evening shifts, in person at our Dallas warehouse. $18/hour.",
        },
    ],
    "skills": [
        "Python",
        "SQL",
        "AWS",
        "Excel",
        "Salesforce",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("leoworks/job-posting-classifier").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 '{
  "jobs": [
    {
      "title": "Senior Data Analyst",
      "company": "Example Co",
      "location": "Chicago, IL",
      "description": "Hybrid role: 3 days a week in our Chicago office. 5+ years of SQL and BI experience required. Full-time. Salary $110,000–$130,000 per year. We are unable to sponsor work visas."
    },
    {
      "title": "Warehouse Associate",
      "company": "Example Logistics",
      "location": "Dallas, TX",
      "description": "Entry level — no experience needed, we train you. Part-time evening shifts, in person at our Dallas warehouse. $18/hour."
    }
  ],
  "skills": [
    "Python",
    "SQL",
    "AWS",
    "Excel",
    "Salesforce"
  ]
}' |
apify call leoworks/job-posting-classifier --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,leoworks/job-posting-classifier"
        }
    }
}
```

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/E05xrjqzEa7EWKO9R/builds/9oZBMcIvg6DX18gJA/openapi.json
