# Workday, SmartRecruiters, Recruitee, Personio Jobs Scraper (`nekruz/ats-jobs-scraper-2`) Actor

Pull every open job from career sites on Workday, SmartRecruiters, Recruitee, Personio and Breezy HR into one clean schema. Paste career-site URLs, filter by keyword, location, remote or posting date, and pay only per job returned.

- **URL**: https://apify.com/nekruz/ats-jobs-scraper-2.md
- **Developed by:** [Nekruz Kasimov](https://apify.com/nekruz) (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 $1.00 / 1,000 jobs

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

## Workday, SmartRecruiters, Recruitee, Personio, Breezy Jobs Scraper

Get every open job from company career sites hosted on **Workday**, **SmartRecruiters**, **Recruitee**, **Personio** or **Breezy HR**, in one normalized dataset. Workday alone powers the career pages of thousands of large enterprises (NVIDIA, Salesforce, Adobe, banks, universities); SmartRecruiters, Recruitee, Personio and Breezy cover tens of thousands of European and mid-size companies.

Reads each platform's own public job-board endpoints, so no browser, no proxies, no CAPTCHAs. Pair it with the companion Actor for Greenhouse, Lever, Ashby and Workable to cover most of the market.

### Use cases

- Enterprise job boards and alerts, refreshed daily.
- Account research: watch hiring at target companies.
- Structured job data for AI agents, RAG pipelines and market analysis.

### Input

| Field | Description |
|---|---|
| `companies` | Career-site URLs. Workday: `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`. SmartRecruiters: `https://jobs.smartrecruiters.com/Ubisoft2` or `smartrecruiters:Ubisoft2`. Recruitee: `https://channable.recruitee.com` or `recruitee:channable`. Personio: `https://circus.jobs.personio.de` or `personio:circus`. Breezy: `https://acme.breezy.hr` or `breezy:acme`. A bare slug is tried on all platforms except Workday. |
| `includeDescription` | Full description as text and HTML. Recruitee and Personio include it for free; Workday and SmartRecruiters need one extra request per job. For Workday this also fills the posting date, requisition id, country and remote type. |
| `keywords`, `locations`, `remoteOnly`, `postedAfter`, `maxJobsPerCompany` | Same filters as the companion Actor. On Workday, keywords are matched by Workday's search, which matches whole words and word beginnings, so use whole words or word beginnings (a fragment such as "gineer" will not match on Workday). |

### Output

One item per job with `platform`, `company`, `companySlug`, `jobId`, `title`, `url`, `applyUrl`, `location`, `locations`, `country`, `isRemote`, `workplaceType`, `department`, `team`, `employmentType`, `experienceLevel`, `salary`, `publishedAt`, `updatedAt`, `requisitionId`, optional `descriptionPlain` and `descriptionHtml`, and `scrapedAt` (Workday items also carry `postedOnText`, the relative age Workday shows, such as "Posted 3 Days Ago"). Fields a platform does not expose are `null`. The `OUTPUT` record summarizes every board: jobs found, after filters, pushed, or why it failed. It contains:

- `boards`: one entry per board or input entry, with `input`, `platform`, `slug`, `status`, `jobsOnBoard`, `jobsFetched` (jobs downloaded, which can be fewer than on the board when paging stops at your cap or date filter), `jobsAfterFilters`, `jobsPushed`, plus `descriptionFailures`, `error`, `failedPage` or `duplicateOf` when they apply. A `duplicate` entry carries only `input`, `platform`, `slug`, `status` and `duplicateOf`.
- `jobsPushed`: jobs saved to the dataset. `jobsCharged`: jobs billed under pay-per-event pricing, otherwise `null`.
- `descriptionFailures`: jobs saved without a description (see Pricing). `descriptionsRecovered`: descriptions that failed once and succeeded on the automatic retry.
- `chargeLimitReached`: `true` if the run stopped at your maximum charge per run.

Each board's `status` is one of `ok` (board read), `not_found` (the URL or `platform:slug` entry points to no board), `not_found_on_any_platform` (a bare slug matched no board on SmartRecruiters, Recruitee, Personio or Breezy), `unrecognized` (not a supported career-site URL or slug), `error` (the board request failed after retries; `error` has the reason), `partial` (a Workday or SmartRecruiters list page after the first failed after retries or came back without a job list; the jobs from the earlier pages are kept and returned, `failedPage` is the page that failed and `error` says why, for example "page 2 failed (HTTP 404); kept 20 jobs from earlier pages"), or `duplicate` (the entry is the same board as an earlier one, so it was not requested; `duplicateOf` is the first input for it).

With `includeDescription`, a Workday or SmartRecruiters job whose description could not be fetched is still returned, with null descriptions and a `descriptionError` field (for example `HTTP 503`).

### Pricing

Per job returned. Filtered-out jobs are free. Set a maximum charge per run to cap spend; the Actor stops cleanly when it is reached (`chargeLimitReached` is `true` in `OUTPUT`).

A board entered more than once is requested and charged once: repeat entries show as `duplicate` in `OUTPUT` and cost nothing. Jobs kept from a `partial` board are charged like any other job.

A Workday or SmartRecruiters job whose description could not be fetched is not charged. Failed detail requests are retried (rate limits and server errors get one more try after a short pause); if the description is still missing, the job is returned with `descriptionError` set and counted in `descriptionFailures`. `jobsCharged` shows what was actually billed. Such a job still counts toward `maxJobsPerCompany`.

### Notes

- Workday career sites: copy the URL from the browser.
- Boards are compared by platform and slug, ignoring case, so `smartrecruiters:Ubisoft2`, `https://jobs.smartrecruiters.com/ubisoft2/` and `jobs.smartrecruiters.com/Ubisoft2` are one board. The same bare slug entered twice is a repeat too.
- Career-site URLs without `https://` (for example `channable.recruitee.com`) are read as URLs, not as bare slugs.
- Workday and SmartRecruiters are read page by page. If a later page fails, the run keeps the earlier jobs instead of dropping the board, and the final status message adds "(N incomplete, see OUTPUT)". Recruitee, Personio and Breezy are a single request each, so they are never `partial`.
- Breezy boards do not expose descriptions or posting text beyond the listing.
- Workday lists only a relative age ("Posted 3 Days Ago"), so `postedAfter` works at day precision for Workday boards; with `includeDescription` the exact date from the job page is re-checked.
- Rate limits (HTTP 429), server errors (5xx) and network errors are tried up to 3 times. The Actor waits for the `Retry-After` the server sends (at most 60 s), otherwise 1.5 s, then 3 s.

# Changelog

This Actor's version history is a separate document: https://apify.com/nekruz/ats-jobs-scraper-2/changelog.md

# Actor input Schema

## `companies` (type: `array`):

One entry per company. Workday needs the career-site URL (https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite). Others accept a URL (https://jobs.smartrecruiters.com/Ubisoft2, https://channable.recruitee.com, https://circus.jobs.personio.de, https://breezy.breezy.hr), a prefixed slug (smartrecruiters:Ubisoft2, recruitee:channable, personio:circus, breezy:breezy), or a bare slug, which is tried on SmartRecruiters, Recruitee, Personio and Breezy.

## `includeDescription` (type: `boolean`):

Add the full description as plain text and HTML. For Workday and SmartRecruiters this costs one extra request per job.

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

Keep only jobs whose title contains one of these words (case-insensitive).

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

Keep only jobs whose location contains one of these strings (case-insensitive).

## `remoteOnly` (type: `boolean`):

Keep only jobs marked remote or work-from-anywhere by the career site.

## `postedAfter` (type: `string`):

ISO date (YYYY-MM-DD). Keep only jobs published on or after this date. Workday lists only a relative age ("Posted 3 Days Ago"), so the filter uses that at day precision; with includeDescription on, the exact date from the job page is re-checked.

## `maxJobsPerCompany` (type: `integer`):

0 means no limit.

## Actor input object example

```json
{
  "companies": [
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "smartrecruiters:Ubisoft2",
    "recruitee:channable"
  ],
  "includeDescription": false,
  "keywords": [],
  "locations": [],
  "remoteOnly": false,
  "maxJobsPerCompany": 25
}
```

# Actor output Schema

## `items` (type: `string`):

One record per open job, normalized across Workday, SmartRecruiters, Recruitee, Personio and Breezy.

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

The OUTPUT record: per-target statuses, counts, what was charged, and why items were skipped or missing.

# 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 = {
    "companies": [
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "smartrecruiters:Ubisoft2",
        "recruitee:channable"
    ],
    "maxJobsPerCompany": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("nekruz/ats-jobs-scraper-2").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 = {
    "companies": [
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "smartrecruiters:Ubisoft2",
        "recruitee:channable",
    ],
    "maxJobsPerCompany": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("nekruz/ats-jobs-scraper-2").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 '{
  "companies": [
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "smartrecruiters:Ubisoft2",
    "recruitee:channable"
  ],
  "maxJobsPerCompany": 25
}' |
apify call nekruz/ats-jobs-scraper-2 --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nekruz/ats-jobs-scraper-2"
        }
    }
}
```

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/iHWQFZyFTb6phKljx/builds/BzbBqyjdOcEVTwKqc/openapi.json
