# LinkedIn Jobs Scraper | Multi-Role & Remote Search (`herazur/all-jobs-search-api`) Actor

Search LinkedIn jobs by role, location and date. Verify remote work from job content, merge duplicates, and export descriptions, company details and direct LinkedIn job URLs.

- **URL**: https://apify.com/herazur/all-jobs-search-api.md
- **Developed by:** [Furkan Toluç](https://apify.com/herazur) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 job scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## All Jobs Search API

Search Greenhouse, Lever, Ashby and Workday career boards with one Apify Actor. Supply roles, keywords, locations, remote preference and a date window. Company URLs are optional: external search discovers hosted ATS boards, and a small built-in catalog provides fallback coverage.

This repository contains a runnable TypeScript MVP, an Apify input form and dataset view, a Docker build, a local JSONL CLI, provider adapters, persistent monitoring, and automated tests. It is deployed as a private Actor in the owner's account: [open All Jobs Search API](https://console.apify.com/actors/6c9OeBz1Ra1Zaw5Tb). It is not publicly listed in Apify Store. See `docs/DEPLOYMENT.md` for cloud test evidence.

### Quick start

Requires Node.js 22 or later.

```sh
npm ci
npm run build
npm test
npm run search -- examples/input.json output/search
```

Results: `output/search/jobs.jsonl`. Coverage and failures: `output/search/run-report.json`. An empty dataset can be a legitimate outcome for narrow filters; inspect the report before treating it as “no jobs exist.”

```json
{
  "roles": ["AI Automation Specialist", "QA Automation Engineer"],
  "locations": ["Turkey", "Europe"],
  "remote": true,
  "postedWithinDays": 7,
  "maxResults": 1000
}
```

For a broader first trial, use `roles: ["Engineer"]` and omit the date/location filters. The supplied example intentionally keeps the original narrow search.

Set **either** `BRAVE_SEARCH_API_KEY` or `SERPAPI_API_KEY` in your shell or Actor environment to enable discovery. These are operator settings; end users do not have to provide keys or companies. Optional secret input fields override the operator keys. `.env` files are not automatically loaded; Node's `--env-file` or your shell can supply them. Never commit actual credentials.

Without a key, the Actor works with four fallback employers: Stripe, CI\&T, Ashby and NVIDIA. This is a starter catalog, not a substitute for broad discovery. Hosted ATS URLs can supplement it through `careerUrls`.

### Architecture

```text
Input validation → provider-independent company discovery
                    Brave → SerpAPI failover
                            ↓
              Optional URLs + discovered ATS boards + catalog
                            ↓
           Greenhouse / Lever / Ashby / Workday connectors
                            ↓
          Normalization → filters → deduplication → monitoring
                            ↓
                 Dataset / JSONL + coverage report
```

`src/discovery.ts` defines the small `SearchProvider` interface. A provider implements `search(query): Promise<SearchHit[]>`; connector code has no search-vendor dependency. Add another adapter and register it in `providersFor` to support another vendor.

`discovery: "auto"` tries configured Brave first, then SerpAPI on a failed request or no usable ATS hits. A failing provider is disabled for the remainder of the run. Discovery failure never discards supplied URLs or the catalog. The catalog also supplements successful discovery. Explicit URLs take priority, followed by discovered boards, then the catalog, subject to `maxBoards`.

Discovery extracts ATS board URLs from indexed job/board search results. This MVP does not crawl arbitrary company domains to find embedded career links. `careerUrls` accepts hosted board or job URLs; unsupported custom domains are reported and skipped. Search results are used to locate boards; job records come from the ATS feeds, not search snippets.

### Search behavior

| Input | Behavior |
| --- | --- |
| `roles` | OR between roles; all normalized words of a role must occur in the title. No semantic synonym expansion. |
| `keywords` | OR between phrases in title/description; combined with roles using AND. |
| `locations` | OR between locations, matched against listed primary/secondary locations and country fields. |
| `remote: true` | Explicit remote metadata or remote wording in the location is required. |
| `remote: false` | Explicit non-remote or hybrid records; unknown status is excluded. Omit for all workplace types. |
| `postedWithinDays` | Rolling window on the source date, never on `firstSeenAt`; future dates excluded. |
| `unknownDatePolicy` | `exclude` by default when a date filter is active; `include` opts in to unknown dates. |
| `maxResults` | Delivered unique jobs after filtering and monitoring; default 1,000. |
| `maxDiscoveryQueries` | Default 8 logical queries. Failover can call both providers; HTTP retries add requests. |
| `maxBoards` | Default 20 selected boards. Omitted board count appears in the report. |
| `maxJobsPerBoard` | Default 250. Lever/Workday: listing scan cap. Greenhouse/Ashby: title-matched processing cap after the full feed download. |
| `enrich` | Adds a small deterministic skills dictionary and title-based seniority extraction. No LLM requests. |

Europe and Turkey matching uses a limited country/city alias dictionary in `src/filters.ts`. It is not a geocoder or a work-authorization check. City-only locations can be ambiguous. Generic “Remote” or “Worldwide” does not bypass a location constraint. “EMEA” is not treated as synonymous with Europe. Unknown remote status is `null`, not `false`.

Workday uses the public career site's CXS endpoints. They are site implementation details, not a guaranteed public developer API contract. Listing scans are bounded; detail requests are made only for title-matching jobs. A changed endpoint, blocked request, partial page, failed detail, or scan cap appears in the report. There is no login, CAPTCHA solver, proxy rotation or application submission.

### Output contract

Every row includes the proposed common fields plus provenance and salary units:

```json
{
  "title": "Senior QA Automation Engineer",
  "company": "Example",
  "location": "Berlin, Germany",
  "locations": ["Berlin, Germany"],
  "countries": ["DE"],
  "remote": true,
  "workplaceType": "remote",
  "employmentType": "Full-time",
  "salaryMin": 60000,
  "salaryMax": 80000,
  "currency": "EUR",
  "salaryInterval": "year",
  "description": "Build automated quality checks with Python and Playwright.",
  "skills": ["Python", "Playwright"],
  "experienceLevel": "Senior",
  "postedAt": "2026-09-10T10:00:00.000Z",
  "postedAtKind": "lastPublished",
  "updatedAt": null,
  "source": "ashby",
  "sourceBoard": "https://jobs.ashbyhq.com/example",
  "applyUrl": "https://jobs.ashbyhq.com/example/example-job/application",
  "jobId": "example-job",
  "requisitionId": null,
  "firstSeenAt": "2026-09-12T12:00:00.000Z",
  "isNew": true,
  "uniqueId": "stable-sha256-identifier",
  "sources": [{
    "source": "ashby",
    "board": "https://jobs.ashbyhq.com/example",
    "jobId": "example-job",
    "applyUrl": "https://jobs.ashbyhq.com/example/example-job/application"
  }]
}
```

The example is synthetic. Missing salary, seniority or dates remain `null`; basic-mode skills are `[]`. Salaries are taken from structured Lever/Ashby compensation fields, retain their original interval and currency, and are not inferred from prose or converted to annual pay. Greenhouse/Workday salary extraction and external company enrichment are not implemented. For discovered boards without a supplied organization name, `company` falls back to the ATS board slug; Greenhouse can supply its actual company name.

Date provenance is explicit: Greenhouse `first_published` and Workday `startDate` map to `published`; Lever `createdAt` maps to `created` (not guaranteed publication time); Ashby `publishedAt` maps to `lastPublished`. Greenhouse `updated_at` is only `updatedAt`. Relative Workday labels such as “30+ days ago” are never turned into invented exact dates.

### Deduplication and monitoring

Default `deduplication: "strict"` merges identical ATS board/job identities or canonical application URLs, stripping known tracking parameters and keeping job query identifiers. Distinct jobs with the same title remain separate. `crossSource` also merges exact normalized company/title/location/description matches across ATS sources, provided the description is sufficiently long and the match is not ambiguous within one source. It is conservative and cannot guarantee every duplicate is recognized. The `sources` array retains all merged provenance.

```json
{
  "roles": ["Software Engineer"],
  "locations": ["Europe"],
  "remote": true,
  "monitor": true,
  "onlyNewJobs": true,
  "monitorId": "europe-software-jobs"
}
```

Schedule this input daily in Apify after deployment. Creating a schedule is separate from running this code; the repository does not create one automatically.

The first monitoring run returns a baseline of matching jobs. Subsequent runs suppress delivered identities and preserve their first delivery timestamp. `isNew` means **not previously delivered in this monitor scope**, not “published since yesterday.” Use the date filter separately when a publication/creation window is needed. This distinction handles boards with unknown dates and older jobs discovered for the first time.

History uses the named Apify key-value store `all-jobs-search-history-v1`, which persists across independent runs. The scope includes `monitorId` and normalized search filters; changing filters starts a separate history. Limits, discovery vendor, URL order and `onlyNewJobs` do not reset it. Without `monitor`, each invocation has no persistent history and all returned rows are new observations for that invocation.

Only successfully delivered records are marked seen. Filtered records, result-limit overflow and budget-rejected records remain eligible. Matching aliases of previously seen duplicates are also remembered. Delivery and history writes are separate operations: a crash between them may redeliver a record. Consumers needing crash-safe idempotency should use `uniqueId` and source identities. Run **one cloud run per monitor scope at a time**; Apify KV storage does not provide transactional locking here. The local CLI uses an exclusive scope lock; after a killed process, remove its stale `.lock` only after confirming the process is stopped. History has no automatic retention expiry; changing `monitorId` creates a fresh scope.

### Apify and billing

Import this repository as an Actor source and use `.actor/actor.json`. The Dockerfile builds the TypeScript entry point. Configure the search provider key in the Actor environment. Output rows go to the default dataset; `RUN_REPORT` in the default key-value store contains source errors, truncation, discovery fallback, delivered counts and run status. All boards failing makes the Actor fail; partial successes remain usable.

Once the Actor is deployed, use the Apify run API with your Actor ID. For example, `POST /v2/acts/{actorId}/runs` with the input JSON, wait for completion, then retrieve its default dataset. There is no separate HTTP server in this MVP; Apify provides the API and scheduling.

Optional PPE hooks use `Actor.pushData(job, eventName)` after deduplication and monitoring. Configure pricing events in Apify before setting `JOB_RESULT_EVENT` and `ENRICHED_JOB_RESULT_EVENT`; they are operator environment settings, not user input. Unregistered events and conflicting automatic dataset events are rejected to avoid accidental free or double charging. Budget-rejected rows are not marked seen. No charges happen in the ordinary local CLI.

The proposed $1/1K basic and $3/1K enriched results are **unvalidated pricing ideas**, not active prices. Discovery queries, Workday detail requests, persistent KV operations, low-yield searches and platform compute all affect margins. This MVP's enrichment is deterministic; it does not include company research. Benchmark those costs before enabling paid publication. Cloud billing behavior has not been exercised against an actual paid run.

### Verification and extension

```sh
npm run check
npm test
npm run build
npm run smoke
```

`npm test` uses synthetic fixtures for source contracts, discovery failover, filters, pagination, retry behavior, deduplication and monitoring durability. `npm run smoke` explicitly contacts one live board on each ATS, processes two sample listings, and saves `output/smoke-report.json`. The low cap intentionally produces `partial`/`truncated` reports. It is not a load test or proof of all tenants working.

Add connector implementations in `src/connectors.ts` and source mappings in `src/normalize.ts`; extend source types, discovery operators, board parsing, input schemas and fixture tests together. SmartRecruiters, Recruitee and Personio are planned extensions; LinkedIn, Indeed and Google Jobs collectors are not included.

Primary integration references: [Greenhouse Job Board API](https://docs.greenhouse.io/job-board.html), [Lever Postings API](https://github.com/lever/postings-api), [Ashby Job Postings API](https://developers.ashbyhq.com/docs/public-job-posting-api), [Brave Web Search API](https://api-dashboard.search.brave.com/api-reference/web/search/get), [SerpAPI Google Search API](https://serpapi.com/search-api), [Apify Actor SDK](https://docs.apify.com/sdk/js/reference/class/Actor).

# Actor input Schema

## `roles` (type: `array`):

OR between roles; every word in a role must appear in the job title.

## `keywords` (type: `array`):

OR between phrases, searched in title and description. Combined with the role filter using AND.

## `locations` (type: `array`):

OR between locations. Europe and Turkey have a limited alias dictionary. Remote jobs still need to match the requested region.

## `remote` (type: `boolean`):

True: explicitly remote. False: explicitly non-remote/hybrid. Omit this field in JSON to include all workplace types.

## `postedWithinDays` (type: `integer`):

Omit to disable date filtering. Uses ATS date and postedAtKind provenance, never firstSeenAt.

## `maxResults` (type: `integer`):

Maximum delivered results after filters, deduplication and monitoring.

## `enrich` (type: `boolean`):

Deterministic keyword extraction; no paid LLM calls or invented salary values.

## `monitor` (type: `boolean`):

Preserve first delivered observation across scheduled runs. Run only one instance of a monitor scope at a time.

## `onlyNewJobs` (type: `boolean`):

Requires monitor. First run returns a baseline; later runs suppress previously delivered jobs.

## `monitorId` (type: `string`):

Optional namespace. Search filters also form part of the history scope.

## `discovery` (type: `string`):

Auto tries Brave then SerpAPI on failure or no supported hits. Provider keys can be supplied by the operator as environment variables.

## `braveSearchApiKey` (type: `string`):

Optional override for BRAVE\_SEARCH\_API\_KEY. Leave unset to use the operator configuration.

## `serpApiKey` (type: `string`):

Optional override for SERPAPI\_API\_KEY. Leave unset to use the operator configuration.

## `maxDiscoveryQueries` (type: `integer`):

Logical queries. Auto failover may make up to two provider calls per query, plus HTTP retries.

## `careerUrls` (type: `array`):

Optional hosted Greenhouse, Lever, Ashby or Workday board/job URLs. Custom company domains are not resolved in this MVP.

## `useCatalog` (type: `boolean`):

Supplement discovery with a small catalog of four verified employers. This is not whole-web coverage.

## `sources` (type: `array`):

Enabled connectors.

## `maxBoards` (type: `integer`):

User URLs, discovered boards, then catalog. See the run report for omitted boards.

## `maxJobsPerBoard` (type: `integer`):

Lever/Workday listing scan cap; Greenhouse/Ashby title-matched processing cap after the full feed download. Truncation is reported.

## `concurrency` (type: `integer`):

HTTP requests are also throttled by host.

## `unknownDatePolicy` (type: `string`):

Only applies when postedWithinDays is set. Exclude is the strict default.

## `deduplication` (type: `string`):

Strict: same ATS identity or application URL. CrossSource additionally requires exact normalized company, title, locations and description across different ATS sources.

## Actor input object example

```json
{
  "roles": [],
  "keywords": [],
  "locations": [],
  "maxResults": 1000,
  "enrich": false,
  "monitor": false,
  "onlyNewJobs": false,
  "discovery": "auto",
  "maxDiscoveryQueries": 8,
  "careerUrls": [],
  "useCatalog": true,
  "sources": [
    "greenhouse",
    "lever",
    "ashby",
    "workday"
  ],
  "maxBoards": 20,
  "maxJobsPerBoard": 250,
  "concurrency": 3,
  "unknownDatePolicy": "exclude",
  "deduplication": "strict"
}
```

# Actor output Schema

## `jobs` (type: `string`):

Normalized, deduplicated jobs saved in the default dataset.

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

Search coverage, emitted results and warnings.

# 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("herazur/all-jobs-search-api").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("herazur/all-jobs-search-api").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 herazur/all-jobs-search-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herazur/all-jobs-search-api"
        }
    }
}
```

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/6c9OeBz1Ra1Zaw5Tb/builds/euWbMVRu4HRCotusR/openapi.json
