# Career Site Jobs Scraper: 9 ATS incl. Workday, Personio (`arcadium/career-site-jobs`) Actor

Open jobs from a company's own career site (Greenhouse, Lever, Workday, Personio and 5 more ATS) for a company domain; $1.80 per 1,000 jobs. Posted pay, department, remote flag, description. Monitoring mode returns only new jobs plus free closed-job rows. No recruiter names.

- **URL**: https://apify.com/arcadium/career-site-jobs.md
- **Developed by:** [Arcadium Data](https://apify.com/arcadium) (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.08 / 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

## Career Site Jobs Scraper: 9 ATS incl. Workday, Personio

Give it company domains, careers pages or job board links and it returns every open job as one JSON row: title, department, locations, remote flag, employment type, posted pay, dates, link and description. It reads the company's own board on Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor, SmartRecruiters and Workday, so one company list gives one output shape. $1.80 per 1,000 jobs; you pay only for rows you get.

**Why this actor**

- Keyed by company domain: hand it `stripe.com` and it finds the job board itself.
- The European ATS (Personio, Teamtailor, Recruitee) sit next to Greenhouse, Lever and Workday in one schema.
- Monitoring mode (`onlyChanges`) returns only new jobs plus a free `closed` row per vanished job, and keeps the old state if a board suddenly returns nothing.
- No recruiter names or contact mailboxes; emails and phones in descriptions are masked.

Built by Arcadium Data: public data, dated and agent-ready, never personal data.

### Use with AI agents

Returns open jobs from company career sites (9 job systems) for a company domain; it finds the job board by itself. Agents can call it through the Apify MCP server (`https://mcp.apify.com`, open source as `apify/actors-mcp-server`) as `arcadium/career-site-jobs`: the input schema is read from the actor, the result is a dataset of JSON rows.

Input example (minimal):

```json
{
  "companies": [
    "stripe.com"
  ],
  "maxJobsPerCompany": 20
}
```

"Remote engineering jobs posted this week at three companies, at most 100 rows":

```json
{ "companies": ["stripe.com", "ashby:openai", "lever:zoox"], "keywords": ["engineer"], "remoteOnly": true, "postedWithinDays": 7, "maxItems": 100 }
```

What comes back: one `job` row per open position (charged), free `coverage` rows (stated total vs delivered) and a free `error` row for a company that could not be read. In monitoring mode (`onlyChanges`) it also returns free `closed` rows. Filter on the `rowType` field. An input that fails is reported, not hidden (`includeErrorRows`, on by default): the error row carries `rowType: "error"`, a `code` (`invalid_input`, `not_found`, `blocked`, `unavailable`, `filter_unsupported` or `no_match`), a `message` and the `input` it is about. Retry on `blocked` and `unavailable` later; do not retry the other codes with the same input.

Cost of the call above: 20 jobs at $1.80 per 1,000 plus the $0.001 start fee is about $0.04 on the free plan; paid plans pay less. Free rows are never charged.

### What you get

| Field | What it holds |
| --- | --- |
| `id` | `<system>___<native id>`, for example `greenhouse___8172508`. Stable between runs. |
| `ats`, `company`, `companyToken` | The job system, the company name and its board name there. |
| `title`, `department`, `team` | As the company publishes them. Empty when not published. |
| `locations`, `countries`, `remote` | Location strings as published, ISO country codes where known, and `true`, `false` or `null` for remote. |
| `employmentType`, `seniority` | Full-time, part-time and so on; seniority only where the system has the field. |
| `salary` | `min`, `max`, `currency`, `period` and `source`. `structured` means the company published a pay field. `text` means the range was read out of the description. `null` when nothing is stated. |
| `postedAt`, `updatedAt` | ISO dates. Not every system publishes both. |
| `url`, `applyUrl` | The public posting and the apply link. |
| `description` | Plain text or HTML (your choice), with email addresses and phone numbers removed. |
| `language`, `requisitionId`, `scrapedAt` | Where known, plus when the row was collected. |

Output example:

```json
{
  "rowType": "job",
  "id": "ashby___34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "ats": "ashby",
  "company": "Ramp",
  "companyToken": "ramp",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)"],
  "countries": ["US", "CA"],
  "remote": false,
  "employmentType": "Full-time",
  "salary": { "min": 211400, "max": 290600, "currency": "USD", "period": "year", "source": "structured" },
  "postedAt": "2026-04-07T17:12:35.753Z",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "description": "About Ramp\n..."
}
```

Besides jobs, the dataset can hold three kinds of free rows:

- `coverage`: one per company. How many open jobs the board states, how many you received, which system it is and how it was found (`explicit`, `url`, `html` or `guess`). A `shortfall` flag marks companies where fewer jobs came back than the board states.
- `closed`: in monitoring mode, a job that was open last run and is gone now (`id`, `title`, `url`, `closedAt`).
- `error`: a company that could not be read, with the reason.

### Input

```json
{
  "companies": ["stripe.com", "ashby:openai", "https://jobs.lever.co/zoox"],
  "includeDescriptions": "text",
  "countries": ["US"],
  "keywords": ["engineer"]
}
```

| Field | What it does |
| --- | --- |
| `companies` | Domains, careers URLs, job board URLs, or `system:token`. A plain company name (`notion`) also works; see "Finding the job board". |
| `atsAllowList` | Restrict to some systems. Empty means all. |
| `includeSmartRecruiters` | On by default. SmartRecruiters' robots.txt asks crawlers to stay away from its API. Switch this off if you want to honour that. |
| `includeDescriptions` | `text` (default), `html` or `none`. `none` is the lightest and still returns pay where the board publishes it as a field. |
| `maxDescriptionChars` | Shorten descriptions. 0 keeps them whole. |
| `maxJobsPerCompany` | Cap per company, default 1,000. |
| `maxItems` | Cost cap for the whole run: stops after this many charged jobs across all companies. Empty means no limit. |
| `keywords`, `excludeKeywords` | Match against title, department and team. |
| `countries` | Names or two-letter codes. Jobs without a stated country are matched on their location text. |
| `remoteOnly`, `postedWithinDays` | Filters. Jobs without a publish date are kept by the date filter. |
| `onlyChanges` | Monitoring mode, see below. |
| `emitBaseline` | With monitoring: deliver all jobs on the first run (default) or only record them. |
| `stateKey` | Names the saved state. Use a new one when you change filters. |
| `includeErrorRows`, `includeCoverageRows` | Free rows, on by default. |

#### Finding the job board

For a domain or a careers page, the actor looks for a link to a supported job system. If the page has none, it tries the company name against each system. A board only counts when it has at least one open job. Check `detection` on the coverage row: `guess` means the match came from the name alone, and a different company with the same name is possible. Pass `system:token` when you need certainty.

#### Monitoring: only changes

With `onlyChanges` on, the actor remembers each company's open jobs in a named key-value store in your account. Every later run delivers only the new jobs, adds a free `closed` row per vanished job and skips description downloads for jobs it already knows. Schedule it daily and pipe the result to Slack, a sheet or a CRM. You pay a small fee per company checked, so a quiet day is almost free.

If a board suddenly returns no jobs at all, the actor keeps the old state and reports an error instead of marking everything closed.

The saved state covers every job on the board, not only the ones your filters keep. If you stop a run early (or it hits your maximum cost), only the jobs already delivered are remembered, and the rest, including `closed` rows, come with the next run. When `emitBaseline` is off, the first run reads the job list only and makes no per-job requests.

#### Switching from another career-site actor

Inputs written for the most used career-site actors work unchanged; each renamed field is noted once in the log.

| Their field | Ours |
| --- | --- |
| `startUrls`, `organizations`, `domainFilter` | `companies` |
| `greenhouseBoards`, `leverCompanies`, `ashbyOrgs`, `smartrecruitersCompanies` | `companies` as `greenhouse:<token>`, `lever:<token>`, ... |
| `titleKeyword`, `titleSearch`, `keywords` (text) | `keywords` |
| `titleExclusionSearch` | `excludeKeywords` |
| `postedAfter`, `datePostedAfter`, `timeRange` (`24h`, `7d`, `6m`) | `postedWithinDays` |
| `includeDescription: false`, `descriptionType` | `includeDescriptions` |
| `maxItemsTotal`, `limit` | `maxItems` |
| `onlyNewOrUpdated`, `stateKey` | `onlyChanges`, `stateKey` |

A per-company `maxItems` from another actor is read as our run-wide cap; set `maxJobsPerCompany` for a per-company limit.

### Use it from an AI assistant

```text
Use the Apify actor "career-site-jobs" to list open roles.
Input: {"companies":["stripe.com","ashby:openai"],"keywords":["data"],"includeDescriptions":"none","maxJobsPerCompany":200}
Each dataset row with rowType "job" is one position. Key fields: company, title, department, locations, remote, salary, postedAt, url.
Run it, then group the roles by department and list the ones that publish a salary range.
```

### Use it from code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("arcadium/career-site-jobs").call(run_input={
    "companies": ["stripe.com", "ashby:openai"],
    "includeDescriptions": "none",
})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    if job["rowType"] == "job":
        print(job["company"], job["title"], job["locations"])
```

JavaScript:

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('arcadium/career-site-jobs').call({
  companies: ['stripe.com', 'ashby:openai'],
  includeDescriptions: 'none',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.rowType === 'job').length, 'jobs');
```

### Pricing

You pay per job delivered, at the price of your Apify plan. In monitoring mode (`onlyChanges`) you also pay a small fee per company checked, and jobs you already know are not billed again. Failed companies, coverage rows and `closed` rows are free.

| Apify plan | Per 1,000 jobs | Per 1,000 companies checked |
| --- | --- | --- |
| Free | $1.80 | $1.80 |
| Starter (Bronze) | $1.80 | $1.80 |
| Scale (Silver) | $1.58 | $1.58 |
| Business (Gold) | $1.08 | $1.13 |
| Platinum | $1.08 | $1.13 |
| Diamond | $1.08 | $1.13 |

Plus a one-off $0.001 per GB of memory when a run starts.

Set a maximum cost per run in the Apify run options and the actor stops before it goes over.

### Related actors and data

- [Hiring Changes Tracker: New and Closed Jobs per Company](https://apify.com/arcadium/hiring-changes)
- [ATS Lookup: Which ATS a Company Uses, Job Board, Open Jobs](https://apify.com/arcadium/ats-lookup)
- [Company Hiring Profile: ATS, Open Jobs, Hiring Signals](https://apify.com/arcadium/company-hiring-profile)
- [Indeed Jobs Scraper: Company Domains, Salary, Descriptions](https://apify.com/arcadium/indeed-jobs)
- [Arcadium Data Hiring Signals API](https://arcadiumdata.io/hiring/): open jobs, ATS and hiring signals for a company domain from our daily jobs database.
- [Company Pulse](https://arcadiumdata.io/data/company-pulse/): dated change events per company domain (hiring up or down, new country, ATS switch, first posted pay).
- [Jobs data feed](https://arcadiumdata.io/data/jobs-feed/): the whole open-jobs table as daily files, keyed by employer domain.
- [Posted-pay tracker](https://arcadiumdata.io/data/pay-transparency/): share of job ads that state pay, by country and employer size.
- [Arcadium Data MCP Server](https://apify.com/arcadium/arcadium-mcp): AI agents can ask the same questions as tools (`company_profile`, `company_pulse`, `jobs_search`, `company_hiring`).

### Use cases

- Sourcing: pull every open role of a target list of companies, filtered by country or keyword.
- Sales intelligence: a company that opens ten sales roles is growing. Monitor target accounts and act on the change.
- Job boards and newsletters: a daily feed of new roles across hundreds of employers.
- Pay research: collect the salary ranges companies publish, by role and city.
- Market analysis: count open roles per department over time.

### Limitations

- Public career pages only. Nothing behind a login.
- Supported: Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor, SmartRecruiters, Workday. A company whose careers site is a custom page or runs on another system returns a "no board found" error row.
- Workday boards are found from their address (`https://company.wd5.myworkdayjobs.com/Site`), from `workday:company/wd5/Site`, or from a careers page that links to the board. A company name alone cannot find a Workday board. Workday lists show only relative dates, so the actor reads one extra page per new job to get the exact posting date and the description; that makes Workday runs slower than the other systems (a few jobs per second). Boards over 2,000 jobs are read in slices (by job category or similar) and merged; jobs are counted once.
- Pay is returned only when the company states it. Ranges read out of free text carry `source: "text"` and can be wrong; check them before relying on them.
- `remote` is `null` when the system does not say. Some systems mark a job remote because one of several locations is remote.
- Posting dates are what the company publishes. Some systems give no update date, some give a creation date instead of a publish date.
- Large Greenhouse and Ashby boards are delivered by the system in one piece, so `maxJobsPerCompany` limits the rows you get, not the data read.
- Very large boards (tens of thousands of jobs, for example big Workday sites) need a longer run timeout than the 1 hour default; raise it in the run options for the first full run. Monitoring runs after that are much faster.
- In monitoring mode, descriptions are only fetched for new jobs. Closed rows carry id, title and link, not the full old record.

### Privacy

Recruiter names, contact mailboxes and similar fields are never collected. Email addresses and phone numbers inside descriptions are replaced by `[email]` and `[phone]`. You get the job, not the people behind it.

### FAQ

**How fresh is the data?**
It is read when you run the actor, so it matches what the company shows at that moment.

**Can I get only new jobs since my last run?**
Yes, set `onlyChanges` and keep the same `stateKey`. The first run delivers everything (or just records the baseline with `emitBaseline` off). To start over, use a new `stateKey`.

**A company returned an error. Why?**
Open its careers page. If the jobs live on a system that is not in the list above, or in a custom page, there is no board to read. Try `system:token` if you know the board name. Errors are not charged.

**Why did I get fewer jobs than the board states?**
Look at the `coverage` row: `stopReason` tells you whether your cap, a filter or the budget ended the list. `shortfall: true` marks a real gap.

**Something looks wrong in the output. Who do I tell?**
Open an issue on the actor page with the run link. We read them all.

# Actor input Schema

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

One entry per company: a company domain (stripe.com), a careers page URL, a job board URL (https://jobs.lever.co/zoox) or `system:token`, e.g. `greenhouse:stripe` or `ashby:openai`. Domains and careers pages are resolved automatically. For Workday paste the board address (https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite) or write `workday:nvidia/wd5/NVIDIAExternalCareerSite`. Supported: Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor, SmartRecruiters, Workday.

## `atsAllowList` (type: `array`):

Leave empty to allow every supported system. Select some to restrict both detection and explicit entries to them.

## `includeSmartRecruiters` (type: `boolean`):

SmartRecruiters' robots.txt asks crawlers to stay out of its API. If you want to respect that, switch this off and SmartRecruiters boards are skipped.

## `includeDescriptions` (type: `string`):

Plain text, the original HTML, or none. Email addresses and phone numbers inside descriptions are always removed. 'None' is the lightest option and still returns pay when the job board publishes it as a field.

## `maxDescriptionChars` (type: `integer`):

0 keeps descriptions whole.

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

Upper limit of jobs delivered per company. In 'only changes' mode it caps the new jobs per run.

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

Stops the run after this many charged jobs across all companies, 1 to 1,000,000. A cost cap; leave empty for no limit. Free coverage, closed and error rows do not count.

## `onlyChanges` (type: `boolean`):

Delivers jobs that appeared since the previous run and adds a free `closed` row for every job that disappeared. The first run delivers all current jobs. State is stored in a named key-value store in your account. Charged per company checked plus per new job.

## `emitBaseline` (type: `boolean`):

Only used with 'only changes'. Switch off to just record today's jobs as the starting point and get changes from the next run on.

## `stateKey` (type: `string`):

Names the saved state, so several monitoring setups can run side by side. Use 1-40 letters, digits or dashes. Use a new name whenever you change the filters of a monitoring setup. Example: weekly-monitoring.

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

Keep jobs whose title, department or team contains any of these words. Example: \["engineer", "data"].

## `excludeKeywords` (type: `array`):

Drop jobs whose title, department or team contains any of these words. Example: \["intern", "sales"].

## `countries` (type: `array`):

Country names or two-letter codes (Germany, DE, US). Jobs without a stated country are matched on their location text. Example: \["Germany", "US"].

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

Keeps only jobs the company marks as remote.

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

Drops jobs published longer ago. Jobs without a publish date are kept.

## `includeErrorRows` (type: `boolean`):

Adds a free row to the dataset for every company that could not be read, so a failure is never silent.

## `includeCoverageRows` (type: `boolean`):

Adds a free row per company: how many jobs the board states, how many were delivered and how the board was found.

## `maxRetries` (type: `integer`):

How often a request is retried after a temporary error.

## `proxyConfiguration` (type: `object`):

Leave empty. The actor uses the cheapest proxy that works. Set your own proxy only if you must.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "ashby:openai",
    "lever:zoox"
  ],
  "includeSmartRecruiters": true,
  "includeDescriptions": "text",
  "maxDescriptionChars": 0,
  "maxJobsPerCompany": 50,
  "emitBaseline": true,
  "stateKey": "default",
  "includeErrorRows": true,
  "includeCoverageRows": true,
  "maxRetries": 3
}
```

# Actor output Schema

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

All rows of this run in the default dataset.

## `view_overview` (type: `string`):

No description

## `view_salaries` (type: `string`):

No description

## `view_closed` (type: `string`):

No description

## `view_coverage` (type: `string`):

No description

## `view_errors` (type: `string`):

No description

## `record_stats` (type: `string`):

Requests, data volume, retries, companies read and cost per job for this 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 = {
    "companies": [
        "stripe.com",
        "ashby:openai",
        "lever:zoox"
    ],
    "includeDescriptions": "text",
    "maxJobsPerCompany": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("arcadium/career-site-jobs").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": [
        "stripe.com",
        "ashby:openai",
        "lever:zoox",
    ],
    "includeDescriptions": "text",
    "maxJobsPerCompany": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("arcadium/career-site-jobs").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": [
    "stripe.com",
    "ashby:openai",
    "lever:zoox"
  ],
  "includeDescriptions": "text",
  "maxJobsPerCompany": 50
}' |
apify call arcadium/career-site-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arcadium/career-site-jobs"
        }
    }
}
```

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/f9mxVDEFKZ0VQeTDO/builds/ivmmXG9eXMkQKc8mc/openapi.json
