# Greenhouse, Lever & Ashby Job Scraper & Monitor (`olegtestov/greenhouse-lever-ashby-job-changes`) Actor

Track job postings at your target companies. Get NEW, CHANGED and confirmed CLOSED events from Greenhouse, Lever and Ashby, with stable event IDs for deduplication. Ready for n8n and Slack. Free Actor; Apify platform usage applies.

- **URL**: https://apify.com/olegtestov/greenhouse-lever-ashby-job-changes.md
- **Developed by:** [Oleg Testov](https://apify.com/olegtestov) (community)
- **Categories:** Jobs, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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 Job Scraper & Monitor

**Monitor a recruiter watchlist daily and get only new, changed and confirmed closed jobs from Greenhouse, Lever and Ashby. Connect the feed to n8n, Slack or Google Sheets. The Actor is free; Apify platform usage applies.**

### Quick start

1. Run the sample watchlist and inspect CURRENT jobs.
2. Replace it with your companies and save an Actor task.
3. Schedule the task daily with the same Monitor ID. Later runs return only NEW, CHANGED and CLOSED rows; send them to Slack, Google Sheets, n8n or a webhook with Apify integrations.

The sample input has *Show current jobs on the first run* turned on, so the first run lists every open job as `CURRENT`. CURRENT rows are a baseline, not new-job alerts. Every later run with the same Monitor ID returns only what changed since the previous run; an empty dataset means nothing changed.

### Who uses it

- **Recruiters and staffing agencies**: see new roles at target accounts the day they are posted, and know when a posting is taken down.
- **Sales and growth teams**: hiring signals. A company opening five data-engineer roles is a lead for a data tool.
- **Job boards and job-alert products**: a deduplicated feed of new postings straight from the employer's board.
- **Investors and market analysts**: new teams and new locations across a portfolio.
- **AI agents (MCP)**: "which roles did these companies open or close since the last check?"

### Example events

The three rows below are made-up examples (company "Acme"); real rows have the same fields.

A new job:

```json
{
  "changeType": "NEW",
  "eventId": "5c1f0e9a7b2d4c6e8a10",
  "company": "Acme",
  "ats": "greenhouse",
  "title": "Senior Data Engineer",
  "locations": ["Madrid, Spain"],
  "workplaceType": "hybrid",
  "department": "Engineering",
  "url": "https://job-boards.greenhouse.io/acme/jobs/1234567",
  "postedAt": "2026-10-06T09:12:00Z",
  "checkedAt": "2026-10-07T06:00:00Z"
}
```

A changed job, with the old value:

```json
{
  "changeType": "CHANGED",
  "eventId": "9d2a4b6c8e0f1a3b5c7d",
  "company": "Acme",
  "ats": "ashby",
  "title": "Staff Backend Engineer",
  "changedFields": ["title"],
  "previousValues": {"title": "Senior Backend Engineer"},
  "url": "https://jobs.ashbyhq.com/acme/0b6e7c1a-4d2f-4e8a-9c3b-2f1d5a7e9c11",
  "checkedAt": "2026-10-07T06:00:00Z"
}
```

A closed job:

```json
{
  "changeType": "CLOSED",
  "eventId": "e4f6a8b0c2d4e6f8a0b2",
  "company": "Acme",
  "ats": "lever",
  "title": "Account Executive, DACH",
  "url": "https://jobs.lever.co/acme/7a1c3e5b-9d2f-4b6a-8c0e-1f3a5c7e9b2d",
  "firstSeenAt": "2026-09-14T06:00:00Z",
  "closedAt": "2026-10-07T06:00:00Z",
  "checkedAt": "2026-10-07T06:00:00Z"
}
```

| changeType | Meaning |
|---|---|
| `NEW` | The job appeared since the previous run |
| `CHANGED` | Title, department, team, location, workplace type, employment type or salary changed. `changedFields` and `previousValues` show what changed |
| `CLOSED` | The posting disappeared from the board and the closure was confirmed (`closedAt` is set) |
| `CURRENT` | Snapshot mode, or the first run when *Show current jobs on the first run* is on |

All fields: `eventId` (stable id of this change, for de-duplication), `changeType`, `company`, `ats`, `boardId`, `boardUrl`, `detection`, `checkedAt`, `jobId`, `title`, `url`, `applyUrl`, `department`, `team`, `locations`, `workplaceType` (onsite / hybrid / remote), `employmentType`, `postedAt`, `updatedAt`, `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText`, `requisitionId`, `changedFields`, `previousValues`, `firstSeenAt`, `closedAt`, and `description` (empty unless you turn on *Include job description*).

### Input

- **Companies to watch**, one per line. Board URLs are most reliable: `https://job-boards.greenhouse.io/stripe`, `https://jobs.lever.co/palantir`, `https://jobs.ashbyhq.com/ramp`. A company name (`Stripe`), a domain or careers page (`ramp.com`) or an explicit board (`greenhouse:stripe`, `lever-eu:<slug>`) also work; check what was detected in the run's `SUMMARY` before you schedule. Optional label: `Stripe | stripe.com`.
- **Monitor ID** identifies this watchlist's saved history in your Apify account. Keep it unchanged for recurring checks; use another ID (for example `recruiter-watchlist`) for a separate watchlist. Changing it starts a new baseline.
- **Show current jobs on the first run**: on in Apify Console. API calls and tasks that leave it out keep it off, and the first run then only saves the baseline.
- Optional filters: title keywords (`engineer`, `data`), excluded words (`intern`), location (`Spain`, `remote`), department. Filters only reduce output rows.
- **Output mode = Full snapshot every run** returns every open job each time instead of changes.

The saved state lives in a named key-value store `ats-job-monitor-<id>-<hash>` in your own Apify account (the exact name is in the run's `SUMMARY`); delete it to start over. The `SUMMARY` record lists every input line with its status, detected board, open jobs and counts.

### API and AI agents

- **API**: the API tab on this page shows ready-made calls. `run-sync-get-dataset-items` starts a run and returns its rows in one request.
- **MCP**: connect your AI client to the Apify MCP server (`https://mcp.apify.com`) and find this Actor with the `search-actors` tool ("greenhouse lever ashby job monitor"). The first call with a new Monitor ID only saves a baseline (pass `"emitBaselineOnFirstRun": true` to also get the current jobs); an agent cannot get yesterday's changes without an earlier run.

### Price

The Actor has no fee. Apify charges your account for compute, storage and other applicable platform usage. We will announce any Actor fee at least 14 days before enabling it.

### Delivery

Delivery is **at-least-once**. If the rows were written but saving the state failed, the next run sends the same changes again with the same `eventId`. De-duplicate by `eventId` if your destination must not see a change twice. A later, separate occurrence of the same change (for example a title that changes back and forth) gets a new `eventId`. A missing or damaged saved state (new Monitor ID, deleted store, or a record with the wrong structure) starts a new baseline with new `eventId` values. If the saved state cannot be read at all (a storage error), that company's check fails for this run: no rows, no charge, and the state is left as it was.

### Safety against false "closed" alerts

Closures require two missing observations and an additional check. Large disappearances are held for review. CLOSED means the posting disappeared; it does not establish that someone was hired.

- A failed or detectably bad read is an error for that company and changes nothing: HTTP 403 or 429, timeouts, a Greenhouse response whose `meta.total` is missing or differs from the jobs returned, an error response, postings without id or title, or the same id twice.
- A job is reported `CLOSED` only after it has been missing in **2 runs in a row** (set *Confirm closure after N runs*). Before closing, the board is read once more; a job present in that read stays open and its count starts over. A job that reappears in any later run starts over too.
- **Mass disappearances are held, not closed.** When 3 or more jobs and more than 30% of a board vanish in one check, they stay *pending*: no `CLOSED` rows, `pendingClosures` in the run summary. A source that keeps returning the same cut-down list looks exactly like this, and the Actor cannot tell the two apart. Turn on *Report mass disappearances as closed* if you prefer those closures.
- If the saved state is missing or damaged, the run saves a new baseline and reports no changes.

### Sources and compliance

- Only the official public job-board APIs that employers use to publish their jobs: Greenhouse Job Board API, Lever Postings API and Ashby Job Posting API. No login, no cookies, no browser, no proxy.
- To find a company's board, the Actor reads robots.txt first and fetches at most four pages of the company website (the given URL, `/careers`, `/jobs`, the home page). The result is cached for 30 days.
- No personal data fields. Email addresses and phone numbers are removed from every text field (title, department, team, locations, salary text, company name, your input labels, descriptions, the run summary and error texts), in the output and in the saved state. Fields that are not plain text or numbers are dropped. A job URL must be https with a normal host and no contact in its path or parameters; otherwise the Actor uses the board's standard posting URL (apply URLs are dropped). Job descriptions are **off by default**; a description may still name a contact person if the employer wrote it there. Every dataset row, every record the Actor stores and every log line passes the same contact filter on the way out (URL-encoded text is decoded first; runs of 9+ digits count as phone numbers unless they are the posting's own id). Number ranges without a currency ("45.000 - 55.000") are removed from free text as possible phone numbers, but kept in the structured `salaryText` field when each amount has a thousands separator, a currency or a "k", and the text names no contact. Phone detection is pattern-based: it covers common formats but cannot promise to catch every way a number can be written.
- Requests are rate-limited per host.
- The Actor runs with limited permissions: it uses only its own storages in your account.

### Limits

- Supported job boards: Greenhouse, Lever, Ashby. Other systems (Workday, Workable, Recruitee, SmartRecruiters, Personio and others) are reported as "not supported in this version".
- One run works for at most **1 hour** (or until its run timeout, if shorter). Shortly before that it stops starting new companies and saves nothing more, and at the limit it stops the company in progress. Companies it did not finish are listed as skipped in `SUMMARY` with what was really done; if rows were output but the state was not saved, the next run sends those changes again with the same `eventId`. Split very long watchlists into several tasks with different Monitor IDs.
- Don't run two copies on the same Monitor ID at once; the second one exits without changes. It checks whether the run recorded in the Monitor ID's state is still running. If that run's status cannot be read, the second run stops until 5 minutes after the first run's time limit and says when to retry. This is a check, not a lock: two runs started at the same moment can both proceed, and then they may report the same changes twice or overwrite each other's saved state. In a rare case a save that the first run sent just before its time limit can still be stored after the second run has started; the effect is the same (changes reported again with the same `eventId`, or one run's saved state replaced by the other's).
- Detection by name or domain can miss companies that embed jobs with custom code. Pass the board URL then. Rows found by guessing a name are marked `detection: "slug-guess"`.
- Structured salary comes from Ashby and Lever where the employer publishes it; salary text changes are reported too. Greenhouse usually puts salary only in the description.

### FAQ

**Why is my dataset empty?** Nothing changed since the previous run with this Monitor ID, or it was the first run with *Show current jobs on the first run* off (then it only saved the baseline). The run's status message and `SUMMARY` show the counts per company.

**Can I get the same change twice?** Yes, rarely: after a failed state save the next run re-sends it with the same `eventId`.

**Can I get the full list every run?** Yes: *Output mode = Full snapshot every run*.

**How many companies per run?** Up to 5,000 lines; 5 companies are checked in parallel by default. A run stops after 1 hour; split longer watchlists.

**My company is not found.** Open its careers page, click any job, and paste that board URL (for example `https://jobs.lever.co/acme`). If it uses another system, open an issue and tell us which one.

# Actor input Schema

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

One company per line. Board URLs are most reliable; company names, domains and careers URLs are also accepted. Supported systems: Greenhouse, Lever and Ashby. Check each detected company before scheduling. Examples: https://job-boards.greenhouse.io/stripe, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/ramp, ramp.com, Stripe, greenhouse:stripe. Optional label: 'Stripe | stripe.com'.

## `monitorId` (type: `string`):

Identifies this watchlist's saved history in your Apify account. Keep it unchanged for recurring checks. Choose another ID for a separate watchlist. Changing it starts a new baseline. Example: recruiter-watchlist.

## `emitBaselineOnFirstRun` (type: `boolean`):

Shows CURRENT rows so you can check your watchlist. Later runs return only changes. CURRENT rows are a baseline, not new-job alerts. On by default in Apify Console; API calls and tasks that leave it out keep it off.

## `mode` (type: `string`):

'changes' (monitor): output only jobs that are NEW, CHANGED or CLOSED since the previous run with the same Monitor ID. 'snapshot': output every open job each run (plus CLOSED ones), each marked with its change type. Both modes are free to use; you pay only your Apify platform usage.

## `includeChanged` (type: `boolean`):

Output a CHANGED row when title, department, team, location, workplace type, employment type or salary changes. The row lists changedFields and previousValues.

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

Output a CLOSED row when a job disappears from the board.

## `confirmClosedAfterRuns` (type: `integer`):

A job is reported CLOSED after it has been missing in this many consecutive runs. Default 2. A job that reappears (also in the re-read done before closing) starts over. 1 = report after one missing run.

## `allowMassClosures` (type: `boolean`):

Off by default: when 3 or more jobs and more than 30% of a board vanish in one check, they are held as pending and no CLOSED rows are written, because a source that returns a cut-down list looks exactly the same. Turn on if you prefer to get those closures (after the usual number of missing runs).

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

Case-insensitive. Example: engineer, data, sales. Empty = all titles. Filters only reduce output rows. The Actor is free to use; you pay only your Apify platform usage.

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

Case-insensitive. Example: intern, senior.

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

Case-insensitive match on locations and workplace type. Example: Spain, Madrid, remote.

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

Case-insensitive. Example: engineering, marketing.

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

Off by default. Turn on to add the plain-text job description. Email addresses and phone numbers are always removed from it.

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

Cut descriptions to this many characters. 0 = no limit.

## `guessFromDomain` (type: `boolean`):

If no Greenhouse, Lever or Ashby link is found on the company's careers page, try the company or domain name as a board name. Such rows have detection = 'slug-guess'.

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

How many companies are checked at the same time. Requests to one ATS host are always rate-limited.

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

Not needed: the job-board APIs are public. Use only if your network is blocked.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/databricks",
    "https://jobs.lever.co/zoox",
    "https://jobs.ashbyhq.com/openai"
  ],
  "monitorId": "default",
  "emitBaselineOnFirstRun": true,
  "mode": "changes",
  "includeChanged": true,
  "includeClosed": true,
  "confirmClosedAfterRuns": 2,
  "allowMassClosures": false,
  "includeDescription": false,
  "maxDescriptionChars": 0,
  "guessFromDomain": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One row per CURRENT, NEW, CHANGED or CLOSED job event.

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

Per-company status, counts and billing 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": [
        "https://job-boards.greenhouse.io/databricks",
        "https://jobs.lever.co/zoox",
        "https://jobs.ashbyhq.com/openai"
    ],
    "emitBaselineOnFirstRun": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("olegtestov/greenhouse-lever-ashby-job-changes").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/databricks",
        "https://jobs.lever.co/zoox",
        "https://jobs.ashbyhq.com/openai",
    ],
    "emitBaselineOnFirstRun": True,
}

# Run the Actor and wait for it to finish
run = client.actor("olegtestov/greenhouse-lever-ashby-job-changes").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/databricks",
    "https://jobs.lever.co/zoox",
    "https://jobs.ashbyhq.com/openai"
  ],
  "emitBaselineOnFirstRun": true
}' |
apify call olegtestov/greenhouse-lever-ashby-job-changes --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,olegtestov/greenhouse-lever-ashby-job-changes"
        }
    }
}
```

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/3gJVzlVSDwFQEkb62/builds/zpb2AuKf5wq4VeIWc/openapi.json
