# Career Site Jobs Scraper – Greenhouse, Lever, Ashby, Workday (`pooh20240/multi-ats-job-feed`) Actor

Monitor job openings of any company list straight from their ATS. Live data, only-new mode for daily hiring-signal alerts, 8 ATS supported. $3 per 1,000 jobs.

- **URL**: https://apify.com/pooh20240/multi-ats-job-feed.md
- **Developed by:** [Thongpoo Supong](https://apify.com/pooh20240) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 job postings

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?

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 Monitor (Greenhouse, Lever, Ashby, Workday +4)

Give a list of companies and get their **current job openings straight from their
ATS**. On scheduled runs you can emit **only what is new since the last run**.

Supported ATS boards (public JSON/XML, no login, no browser):

| ATS | Shorthand example | Board URL example |
| --- | --- | --- |
| Greenhouse | `greenhouse:stripe` | `https://boards.greenhouse.io/stripe` |
| Lever | `lever:spotify` | `https://jobs.lever.co/spotify` |
| Ashby | `ashby:ramp` | `https://jobs.ashbyhq.com/ramp` |
| SmartRecruiters | `smartrecruiters:BoschGroup` | `https://jobs.smartrecruiters.com/BoschGroup` |
| Recruitee | `recruitee:optics11` | `https://optics11.recruitee.com` |
| Workable | `workable:runware` | `https://apply.workable.com/runware` |
| Personio | `personio:personio` | `https://personio.jobs.personio.de` |
| Workday | `workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | `https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite` |

### Input

`companies` (required, array of strings). Each entry can be:

1. **ATS board URL** — e.g. `https://boards.greenhouse.io/stripe`.
2. **Shorthand** `ats:slug` — e.g. `greenhouse:stripe`, `lever:spotify`,
   `ashby:ramp`, `smartrecruiters:BoschGroup`, `recruitee:optics11`,
   `workable:runware`, `personio:personio`,
   `workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`.
3. **Any careers-page URL** — the ATS is detected by scanning the page HTML
   for known ATS host names.
4. **A bare company name/slug** (guess mode) — tried against the major ATS in
   order until one returns jobs.

Optional filters:

- `titleKeywords` / `excludeTitleKeywords` — keep/drop jobs by title keyword.
- `locationKeywords` — keep jobs whose location matches any keyword.
- `remoteOnly` — only jobs flagged or named remote.
- `postedWithinDays` — skip jobs older than N days (when `postedAt` is known).
- `onlyNew` — emit only jobs not seen in previous runs (state is stored in the
  actor's named key-value store).
- `includeDescription` (default `true`) — set to `false` to save time on
  SmartRecruiters/Workday by skipping per-job detail calls.
- `maxItemsPerCompany` (default 1000) and `maxItems` (default 5000) caps.

SmartRecruiters and Workday are fetched in two steps to save money on capped
runs: the list of jobs is fetched first, then per-job detail requests are made
**only for the jobs that will actually be pushed** (after filters, `onlyNew`
and the global `maxItems` budget are applied). A `maxItems=1` run therefore
makes exactly one detail request, not one per board job.

### Output

One dataset item per job:

| Field | Type | Notes |
| --- | --- | --- |
| `company` | string | input entry exactly as given |
| `companyName` | string|null | from the ATS; falls back to the prettified board slug/tenant when the ATS is silent |
| `ats` | string | greenhouse, lever, ashby, smartrecruiters, recruitee, workable, personio, workday |
| `boardSlug` | string | board slug used for the API |
| `jobId` | string | ATS job id |
| `title` | string | job title |
| `department`, `team` | string|null | from the ATS |
| `locations` | string\[] | one or more locations |
| `remote` | boolean|null | explicit flag or heuristic on the location |
| `employmentType` | string|null | e.g. Full-time |
| `url`, `applyUrl` | string | canonical posting and apply URLs |
| `postedAt` | ISO string|null | publication date (Workday uses an approximation) |
| `postedAtRaw` | string|null | raw date text (e.g. Workday "Posted 3 Days Ago") |
| `updatedAt` | ISO string|null | last update |
| `salary` | object|null | `{min,max,currency,interval}` |
| `descriptionHtml`, `descriptionText` | string|null | null when `includeDescription=false` |
| `isNew` | boolean | true for items not seen in earlier runs |
| `scrapedAt` | ISO string | when the item was collected |

A run also writes a `SUMMARY` key-value record with one status row per company
(`ok | skipped | not_found | unsupported | blocked | error`, plus job counts; `skipped` = not processed because `maxItems` or your charge limit was reached). If **every**
company fails, the run fails loudly instead of returning an empty dataset.

### What is NOT collected

No personal data: recruiter / hiring-manager / applicant names, emails,
avatars, application `questions`, `mailbox_email`, `open_questions` or
`data_compliance` fields are never in the output. Targets are public ATS APIs;
no logins, no CAPTCHA bypass, no proxies.

### Scheduling & "only new" recipe

1. Run once with `onlyNew: false` to seed the state.
2. Schedule the actor (e.g. daily) with the SAME `companies` list and
   `onlyNew: true`. Only jobs published after the previous run are emitted.
3. Change the `companies` list whenever your target set changes — state is keyed
   on the sorted list, so a new list starts fresh automatically.

State entries not seen for 90 days are pruned automatically.

### Limits & behaviour

- Per host: max 2 concurrent requests, >=300 ms between requests, 30 s timeout.
- Retries (up to 3) with exponential backoff on transient errors and 429; a
  `Retry-After` header is honoured (capped at 60 s).
- A 403, CAPTCHA or JS-challenge page is recorded as `blocked` and never retried.
- Items are validated against the output schema; if more than 20% fail, the run
  fails loudly so a board layout change is never reported as "no jobs".

### Development

```bash
npm install
npm test        # offline, fixtures only
```

# Actor input Schema

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

Each entry is one of: an ATS board URL (e.g. https://boards.greenhouse.io/stripe), a shorthand (e.g. greenhouse:stripe, lever:spotify, ashby:ramp, smartrecruiters:BoschGroup, recruitee:optics11, workable:runware, personio:personio, workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite), any careers-page URL (the ATS is detected from its HTML), or a bare company name (guess mode, tried against the major ATS).

## `titleKeywords` (type: `array`):

Jobs whose title matches any of these keywords are kept; jobs matching none are skipped.

## `excludeTitleKeywords` (type: `array`):

Jobs whose title matches any of these keywords are skipped.

## `locationKeywords` (type: `array`):

Jobs whose location matches any of these keywords are kept; jobs matching none are skipped.

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

When true, keeps only jobs explicitly flagged remote or whose location contains 'remote'.

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

Jobs with a known postedAt older than this number of days are skipped. Leave empty to keep all jobs regardless of posting date.

## `onlyNew` (type: `boolean`):

When true, only jobs not seen in earlier runs (see the actor's key-value store) are emitted. Use with the same companies list on scheduled runs.

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

When false, descriptionHtml and descriptionText are null and per-job detail calls are skipped on SmartRecruiters and Workday (faster runs).

## `maxItemsPerCompany` (type: `integer`):

Stop fetching detail/list pages for a company after this many items.

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

Stop processing further companies once this many items have been pushed.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "lever:spotify",
    "ashby:ramp"
  ],
  "remoteOnly": false,
  "onlyNew": false,
  "includeDescription": true,
  "maxItemsPerCompany": 1000,
  "maxItems": 5000
}
```

# Actor output Schema

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

One dataset item per job posting (title, company, locations, remote flag, dates, salary when published, description, isNew). No personal data.

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

Per-company status rows: ok, skipped, not\_found, unsupported, blocked or error, with jobsFound and jobsPushed.

# 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": [
        "greenhouse:stripe",
        "lever:spotify",
        "ashby:ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pooh20240/multi-ats-job-feed").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": [
        "greenhouse:stripe",
        "lever:spotify",
        "ashby:ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pooh20240/multi-ats-job-feed").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": [
    "greenhouse:stripe",
    "lever:spotify",
    "ashby:ramp"
  ]
}' |
apify call pooh20240/multi-ats-job-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pooh20240/multi-ats-job-feed"
        }
    }
}
```

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/KwD6pnBnj0HodGGYS/builds/FxYaQwROOs0EZoyFe/openapi.json
