# Greenhouse, Lever & Ashby Jobs Scraper + AI Salary Extraction (`gingerwolfe/ats-jobs-ai`) Actor

Scrape public Greenhouse, Lever, and Ashby job boards. AI extracts salary, work mode, seniority, years of experience, and tech stack — 94% salary accuracy on hand-checked postings.

- **URL**: https://apify.com/gingerwolfe/ats-jobs-ai.md
- **Developed by:** [Shannon McNeil](https://apify.com/gingerwolfe) (community)
- **Categories:** Jobs, AI, 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 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?

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

## Greenhouse, Lever & Ashby Jobs Scraper + AI Salary Extraction

**Clean, structured job postings from Greenhouse, Lever, and Ashby — with salary, work mode, seniority, experience, and tech stack already extracted by AI, and accuracy you can verify.**

Give it a list of companies and you get one record per open role, read straight from each company's public applicant-tracking-system (ATS) job board. Turn on AI normalization and each record also gets the details that are normally buried in the description: salary range, remote/hybrid/onsite, seniority, minimum years of experience, visa sponsorship, and tech stack.

### Why use this Actor

- **Straight from the source.** Greenhouse, Lever, and Ashby publish public JSON job boards, so this Actor reads them directly. No browser, no login, no anti-bot workarounds, so runs are fast, cheap, and rarely break.
- **AI fields you can filter on.** You get `salaryMin`/`salaryMax` as numbers, `workMode` as `remote`/`hybrid`/`onsite`, and `techStack` as an array. You can filter on them in a spreadsheet, SQL, or your app without writing parsing code.
- **Measured accuracy.** We tested the AI fields against hand-checked labels on 50 real postings (details below). If a posting doesn't state a salary, you get `null`, not a guess.
- **Pay only for what you get.** Billing is per job delivered, plus a separate charge per job the AI actually enriched. Scraping-only runs are $1 per 1,000 jobs.

### Proven accuracy

We built a 50-posting test set from live Gusto, Databricks, Ramp, Stripe, and Palantir boards, ran the AI over it, and checked every disputed answer by hand against the real posting:

| Field | Accuracy | Sample |
|---|---|---|
| Salary range (min / max / currency) | **94%** | 47 / 50 |
| Work mode (remote / hybrid / onsite) | **95.7%** | 45 / 47 |
| Minimum years of experience | **100%** | 34 / 34 |

**It never invented a salary.** Of the 20 test postings that state no pay, the AI returned `null` for all 20. A missing salary stays missing instead of getting a plausible-looking number.

### Why not just run GPT on the descriptions yourself?

You can. But then you'd be rebuilding and testing what's already proven here:

- **Fetching:** three different ATS APIs with different field names, pagination, rate limits, HTML-encoded descriptions, and dead boards that need auto-detection.
- **Prompting:** handling the edge cases. Multiple pay zones (report the overall min to max), "2–3 years" that isn't a salary, Canadian `$` that isn't USD, and a city name that means onsite.
- **Schema enforcement:** constrained JSON output so every record parses, with automatic escalation to a stronger model when the fast one isn't confident.
- **Evaluation:** a labeled test set and scoring harness. Without one, you don't know your accuracy. You just hope.

Most of the work is that last mile, and this Actor already ships it for **$4 per 1,000 jobs** with no API key, prompt engineering, or eval set to maintain.

### How to use it

1. Click **Try for free** and add companies to **Companies**: a board slug (`gusto`) or a careers URL (`https://jobs.lever.co/palantir`).
2. Leave **ATS** on **Auto-detect**, or pick Greenhouse, Lever, or Ashby.
3. Turn on **AI normalize** if you want the extracted fields.
4. Optionally add filters and a per-company cap, then click **Start**.
5. Download the results as JSON, CSV, Excel, or HTML, or pull them through the Apify API.

#### Input example

```json
{
  "companies": ["gusto", "https://jobs.lever.co/palantir", "ramp"],
  "ats": "auto",
  "aiNormalize": true,
  "filters": { "keywords": ["engineer", "data"], "remoteOnly": false, "postedWithinDays": 30 },
  "maxJobsPerCompany": 200
}
```

| Field | Type | Description |
|---|---|---|
| `companies` | string\[] | **Required.** Company slugs or careers-page URLs on Greenhouse, Lever, or Ashby. |
| `ats` | enum | `auto` (default) tries Greenhouse, then Lever, then Ashby. Or force one: `greenhouse`, `lever`, `ashby`. |
| `aiNormalize` | boolean | Add the AI-extracted `ai` object to each job. Default `false`. |
| `filters.keywords` | string\[] | Keep jobs whose **title or department** contains any keyword (case-insensitive). |
| `filters.remoteOnly` | boolean | Keep only roles that look remote. |
| `filters.postedWithinDays` | integer | Keep jobs posted in the last N days. Jobs with no date are kept. |
| `maxJobsPerCompany` | integer | Cap jobs per company (default 500). |

Filters run **before** AI normalization, so you're never charged AI fees for jobs you filtered out.

### Output

You get one record per job. Here is a real one from our test run (description shortened):

```json
{
  "company": "gusto",
  "ats": "greenhouse",
  "jobId": "7918214",
  "title": "Staff Software Engineer, Tax Experiences",
  "url": "https://job-boards.greenhouse.io/gusto/jobs/7918214",
  "location": "San Francisco, CA - Hybrid",
  "department": "Engineering",
  "postedAt": "2026-07-06",
  "descriptionText": "About Gusto\n\nAt Gusto, we're on a mission to grow the small business economy. We handle the hard stuff — payroll, health insurance, 401(k)s, and HR — so owners can focus on their craft and their customers…",
  "ai": {
    "salaryMin": 163000,
    "salaryMax": 247000,
    "salaryCurrency": "USD",
    "salaryPeriod": "year",
    "seniority": "staff",
    "workMode": "hybrid",
    "visaSponsorship": "unknown",
    "techStack": ["React", "TypeScript", "Ruby on Rails", "SQL", "AI"],
    "yearsExperienceMin": 8,
    "confidence": 0.92
  }
}
```

| Field | Description |
|---|---|
| `company`, `ats`, `jobId` | Where the job came from. `ats:company:jobId` is unique and stable across runs, so you can use it for deduplication and change tracking. |
| `title`, `department`, `location`, `url` | As posted. |
| `postedAt` | `YYYY-MM-DD`. Published or last-updated date, depending on the ATS. |
| `descriptionText` | The full description as plain text (HTML removed). |
| `ai.salaryMin` / `ai.salaryMax` | Numbers. Lowest and highest pay across all stated ranges and zones. `null` if no pay is stated. |
| `ai.salaryCurrency` / `ai.salaryPeriod` | ISO code (`USD`, `CAD`, `GBP`, …) and `year` / `month` / `week` / `day` / `hour`. |
| `ai.seniority` | `intern`, `junior`, `mid`, `senior`, `staff`, `principal`, `lead`, `manager`, `director`, `executive`. |
| `ai.workMode` | `remote`, `hybrid`, or `onsite`. |
| `ai.visaSponsorship` | `yes` / `no` only when the posting says so, otherwise `unknown`. |
| `ai.techStack` | Technologies, languages, frameworks, and tools named in the posting. |
| `ai.yearsExperienceMin` | Minimum years required, as an integer. |
| `ai.confidence` | The model's own 0–1 confidence. Below 0.6, the job is automatically re-checked by a stronger model. |

The `ai` object appears only when **AI normalize** is on and extraction succeeded for that job. The dataset has two views: **Jobs overview** (the key fields as a table) and **Full details** (everything).

### Pricing

This Actor uses **pay-per-event** pricing. You pay for results, not compute time.

| Event | Price | Charged when |
|---|---|---|
| Job scraped | **$1.00 per 1,000 jobs** ($0.001 each) | Once for each job delivered to your dataset |
| Job AI-normalized | **$4.00 per 1,000 jobs** ($0.004 each) | Once for each job the AI successfully enriched (only with **AI normalize** on) |

| Example run | Cost |
|---|---|
| 1,000 jobs, scraping only | **$1.00** |
| 1,000 jobs with AI normalization | **$5.00** |
| 10,000 jobs with AI normalization | **$50.00** |

- If the AI fails on a job, you're not charged the AI fee for it.
- Filtered-out jobs are free.
- **Max cost per run is respected.** If you set a spending limit, the Actor works out how many jobs the remaining budget covers before it calls the AI, delivers exactly that many, and stops cleanly. You're never billed past your limit, and every job you're billed for arrives.
- Apify adds a tiny per-run start fee (fractions of a cent).

### FAQ

**Do I need my own OpenAI or Anthropic API key?**
No. AI normalization runs on Claude (Anthropic) and is included in the per-job price.

**Which companies are supported?**
Any company whose careers page runs on Greenhouse, Lever, or Ashby. That covers thousands of tech and growth companies. A company's slug is usually the last part of its board URL, like `job-boards.greenhouse.io/`**`gusto`** or `jobs.lever.co/`**`palantir`**. If you're unsure, use **Auto-detect** or paste the full careers URL.

**What happens if a company isn't on any of the three?**
The Actor logs a warning, skips it, and moves on. You aren't charged for it.

**How fresh is the data?**
It's live. Every run reads the current board, so a job removed from the board won't show up in your next run.

**How do I track new jobs over time?**
Schedule the Actor (for example daily) and compare runs on `ats:company:jobId`.

**Is scraping job boards allowed?**
This Actor only reads **public** job-board endpoints that the ATS vendors publish for this purpose. It collects job data only, no personal data. You're responsible for how you use the results.

**Can I use it from code, Zapier, Make, or n8n?**
Yes. Like any Apify Actor, you can start it and read its dataset through the Apify API, SDKs, webhooks, and integrations.

### Limitations

- **Multi-zone salary bands.** Some postings list several geographic pay zones. The AI is told to report the overall lowest-to-highest range, but occasionally it reports only the primary zone and misses a lower minimum from a cheaper location (about 1 in 50 in testing).
- **Occasional non-USD miss.** Non-USD salaries are usually extracted correctly, but in testing the AI once missed a clearly stated GBP range (about 1 in 50).
- **AI fields are only as good as the posting.** If a posting doesn't mention remote work, years, or visas, the field is `null` or `unknown`. That's by design.
- **Three ATSs only.** Workday, iCIMS, SmartRecruiters, and custom career sites aren't supported yet.
- **`postedAt` meaning varies by ATS.** Some report the publish date and others the last update.
- **Keyword filter checks the title and department only**, not the full description, to avoid false matches.

### Feedback

Found a posting the AI got wrong, or need another ATS? Open an issue on the Actor's **Issues** tab with the job URL. Misses get added to the test set, so every fix is measured.

# Actor input Schema

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

Company slugs or careers-page URLs on Greenhouse, Lever, or Ashby. Examples: "stripe", "https://jobs.lever.co/palantir".

## `ats` (type: `string`):

Which applicant-tracking system to query. "auto" tries Greenhouse, then Lever, then Ashby, and accepts the first that recognizes the company.

## `aiNormalize` (type: `boolean`):

Use Claude to extract salary, seniority, work mode, visa sponsorship, tech stack, and years of experience from each posting. Billed per normalized job.

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

Cap the number of jobs kept per company.

## `filters` (type: `object`):

Optional. { "keywords": \["engineer"], "remoteOnly": false, "postedWithinDays": 30 }

## `canaryMode` (type: `boolean`):

For scheduled monitoring runs. Fails the run if any listed board returns 0 jobs, can't be matched, errors, or (with AI on) no job gets AI fields. Leave off for normal scraping.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "https://jobs.lever.co/palantir"
  ],
  "ats": "auto",
  "aiNormalize": false,
  "maxJobsPerCompany": 500,
  "filters": {
    "keywords": [],
    "remoteOnly": false,
    "postedWithinDays": 30
  },
  "canaryMode": false
}
```

# Actor output Schema

## `overview` (type: `string`):

All scraped jobs, shown with the overview table (title, company, work mode, salary, tech stack).

## `details` (type: `string`):

Every field, including the full description text and the complete AI object.

# 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",
        "https://jobs.lever.co/palantir"
    ],
    "filters": {
        "keywords": [],
        "remoteOnly": false,
        "postedWithinDays": 30
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("gingerwolfe/ats-jobs-ai").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",
        "https://jobs.lever.co/palantir",
    ],
    "filters": {
        "keywords": [],
        "remoteOnly": False,
        "postedWithinDays": 30,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("gingerwolfe/ats-jobs-ai").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",
    "https://jobs.lever.co/palantir"
  ],
  "filters": {
    "keywords": [],
    "remoteOnly": false,
    "postedWithinDays": 30
  }
}' |
apify call gingerwolfe/ats-jobs-ai --silent --output-dataset

```

## MCP server setup

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

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/tRfGjMEyA0W7tznMH/builds/jMfzEKBhXXAS5UBjR/openapi.json
