# Career Site Jobs Scraper: Greenhouse, Lever, Ashby, Workable (`lasting_bonnethead/ats-jobs`) Actor

Get normalized job postings straight from companies' own career sites on Greenhouse, Lever, Ashby and Workable. Auto-detects the ATS from any careers URL, filters by keyword, location and remote, and can return only jobs that are new since the last run.

- **URL**: https://apify.com/lasting_bonnethead/ats-jobs.md
- **Developed by:** [Fatih S.](https://apify.com/lasting_bonnethead) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 job returneds

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: Greenhouse, Lever, Ashby & Workable

Get every open job straight from the source: the company's own applicant-tracking system (ATS). Paste career-page URLs, ATS board links or just company slugs, and get clean, **normalized** job postings in one dataset, whether the company runs **Greenhouse, Lever, Ashby or Workable**.

Built for scheduled monitoring: switch on **Only new jobs** and each run returns (and charges for) only postings that appeared since the last run.

### What you get

- ✅ **4 ATS platforms, one schema**: Greenhouse, Lever, Ashby and Workable all map to the same fields.
- 🔎 **ATS auto-detection**: paste `https://www.figma.com/careers/` and the Actor finds the Greenhouse board behind it. Bare slugs like `linear` are looked up on Greenhouse, Lever, Ashby and Workable.
- 🆕 **Only new jobs since last run**: job IDs are remembered per company in a named key-value store. Perfect for daily alerts and job-board feeds.
- 💰 **Salary when published**: structured pay ranges from Ashby and Lever, plus pay-transparency ranges parsed from descriptions (min, max, currency, interval).
- 🧹 **Filters**: keywords (title, department or full text), exclude words, locations, remote/hybrid/on-site, departments, employment type, posted within N days.
- 📊 **Hiring signals per company**: a `SUMMARY` record with job counts, new and removed jobs, top departments, remote share and salary share for each company.
- 🛡️ **Polite and reliable**: official public job-board endpoints only, robots.txt checked per host, Crawl-delay honoured, automatic back-off on 429s, and per-company error isolation, so one bad URL never kills the run.
- ⚡ **Fast and cheap**: plain HTTP JSON with no browser and no proxy. In our final test, 3,062 jobs from 10 companies on Greenhouse, Lever, Ashby and Workable came back in about 11 seconds.

### Input examples

**Pull everything from a few companies**

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/openai",
    "https://apply.workable.com/huggingface/"
  ]
}
```

**US or remote engineering roles posted in the last 7 days, text descriptions only**

```json
{
  "companies": ["greenhouse:anthropic", "ashby:notion", "linear", "https://www.figma.com/careers/"],
  "keywords": ["engineer", "ML"],
  "excludeKeywords": ["intern"],
  "locations": ["United States", "Remote"],
  "postedWithinDays": 7,
  "descriptionFormat": "text"
}
```

**Daily new-jobs monitor (schedule this task)**

```json
{
  "companies": ["greenhouse:stripe", "lever:spotify", "ashby:ramp", "workable:huggingface"],
  "onlyNew": true,
  "stateStoreName": "my-sales-hiring-monitor",
  "skipOutputOnFirstRun": true,
  "keywords": ["account executive", "SDR", "BDR"],
  "keywordsIn": "title"
}
```

#### Accepted company formats

| You paste | Detected as |
|---|---|
| `https://job-boards.greenhouse.io/stripe`, `https://boards.greenhouse.io/embed/job_board?for=stripe`, `greenhouse:stripe` | Greenhouse `stripe` |
| `https://jobs.lever.co/palantir`, `https://jobs.eu.lever.co/acme`, `lever:palantir`, `lever-eu:acme` | Lever (global or EU) |
| `https://jobs.ashbyhq.com/ramp`, `ashby:ramp` | Ashby `ramp` |
| `https://apply.workable.com/huggingface/`, `workable:huggingface` | Workable `huggingface` |
| `https://www.figma.com/careers/` (any careers page) | Scanned for ATS links and embeds |
| `linear` (bare slug) | Probed on Greenhouse, Lever, Ashby and Workable; the board with most jobs wins |

### Output

One dataset item per job:

```json
{
  "id": "ashby:notion:1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "jobId": "1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "sourceAts": "ashby",
  "companySlug": "notion",
  "company": "Notion",
  "title": "Software Engineer, Developer Platform",
  "department": "Engineering",
  "team": "Engineering",
  "location": "San Francisco, California",
  "locations": [
    "San Francisco, California",
    "New York, New York"
  ],
  "country": "United States",
  "isRemote": false,
  "workplaceType": "hybrid",
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "FullTime",
  "postedAt": "2026-08-24T14:44:49.699Z",
  "updatedAt": null,
  "salary": {
    "min": 213000,
    "max": 320000,
    "currency": "USD",
    "interval": "YEAR",
    "text": "$213,000 - $320,000",
    "source": "description"
  },
  "descriptionText": "Who We Are\n\nNotion is the collaborative AI workspace where teams and agents think together. We're building one place where your knowledge, projects, meetings, a…",
  "descriptionHtml": "<h1>Who We Are</h1><p style=\"min-height:1.5em\">Notion is the collaborative AI wo…",
  "jobUrl": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "applyUrl": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a/application",
  "requisitionId": null,
  "hiringEntity": null,
  "boardUrl": "https://jobs.ashbyhq.com/notion",
  "scrapedAt": "2026-10-08T09:08:19.189Z"
}
```

| Field | Notes |
|---|---|
| `id` | Stable unique key `ats:company:jobId`; use it for de-duplication |
| `isRemote` / `workplaceType` | `isRemote` = fully remote. `workplaceType` is `remote`, `hybrid`, `onsite` or `null` |
| `employmentType` | `FULL_TIME`, `PART_TIME`, `CONTRACT`, `INTERN`, `TEMPORARY`, `OTHER` or `null` (raw value in `employmentTypeRaw`) |
| `postedAt` / `updatedAt` | ISO 8601. Greenhouse = first published / last updated, Lever = created, Ashby = published, Workable = published |
| `salary` | `{min, max, currency, interval, text, source}`; `source` is `structured` (from the ATS) or `description` (parsed pay range) |
| `country` | From the ATS where available, otherwise inferred from the location text |
| `isNew`, `firstSeenAt` | Only in *Only new jobs* mode |

The run's key-value store has a **`SUMMARY`** record listing each input with `status` (`ok`, `not_found`, `robots_disallowed`, `unsupported_ats`, `ats_not_detected`, `error`), job counts, new/removed jobs, top departments, remote share and salary share.

### Field coverage (measured on 2,900+ live jobs from 9 companies, Oct 2026)

| ATS | Title, location, URLs, description | Department | Employment type | Posted date | Salary (where employer publishes it) |
|---|---|---|---|---|---|
| Greenhouse | 100% | 100% | rarely exposed | 100% | parsed from description (3–88% depending on employer) |
| Lever | 100% | 100% | 92–100% | 100% | 34–65% |
| Ashby | 100% | 100% | 100% | 100% | 66–81% |
| Workable | 100% | 100% | 100% | 100% | if published |

### Pricing

Pay per event, no subscription:

| Event | Price |
|---|---|
| Job returned | **$0.002** per job ($2 per 1,000 jobs). Lower on paid Apify plans: $0.0018 (Starter), $0.0015 (Scale), $0.0012 (Business) |
| Company board checked | $0.0003 per company board fetched successfully |

Boards that fail (not found, unsupported ATS, robots.txt) are not charged. In *Only new jobs* mode you pay only for new jobs plus the per-company check, so a daily monitor of 100 companies with a few dozen new postings costs roughly $0.10–0.15 per day. Set **Max total charge** on the run to cap spend; the Actor stops cleanly at the limit.

Example: pulling all ~725 open Stripe jobs costs about $1.45.

### Good to know

- **Careers pages rendered by JavaScript** may hide their ATS link. If detection fails, paste the ATS board URL instead.
- **Not supported yet: Workday.** Workday career-site URLs are recognized and listed in the run `SUMMARY` as `unsupported_ats` (no charge). Workday support is planned for a later version.
- **Not supported:** SmartRecruiters, whose `api.smartrecruiters.com/robots.txt` disallows all crawlers except LinkedInBot, and sites like LinkedIn, Indeed and Glassdoor, whose terms prohibit scraping.
- Recruiter and hiring-manager names are **not collected**. Output is company and job data only.
- Unlisted Ashby jobs (direct-link only) are skipped.

### Is it legal?

The Actor reads job postings that employers publish for the public through their ATS's public job-board endpoints, the same data any visitor sees without logging in. It honours robots.txt and rate limits and collects no personal data. Job descriptions are the employers' content, so check that your use (e.g. republishing on a job board) is permitted. This is not legal advice.

### FAQ

**How do I find a company's ATS?** Paste their careers page URL; the Actor detects it. Or open any job on their site and look at the URL (`greenhouse.io`, `lever.co`, `ashbyhq.com`, `workable.com`).

**Can I use different filters for different companies?** Run separate tasks, each with its own `stateStoreName` if you use *Only new jobs*.

**How do I get alerts?** Schedule a task with *Only new jobs* on, then add an integration (Slack, email, webhook, Zapier, Make) that fires when the run succeeds.

# Actor input Schema

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

One per line. Accepts: ATS board URLs (https://job-boards.greenhouse.io/stripe, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/ramp, https://apply.workable.com/huggingface), a company's own careers page (the ATS is detected from links/embeds in the page), prefixed slugs (greenhouse:stripe, lever:palantir, lever-eu:slug, ashby:ramp, workable:huggingface) or a bare slug (looked up on Greenhouse, Lever, Ashby and Workable). Workday sites are not supported yet and are reported as unsupported (not charged).

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

Keep jobs matching at least one keyword. Terms of 3 characters or fewer (AI, ML, QA, SDR) match whole words only.

## `keywordsIn` (type: `string`):

Where keywords are matched. Title-only is the most precise.

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

Drop jobs whose title contains any of these (e.g. Senior, Intern).

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

Keep jobs whose location, secondary locations or country contain one of these strings (e.g. London, Germany, "New York"). Add "Remote" to also keep remote jobs.

## `remote` (type: `string`):

Keep only remote jobs, drop remote jobs, or keep all. Remote is detected from the ATS workplace field or "Remote" in the location.

## `departments` (type: `array`):

Keep jobs whose department or team contains one of these strings (e.g. Engineering, Sales).

## `employmentTypes` (type: `array`):

Leave empty for all. Jobs with unknown type are excluded when this is set.

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

0 = no date filter. Jobs without a publish date are kept unless 'Drop undated jobs' is on.

## `dropUndatedWhenFiltering` (type: `boolean`):

When "Posted within" is set, also drop jobs whose ATS gives no publish date.

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

Remembers job IDs per company in a named key-value store and outputs only jobs not seen before. Ideal for scheduled runs: you pay for new jobs only (plus a small per-company check fee).

## `stateStoreName` (type: `string`):

Named key-value store that keeps seen job IDs. Use a different name for each saved task/filter set.

## `skipOutputOnFirstRun` (type: `boolean`):

With 'Only new jobs' on, the first run records current jobs without outputting them, so later runs return only truly new postings.

## `stateRetentionDays` (type: `integer`):

How long a job ID is remembered after it was last seen. Keeps jobs that briefly disappear from being reported as new again.

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

Turn off for faster, smaller output.

## `descriptionFormat` (type: `string`):

Plain text, HTML, or both.

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

0 = no limit.

## `maxJobsTotal` (type: `integer`):

0 = no limit. The run also stops at your 'max total charge' setting.

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

How many companies are processed at the same time.

## `probeBareSlugs` (type: `boolean`):

For entries like "notion" (no URL/prefix), look the slug up on Greenhouse, Lever, Ashby and Workable and use the board with most jobs.

## `enabledAts` (type: `array`):

Turn individual ATS sources on or off.

## `perHostConcurrency` (type: `integer`):

Politeness limit per ATS host (robots.txt Crawl-delay always wins, e.g. Lever = 1 request/second).

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "ashby:ramp"
  ],
  "keywordsIn": "title",
  "remote": "any",
  "postedWithinDays": 0,
  "dropUndatedWhenFiltering": false,
  "onlyNew": false,
  "stateStoreName": "ats-jobs-state",
  "skipOutputOnFirstRun": false,
  "stateRetentionDays": 60,
  "includeDescription": true,
  "descriptionFormat": "both",
  "maxJobsPerCompany": 0,
  "maxJobsTotal": 0,
  "maxConcurrency": 3,
  "probeBareSlugs": true,
  "enabledAts": [
    "greenhouse",
    "lever",
    "ashby",
    "workable"
  ],
  "perHostConcurrency": 4
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "companies": [
        "https://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "ashby:ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lasting_bonnethead/ats-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": [
        "https://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "ashby:ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("lasting_bonnethead/ats-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": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "ashby:ramp"
  ]
}' |
apify call lasting_bonnethead/ats-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lasting_bonnethead/ats-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/URBXFTyyEiJSf4X1o/builds/XMmpVhRURazHjNrPV/openapi.json
