# Greenhouse, Lever & Ashby Jobs: New & Closed Roles (`jp-data-lab/greenhouse-lever-ashby-jobs`) Actor

Job postings from company career boards on Greenhouse, Lever and Ashby, in one format, from their public APIs. Schedule it to get only new and closed roles. Filter by title, location, remote and salary. No proxy, no browser, pay per job.

- **URL**: https://apify.com/jp-data-lab/greenhouse-lever-ashby-jobs.md
- **Developed by:** [JP Data Lab](https://apify.com/jp-data-lab) (community)
- **Categories:** Jobs, AI
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 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.

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: New & Closed Roles

Get job postings straight from company career boards on Greenhouse, Lever and Ashby, in one clean format. Run it once, or schedule it and receive only new roles, plus the roles that closed. No API key, no browser and no proxy.

Every row comes from the platform's public job board API, so you get what the employer published: title, department, location, workplace type, salary, description and the link to apply.

Use it to build a job board, send role alerts, track competitors' hiring, find companies hiring for what you sell, or give an AI agent fresh postings to match against a CV.

### Quick start

Read a few companies. Use a name, a career board URL, or `platform:slug`:

```json
{ "companies": ["stripe", "https://jobs.lever.co/spotify", "ashby:ramp"] }
```

Daily alert for remote engineering roles at those companies, with closed roles. Schedule this and connect the run to Slack, email or a webhook:

```json
{
  "companies": ["stripe", "lever:spotify", "ashby:ramp"],
  "onlyNew": true,
  "includeClosed": true,
  "titleKeywords": ["engineer", "engineers"],
  "excludeTitleKeywords": ["intern"],
  "remoteOnly": true
}
```

Search the bundled directory instead of listing companies:

```json
{
  "directoryCategories": ["ai", "fintech"],
  "titleKeywords": ["product manager"],
  "locationKeywords": ["London", "Remote"],
  "minAnnualSalary": 120000,
  "maxJobs": 200
}
```

### Finding a company

| You enter | What happens |
|---|---|
| `stripe` | Looked up in the directory. If it is not there, tried on Greenhouse, Lever and Ashby in that order, and the first board with jobs is used |
| `https://job-boards.greenhouse.io/stripe` | Greenhouse, Lever (including EU) and Ashby board URLs are recognized |
| `greenhouse:stripe`, `lever:spotify`, `lever-eu:acme`, `ashby:ramp` | Read exactly that board |

A name can match a different company with the same board name. `companiesResolved` in `RUN_SUMMARY` shows which board was used. Use `platform:slug` when you need to be sure. Company names on Lever and Ashby come from the directory or the board name, because those APIs do not return one.

The **directory** holds 333 boards that had open jobs when they were checked against the official APIs: 189 on Greenhouse, 123 on Ashby and 21 on Lever. It is a starting point and not a list of every employer. Any public board works through `companies`.

### How onlyNew works

- **First run:** outputs the newest matching jobs up to your limits and marks all other matching jobs as seen. You are not charged for those.
- **Later runs:** outputs only jobs not seen before. If there are more than `maxJobsPerCompany`, the rest wait for the next run.
- **Nothing new:** the board's API is asked whether anything changed. If not, it is not downloaded again and you pay no result charges.
- **Closed jobs:** with `includeClosed`, a job you received earlier that has left the board is output once with `status: "closed"`. The platforms do not say why a job left: it may be filled, cancelled or reposted under a new ID.
- **Changing a filter** starts a fresh record for that board. `postedWithinDays` and the limits do not.
- A board that suddenly returns no jobs at all is not treated as closing everything. It is counted under `emptyBoardsKept`.

The record is saved in a key-value store named `ats-job-boards-seen` in your Apify account. Delete it to start again. Set `monitorName` to keep two schedules with the same search separate.

### Output

One row per job:

```json
{
  "id": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "status": "open",
  "ats": "ashby",
  "board": "ramp",
  "company": "Ramp",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "country": "USA",
  "workplaceType": "hybrid",
  "employmentType": "Full-time",
  "postedAt": "2026-04-07T17:12:35Z",
  "updatedAt": null,
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "salaryInterval": "year",
  "salaryText": "$211.4K - $290.6K",
  "salarySource": "field",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "description": "Full text, up to 50,000 characters.",
  "descriptionHtml": null,
  "closedAt": null,
  "fetchedAt": "2026-11-02T00:00:00Z"
}
```

Dates are UTC. Fields a platform does not provide are `null`. Closed rows carry the title, location and link from the earlier run, with `closedAt` set.

What each platform provides:

| Field | Greenhouse | Lever | Ashby |
|---|---|---|---|
| `workplaceType` | Read from the location text, otherwise `null` | Yes | Yes |
| `employmentType` | No | Yes, in the employer's wording | Yes |
| `country` | No | Yes (ISO code) | Yes, in the employer's wording, often missing |
| `team` | No | Yes | Yes |
| `updatedAt` | Yes | No | No |
| Salary | From the description | Field, or from the description | Field, or from the description |

`salarySource` is `field` when the platform provides the pay range, and `description` when it was read from the text. With `description`, the first range in the text is used, and many employers list several ranges by region. Check `salaryText` and the posting before relying on it.

### Personal details

The public job APIs do not return recruiter or hiring manager details, and this Actor does not read Greenhouse's custom `metadata` field. Descriptions are written by employers and can mention people, so email addresses, phone numbers and names in phrases such as "reports to Jane Doe" are replaced with `[email removed]`, `[phone removed]` and `[name removed]`. In a check of 3,906 jobs, 2 descriptions named a person this way. This cannot catch every mention. `detailsRemoved` in `RUN_SUMMARY` counts the replacements.

### Input

| Field | Default | Meaning |
|---|---|---|
| `companies` | 3 examples | Names, board URLs or `platform:slug`, up to 5,000 boards in total |
| `directoryCategories` | none | Also read the bundled directory: `all`, `ai`, `fintech`, `devtools`, `security`, `saas`, `consumer`, `health`, `crypto`, `climate`, `hardware` |
| `onlyNew` | `false` | Output only jobs not seen in earlier runs |
| `includeClosed` | `false` | With `onlyNew`, also output jobs that left the board |
| `monitorName` | none | Keeps two schedules with the same search separate |
| `titleKeywords` | none | Keep jobs whose title contains any of these |
| `excludeTitleKeywords` | none | Leave out jobs whose title contains any of these |
| `locationKeywords` | none | Keep jobs whose location contains any of these |
| `departmentKeywords` | none | Keep jobs whose department or team contains any of these |
| `descriptionKeywords` | none | Keep jobs whose description contains any of these |
| `remoteOnly` | `false` | Only fully remote jobs. Hybrid is left out |
| `postedWithinDays` | `0` | Only jobs posted in the last N days |
| `onlyWithSalary` | `false` | Only jobs with a pay range |
| `minAnnualSalary` | `0` | Range reaches this yearly amount, in the job's own currency. Hourly and monthly pay are converted |
| `includeDescription` | `true` | Add `description` |
| `includeHtml` | `false` | Add `descriptionHtml` |
| `maxJobsPerCompany` | `50` | Newest first |
| `maxJobs` | `1000` | Total for the run |

Keywords ignore case and accents. English words match whole words, so `engineer` does not match `engineers`: add both forms.

### When a board fails

One bad board does not stop the run, and it costs nothing. `RUN_SUMMARY` lists each one under `boardsFailed` with the reason. The run fails only if no board could be read.

```json
{
  "boardsRequested": 3,
  "boardsRead": 2,
  "boardsUnchanged": 0,
  "boardsFailed": [{ "company": "acme", "reason": "no job board found on Greenhouse, Lever or Ashby" }],
  "boardsNotReached": 0,
  "firstReadBoards": 0,
  "jobsRead": 540,
  "pushed": 100,
  "closed": 0,
  "seenBefore": 0,
  "filteredOut": 400,
  "overLimit": 40,
  "detailsRemoved": 12,
  "sources": { "greenhouse": { "fetched": 500, "boards": 1 }, "ashby": { "fetched": 40, "boards": 1 } }
}
```

`boardsNotReached` counts boards left unread because the run hit `maxJobs` or your spending limit. `overLimit` counts matching jobs left out by the limits.

Long runs can be moved to another server by Apify. The run saves its progress and continues where it stopped instead of starting over. A run of all 333 boards with no limits took about 23 minutes and returned about 36,500 jobs.

### What it respects

- **Official APIs only.** It reads the documented public endpoints of Greenhouse's Job Board API, Lever's Postings API and Ashby's job posting API. It does not open career pages or Workday, and SmartRecruiters is left out because its robots.txt disallows the API.
- **robots.txt.** It is checked for each host. There is no switch to override it.
- **Server load.** At least one second between requests to the same host, longer if `Crawl-delay` asks for it, up to 5 seconds. Failed requests are retried twice at most.
- **Honest identification.** Requests carry a descriptive User-Agent with a link to this page. There is no proxy and no browser disguise.

### Limits

- Only currently published jobs. The platforms do not provide closed jobs or history, so run it on a schedule to build your own archive.
- Only companies that use these three platforms and publish a board. Many large employers use Workday or their own system.
- Boards over 60 MB are skipped.

Job postings belong to the employers who wrote them, and this Actor is not affiliated with Greenhouse, Lever, Employ or Ashby. You are responsible for how you use the data.

### Pricing

Pay per job delivered, plus a small charge per run. Failed boards, jobs filtered out, jobs already seen and unchanged boards are free. A closed row counts as one job. Set `maxJobs` or a spending limit in the run options to control the cost.

# Actor input Schema

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

Up to 5,000. Use a company name (stripe), a career board URL (https://jobs.lever.co/spotify), or platform:slug (ashby:ramp). A name that is not in the directory is tried on Greenhouse, Lever and Ashby, and the match is listed in RUN_SUMMARY.

## `directoryCategories` (type: `array`):

Also read the verified company boards bundled with this Actor. Choose all, or one or more categories. Combine with title and location filters, because you pay for each job returned.

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

Remember what this account has received and output only jobs not seen before. Use it with a schedule. The first run outputs the newest jobs up to the limits and marks the other matching jobs as seen. Changing a filter starts a fresh record.

## `includeClosed` (type: `boolean`):

With Only new jobs: also output a row with status closed when a job you received earlier leaves the board. Each closed row is charged as one result.

## `monitorName` (type: `string`):

Only needed to keep two schedules with the same companies and filters separate, for example one per client.

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

Keep jobs whose title contains any of these. Case and accents are ignored. English words match whole words, so add plurals.

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

Leave out jobs whose title contains any of these, for example intern or senior.

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

Keep jobs whose location contains any of these, for example London or Remote. Locations are the employer's own wording.

## `departmentKeywords` (type: `array`):

Keep jobs whose department or team contains any of these.

## `descriptionKeywords` (type: `array`):

Keep jobs whose description contains any of these, for example python or visa sponsorship.

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

Keep only jobs marked as fully remote. Hybrid jobs are left out. On Greenhouse this is read from the location text.

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

Only jobs posted in the last N days. 0 = no limit.

## `onlyWithSalary` (type: `boolean`):

Keep only jobs with a pay range, from the platform's own field or read from the description.

## `minAnnualSalary` (type: `integer`):

Keep only jobs whose range reaches this yearly amount, in the job's own currency. Hourly and monthly pay are converted. Implies only jobs with a salary. 0 = no limit.

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

Add the job description as plain text. Turn it off for much smaller rows.

## `includeHtml` (type: `boolean`):

Also add the description as HTML.

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

Newest first.

## `maxJobs` (type: `integer`):

Total for the whole run. You are charged per result.

## Actor input object example

```json
{
  "companies": [
    "gitlab",
    "lever:spotify",
    "ashby:linear"
  ],
  "onlyNew": false,
  "includeClosed": false,
  "remoteOnly": false,
  "postedWithinDays": 0,
  "onlyWithSalary": false,
  "minAnnualSalary": 0,
  "includeDescription": true,
  "includeHtml": false,
  "maxJobsPerCompany": 50,
  "maxJobs": 1000
}
```

# Actor output Schema

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

One row per job: company, title, department, location, workplace type, dates, salary, links and description. Closed jobs have status closed.

## `runSummary` (type: `string`):

Boards read, unchanged and failed with the reason, names matched to a board, jobs already seen, and jobs left out by the filters or limits.

# 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": [
        "gitlab",
        "lever:spotify",
        "ashby:linear"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jp-data-lab/greenhouse-lever-ashby-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": [
        "gitlab",
        "lever:spotify",
        "ashby:linear",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("jp-data-lab/greenhouse-lever-ashby-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": [
    "gitlab",
    "lever:spotify",
    "ashby:linear"
  ]
}' |
apify call jp-data-lab/greenhouse-lever-ashby-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jp-data-lab/greenhouse-lever-ashby-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/C1o56abH01qXQWSZp/builds/brzEG99vWyA3Lnadn/openapi.json
