# Career Site Jobs Watch: Greenhouse, Lever & Ashby (`koalabed/career-site-jobs-watch`) Actor

Pull open roles from company career sites on Greenhouse, Lever and Ashby, and see what changed since your last run: new jobs, closed jobs and net hiring change per company. Clean job data, a hiring summary and a readable report.

- **URL**: https://apify.com/koalabed/career-site-jobs-watch.md
- **Developed by:** [Sama Alabed](https://apify.com/koalabed) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job rows

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

## Career Site Jobs Watch: Greenhouse, Lever & Ashby

Pull open roles directly from company career sites and track what changed between runs. Supports **Greenhouse**, **Lever** and **Ashby**. Get clean job data plus **new, still-open and closed roles by company**, a hiring-change summary and a readable report.

It answers one question: **what changed at these companies since I last looked?**

### Who it is for

- **Sales and GTM teams** watching target accounts for hiring signals (a company opening 8 sales roles is a buying signal).
- **Recruiters and agencies** tracking which clients and competitors are hiring or freezing.
- **Investors and analysts** following portfolio or watchlist companies.
- **Job seekers and job boards** who want fresh roles from specific companies without re-reading every careers page.
- **AI agents and automations** that need clean, structured hiring data through one API call.

### What you get

For every company you add:

- **Current open jobs** in one schema across all three job systems: title, department, team, location, workplace type, employment type, posted pay (when the company publishes it), posted date, job link and apply link.
- **New jobs:** jobs that appeared since the last run of your monitor.
- **Closed jobs:** jobs that were open last time and are gone now.
- **A company summary:** open, new, closed and net change, in plain language ("Acme added 8 jobs and closed 3 (net +5), 42 open jobs now.").
- **A run summary:** totals, companies hiring up or down, biggest increases and decreases, who is hiring the most, and which fetches failed.
- **An HTML report** (the `REPORT` record in the run's key-value store) that answers the same questions at a glance.

### Supported job systems

| Job system | Paste a link like | Notes |
|---|---|---|
| Greenhouse | `https://job-boards.greenhouse.io/discord` | Also accepts `boards.greenhouse.io`, EU boards, embed links and single job links. |
| Lever | `https://jobs.lever.co/palantir` | Global and EU (`jobs.eu.lever.co`) boards, posting and apply links. |
| Ashby | `https://jobs.ashbyhq.com/linear` | Board, job and application links. Unlisted (direct-link-only) jobs are skipped. |

You can also paste:

- **A single job link.** The whole board of that company is watched.
- **A company's own careers page**, such as `https://example.com/careers`. It is read once and accepted only if it links to exactly one supported board. Many careers pages load jobs with JavaScript; if nothing is found, paste the board link itself.
- **A short form**, such as `greenhouse:discord`, `lever:palantir`, `lever-eu:acme` or `ashby:linear`.

Other systems (Workday, Workable, SmartRecruiters, iCIMS and more) are recognised and get a clear "not supported yet" status row. They are never silently dropped.

### Input

```json
{
  "companies": [
    { "url": "https://job-boards.greenhouse.io/discord", "name": "Discord" },
    { "url": "https://jobs.lever.co/palantir" },
    { "url": "https://jobs.ashbyhq.com/linear" }
  ],
  "monitorName": "my-watchlist",
  "includeOpenJobs": true,
  "includeNewJobs": true,
  "includeClosedJobs": true,
  "includeDescriptions": false
}
```

| Field | Default | What it does |
|---|---|---|
| `companies` | required | List of `{ "url": "...", "name": "optional label" }`. Plain URL strings also work through the API. |
| `monitorName` | none | Use the same name on future runs to track new and closed jobs. Without a name, runs with the same set of companies share a monitor automatically. |
| `includeOpenJobs` | `true` | Return jobs that were already open last time. Turn off to get **only what changed**. |
| `includeNewJobs` | `true` | Return jobs that appeared since the last run. |
| `includeClosedJobs` | `true` | Return jobs that disappeared since the last run. |
| `includeDescriptions` | `false` | Add the full job description text. This makes results much larger. |

### Output

The dataset is ordered for easy reading and parsing:

1. **Company rows** (`record_type: "company"`), one per company you entered, in your order.
2. **Job rows** (`record_type: "job"`): new jobs first, then closed, then still open.
3. **One summary row** (`record_type: "summary"`) at the end.

Job row:

```json
{
  "record_type": "job",
  "monitor_id": "my-watchlist",
  "company_id": "greenhouse:discord",
  "company_name": "Discord",
  "ats": "greenhouse",
  "job_id": "greenhouse:discord:8806482002",
  "source_job_id": "8806482002",
  "status": "new",
  "title": "Commercial Policy Lead",
  "department": "Policy",
  "team": null,
  "location": "San Francisco Bay Area",
  "workplace_type": null,
  "employment_type": null,
  "compensation": null,
  "job_url": "https://job-boards.greenhouse.io/discord/jobs/8806482002",
  "apply_url": null,
  "posted_at": "2026-09-15T16:52:11.000Z",
  "first_seen_at": "2026-09-29T20:00:00.000Z",
  "last_seen_at": "2026-09-29T20:00:00.000Z",
  "closed_at": null,
  "fetched_at": "2026-09-29T20:00:00.000Z"
}
```

Company row (abridged):

```json
{
  "record_type": "company",
  "company_name": "Linear",
  "company_id": "ashby:linear",
  "ats": "ashby",
  "status": "ok",
  "open_jobs": 28,
  "new_jobs": 1,
  "closed_jobs": 3,
  "net_change": -2,
  "change_summary": "Linear added 1 job and closed 3 jobs (net -2), 28 open jobs now."
}
```

Summary row (abridged):

```json
{
  "record_type": "summary",
  "companies_checked": 3,
  "companies_succeeded": 3,
  "companies_failed": 0,
  "current_open_jobs": 396,
  "new_jobs": 4,
  "closed_jobs": 6,
  "net_job_change": -2,
  "companies_hiring_up": 1,
  "companies_hiring_down": 1,
  "headline": "4 new jobs and 6 closed jobs across 3 companies (net -2). 396 jobs open now. 1 hiring up, 1 hiring down."
}
```

**Nulls are honest.** A field is `null` when the job board does not publish it. Nothing is guessed. Greenhouse, for example, publishes no workplace type or country, and its applications happen on the job page, so `apply_url` is `null` there.

#### Company status values

| Status | Meaning | Charged |
|---|---|---|
| `baseline` | First successful check in this monitor. Jobs are saved; changes appear from the next run. | yes |
| `ok` | Checked and compared with the last run. | yes |
| `empty` | The board exists but has no open jobs. | yes |
| `held` | The board suddenly returned no jobs. Closures wait for the next run to confirm (see below). | yes |
| `failed` | The board could not be read (for example HTTP 503 after retries). | no |
| `not_found` | No such board (HTTP 404). | no |
| `invalid_url` | The link could not be understood. The message says how to fix it. | no |
| `unsupported` | A job system we do not support yet, or a careers page without a supported board link. | no |
| `ambiguous` | A careers page links to several boards; add the one you want. | no |
| `duplicate` | The same board appears twice in your list. It is checked and charged once. | no |
| `not_checked` | Over the run limit or your maximum cost per run. | no |

### How monitoring works

- A **monitor** is your list of companies plus its history. History lives in a key-value store named `career-site-jobs-watch-history` **in your own Apify account**. Nothing is stored anywhere else.
- For each company, the monitor keeps the ids of currently open jobs (with title, location, department and link) and the last **52 snapshots** of what changed. Job descriptions are never stored.
- Jobs are identified by the job board's own id, never by title. Two roles with the same title are two jobs; a renamed job is still the same job.
- **The first check of a company is a baseline:** its jobs are reported as `open`, not `new`.
- **Schedule it** (daily or weekly) in Apify Schedules with the same monitor name to get a steady change feed.

#### Failure safety

- **One company failing never fails the run.** It gets `status: "failed"` and every other company is processed normally.
- **A failed fetch never marks jobs closed.** The previous snapshot is kept untouched, and the next successful run compares with it.
- **A board that returns zero jobs** after having jobs is `held`. Its jobs are marked closed only if the next run is empty too, because an empty answer can be a glitch.
- **A board larger than the per-company limit** (5,000 jobs) is read up to the limit. New jobs are reported but closures are not, because the list is incomplete.
- If some new or closed rows cannot be returned (because of your maximum cost per run), that company's history is **not advanced**, so those changes are reported again next run instead of being lost.

#### Duplicates

- Repeated feed entries and page overlaps are merged by job id.
- If one role is published as several postings (for example one per city), each posting is kept, because the job board treats them as separate postings with their own links. `source_group_id` (Greenhouse) and `requisition_id` show which postings belong together.

### API usage

One JSON request is all you need:

```bash
curl -X POST "https://api.apify.com/v2/acts/koalabed~career-site-jobs-watch/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies":[{"url":"https://jobs.lever.co/palantir"}],"monitorName":"weekly-watch","includeOpenJobs":false}'
```

Filter rows by `record_type`. For changes only, set `includeOpenJobs: false`: you then get company rows, new and closed jobs, and the summary.

### MCP / AI agents

The Actor works through the Apify MCP server, like any Apify Actor. Agents can call it with one JSON input (`companies` plus an optional `monitorName`) and read structured rows back. The summary row carries a plain-language `headline`, and each company row carries a `change_summary`, so an agent can answer "what changed at these companies?" without further processing.

### Pricing

Pay per event. You pay for:

- **each company whose job board was read** (company checked), and
- **each job row returned.**

Companies that could not be checked (failed, not found, invalid, unsupported, duplicate, over a limit) are free. Choose only new and closed jobs (`includeOpenJobs: false`) to keep scheduled watches cheap. The current prices are on the Pricing tab. Set **Maximum cost per run** in the run options to cap any run: new and closed jobs are returned first when a cap applies.

**Free Apify plan:** up to 5 companies and 100 job rows per run.

### Limits

- Up to 200 companies per run on paid Apify plans (5 on the free plan), and 25,000 job rows per run.
- Up to 5,000 jobs read per company.
- Only jobs the company has published on its public job board. Internal and unlisted postings are not visible.
- Job descriptions are optional and capped at 20,000 characters each.
- The HTML report lists up to 150 new and 150 closed jobs; the dataset has all of them.
- Workday, SmartRecruiters, Workable and others are not supported in this version.

### Privacy and sources

- The Actor reads only **public job-board feeds** that companies publish for their own careers pages: the Greenhouse Job Board API, the Lever Postings API and the Ashby public Job Posting API. It never logs in, uses no credentials or cookies, and does not bypass any protection.
- Requests are polite: a few at a time, spaced per host (Lever's requested 1-second crawl delay is honoured), with a small number of retries and backoff.
- Job postings contain no personal data beyond what the employer chose to publish. Every job links back to its original posting.
- Your monitor history stays in your own Apify account.

# Actor input Schema

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

Add company career pages from Greenhouse, Lever or Ashby. Paste the job board link (for example <code>https://job-boards.greenhouse.io/discord</code>, <code>https://jobs.lever.co/palantir</code>, <code>https://jobs.ashbyhq.com/linear</code>); a single job link works too. A company's own careers page also works when it links to one of these boards.

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

Use the same name on future runs to track what changed.

## `includeOpenJobs` (type: `boolean`):

Jobs that were already open last time. Turn off to get only what changed.

## `includeNewJobs` (type: `boolean`):

Jobs that appeared since the last run of this monitor.

## `includeClosedJobs` (type: `boolean`):

Jobs that were open last time and are gone now.

## `includeDescriptions` (type: `boolean`):

Add the full job description text to new and open jobs. Makes results much larger.

## Actor input object example

```json
{
  "companies": [
    {
      "url": "https://job-boards.greenhouse.io/discord",
      "name": "Discord"
    },
    {
      "url": "https://jobs.lever.co/palantir",
      "name": "Palantir"
    },
    {
      "url": "https://jobs.ashbyhq.com/linear",
      "name": "Linear"
    }
  ],
  "monitorName": "my-watchlist",
  "includeOpenJobs": true,
  "includeNewJobs": true,
  "includeClosedJobs": true,
  "includeDescriptions": false
}
```

# Actor output Schema

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

Company rows first, then job rows (new, closed, still open), then one summary row. Filter by record\_type.

## `report` (type: `string`):

Readable report: what changed, who is hiring the most, and which fetches failed.

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

The same summary as the dataset's final row.

## `runStatus` (type: `string`):

Charges, request counts, warnings and any input error 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": [
        {
            "url": "https://job-boards.greenhouse.io/discord",
            "name": "Discord"
        },
        {
            "url": "https://jobs.lever.co/palantir",
            "name": "Palantir"
        },
        {
            "url": "https://jobs.ashbyhq.com/linear",
            "name": "Linear"
        }
    ],
    "monitorName": "my-watchlist"
};

// Run the Actor and wait for it to finish
const run = await client.actor("koalabed/career-site-jobs-watch").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": [
        {
            "url": "https://job-boards.greenhouse.io/discord",
            "name": "Discord",
        },
        {
            "url": "https://jobs.lever.co/palantir",
            "name": "Palantir",
        },
        {
            "url": "https://jobs.ashbyhq.com/linear",
            "name": "Linear",
        },
    ],
    "monitorName": "my-watchlist",
}

# Run the Actor and wait for it to finish
run = client.actor("koalabed/career-site-jobs-watch").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": [
    {
      "url": "https://job-boards.greenhouse.io/discord",
      "name": "Discord"
    },
    {
      "url": "https://jobs.lever.co/palantir",
      "name": "Palantir"
    },
    {
      "url": "https://jobs.ashbyhq.com/linear",
      "name": "Linear"
    }
  ],
  "monitorName": "my-watchlist"
}' |
apify call koalabed/career-site-jobs-watch --silent --output-dataset

```

## MCP server setup

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

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/LRcm8P5VJrbaxHw5K/builds/wU2afANvZ8x3TEJzF/openapi.json
