# Career Page Jobs Scraper & Monitor - Greenhouse, Lever, Ashby (`neverempty/career-page-jobs-monitor`) Actor

Open jobs from company careers pages that use an ATS with a public job feed (Greenhouse, Lever, Ashby, Workable, Workday, BambooHR, Teamtailor) or publish JobPosting JSON-LD: title, location, remote, department, salary, job URL. Monitoring returns only new and removed jobs. Unreadable ones are free.

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

## Pricing

from $2.80 / 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 Page Jobs Scraper & Monitor - Greenhouse, Lever, Ashby

Give this Actor the careers pages (or just the domains) of the companies **you** care about. It works for companies whose careers page uses an ATS with a public job feed (Greenhouse, Lever, Ashby, Workable, Workday, BambooHR, Teamtailor) or publishes schema.org JobPosting data; it does not work for every company. It reads each company's own site at run time and returns that company's open jobs, one row per job, and says how it read them. In monitoring mode it returns only the jobs that **appeared** and the jobs that **disappeared** since the last run.

It is not a job database that you search. It reads the companies you name, when you run it.

### What you get per job

`title`, `location`, `remote` (only when the source states it), `workplaceType`, `employmentType`, `department`, `postedAt`, `salary` (when published), `jobUrl`, `jobId`, `descriptionExcerpt` (first 300 characters), `company`, plus where the row came from:

- `source` - `json-ld` or the ATS name (`greenhouse`, `lever`, ...)
- `atsBoard` - the board identifier that was written on the company's page, for example `greenhouse:figma`
- `evidence` - the exact text on the page that named the board, and what tied the board to this company
- `coverage` - `all-jobs-in-feed`, `partial` or `jobs-on-this-page`

### How a company is read

1. The page you gave is fetched (for a bare domain: the home page, up to two careers links found on it, then `/careers` and `/jobs`; at most 4 pages).
2. If the HTML names a job board of a supported ATS, the **public job feed** of that board is read. This gives the complete list.
3. Otherwise, if the HTML has schema.org `JobPosting` data (JSON-LD), those jobs are returned (`coverage: jobs-on-this-page` - only what that one page lists).
4. Otherwise you get a free `no-jobs-found` row. Jobs are never guessed from link texts.

#### ATS that can be read

| ATS | Feed that is read | Notes |
|---|---|---|
| Greenhouse | Job Board API | |
| Lever | Postings API | |
| Ashby | Posting API | |
| Workable | public widget API | |
| Workday | careers site JSON endpoint | at most 500 jobs per company; larger boards are returned as `partial` and are not monitored |
| BambooHR | `/careers/list` | no posting date in the list |
| Teamtailor | `/jobs.rss` | also on a company's own domain |

**Not read: SmartRecruiters.** The `robots.txt` of its job API disallows all paths, so this Actor does not request it. A company whose page names a SmartRecruiters board comes back as a free `robots-disallowed` row.

#### What cannot be read (you are not charged for these)

- **Careers pages that load their jobs only with JavaScript** and do not write an ATS board or JobPosting data into the HTML. Many large companies build their page this way. In a test of 40 well-known companies' careers pages on 2026-10-06 (10 large companies, 15 startups, 8 European, 2 Japanese, 5 with a home-built careers site), 14 returned jobs (all through an ATS feed, 2,433 jobs), 22 had no ATS board and no JobPosting data in their HTML, 3 refused the request or their robots.txt could not be read, and 1 address was a 404. Expect roughly one careers page in three to be readable from the page address alone. If you know the company's job board address (for example `https://jobs.ashbyhq.com/acme`), give that address instead: it is read directly.
- Sites that answer 401/403/429 or a verification page. The Actor does not retry around them.
- Hosts whose `robots.txt` disallows the address (checked at run time for every host, including the ATS feed host).
- ATS that are not in the table (SmartRecruiters, Recruitee, Personio, iCIMS, SuccessFactors, Phenom, Eightfold, Talentio, HRMOS and others).

#### A board must belong to the company

A page can link to another company's board (a parent, a partner, a portfolio company). The board identifier is only used when it is the address you gave, or it is written in the page HTML **and** it can be tied to the site: its name matches the site's host name, or the feed's company name or job addresses do. If a page names several boards and exactly one matches the site name, that one is used; otherwise none is used and you get a free `no-jobs-found` row that lists what was seen. Identifiers are never made up from a company name.

### Monitoring mode

- First run for a company: its jobs are remembered. Nothing is returned, nothing is charged (`baseline-recorded`).
- Later runs: one row per new job (`change: new-job`) and one per job that is no longer listed (`change: removed-job`, with the remembered title, URL and location).
- No change: a free `no-change` row, and one `company-checked` event is charged.
- A company that could not be read in a run is free, and its memory is left untouched, so jobs are not reported as removed because of a failed read.
- If a company's source changes (for example it moves to another ATS), the list is recorded again instead of reporting every job as removed and new.
- Memory is kept per address exactly as you give it. JSON-LD sources are compared by job URL and only cover the jobs written on that page.
- Runs for the same company should not overlap.

### Pricing (pay per event)

- `job-returned` - each job row returned (in monitoring mode: each new or removed job).
- `company-checked` - monitoring mode only: each company that was compared and had no change.
- Free: companies that could not be read, companies with no readable jobs, the first monitoring run, every explanatory row. No start fee.

### Input example

```json
{
    "companies": ["https://www.figma.com/careers/", "https://jobs.lever.co/palantir", "vercel.com"],
    "maxJobsPerCompany": 500,
    "monitoringMode": false
}
```

### Related

If you already know the ATS board names and want filters across many boards, use [ATS Jobs API](https://apify.com/neverempty/ats-jobs-api).

### Notes

- Requests identify themselves as `NeverEmpty/1.0 (+https://apify.com/neverempty)`, go out one at a time, and wait more than one second between requests to the same host.
- `remote` is null unless the source says so. `salary` and `postedAt` are null when the source does not publish them. Nothing is estimated.
- Workday does not give a posting date in its list; BambooHR does not give date or salary in its list.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

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

One per line: the address of a company's careers page (https://www.figma.com/careers/), the address of its job board (https://jobs.lever.co/palantir), or just its domain (vercel.com). For a domain the Actor reads the home page, up to two careers links found on it, and /careers and /jobs. At most 500 companies per run. Only these sites and the public job feed of the ATS they name are read.

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

Return at most this many job rows for one company (1 to 2000). Jobs beyond it are not returned and not charged; a free row says how many were left out. Not used in monitoring mode.

## `monitoringMode` (type: `boolean`):

The first run for a company remembers its jobs and returns nothing for it (free). Later runs with the same address return one row for every job that appeared (change: new-job) and one for every job that disappeared (change: removed-job). A company with no change costs one company-checked event and returns a free no-change row. Companies that could not be read are free and their memory is left as it was.

## `resetMonitoringState` (type: `boolean`):

Clears what monitoring mode remembers for the companies in this run before reading them, so the run becomes a first run for them.

## Actor input object example

```json
{
  "companies": [
    "https://www.figma.com/careers/",
    "https://jobs.lever.co/palantir",
    "vercel.com"
  ],
  "maxJobsPerCompany": 500,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per job of each company given: title, location, remote, employment type, department, posting date, salary, job URL, description excerpt, the source it was read from (json-ld or the ATS name), the ATS board and the evidence that tied the board to the company. In monitoring mode only new and removed jobs. Companies that could not be read, have no readable jobs, or did not change come back as their own rows and those rows are not charged.

# 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://www.figma.com/careers/",
        "https://jobs.lever.co/palantir",
        "vercel.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/career-page-jobs-monitor").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://www.figma.com/careers/",
        "https://jobs.lever.co/palantir",
        "vercel.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/career-page-jobs-monitor").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://www.figma.com/careers/",
    "https://jobs.lever.co/palantir",
    "vercel.com"
  ]
}' |
apify call neverempty/career-page-jobs-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/career-page-jobs-monitor"
        }
    }
}
```

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/8GyBkv8LU2zfb0iU1/builds/RvocgfrihsXgUumrl/openapi.json
