# Job Board Scraper — Greenhouse, Lever & Ashby (`funny_ground/public-job-board-aggregator`) Actor

Collect and normalize public jobs from Greenhouse, Lever, and Ashby. Filter by keyword, location, remote status, or date and track changes across runs. No browser, login, API key, or residential proxy. Just $0.001 per job ($1 per 1,000).

- **URL**: https://apify.com/funny\_ground/public-job-board-aggregator.md
- **Developed by:** [Coor Yu](https://apify.com/funny_ground) (community)
- **Categories:** Jobs
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Job Board Scraper — Greenhouse, Lever & Ashby

Monitor public company job boards without building and maintaining three separate integrations. Add Greenhouse, Lever, or Ashby board URLs and receive one normalized dataset ready for alerts, analytics, search, or a careers product.

### Why users choose this Actor

- Supports Greenhouse, Lever, and Ashby in the same run.
- Filters by keyword, location, remote status, and publication date.
- Detects new, updated, unchanged, and removed jobs between runs.
- Uses public JSON endpoints—no browser, login, API key, CAPTCHA solver, or residential proxy.

### Budget-friendly pay-per-result pricing

- **Result fee:** `$0.001` per job — **$1 per 1,000 output jobs**.
- A small Actor start event and normal Apify platform usage may also apply; see the **Pricing** tab.
- Rows filtered out before they reach the dataset do not incur the per-result fee.

Add one or more supported job boards in **Input**, choose your filters, and click **Start**.

### Why this Actor

- No accounts, cookies, CAPTCHA solvers, API keys, browsers, or residential proxies.
- Uses documented public JSON endpoints instead of fragile page selectors.
- Fetches boards concurrently and normally needs only one or two compressed HTTP requests per board.
- Normalizes different sources into a stable job schema.
- Supports keyword, location, remote, and publication-date filters.
- Detects new, updated, unchanged, and removed jobs between runs.
- Keeps healthy source results when another board temporarily fails.

### Supported sources

| Provider | Accepted URL examples | Data source |
|---|---|---|
| Greenhouse | `https://boards.greenhouse.io/stripe`<br>`https://job-boards.greenhouse.io/stripe` | Public Greenhouse Job Board API |
| Lever | `https://jobs.lever.co/company`<br>`https://jobs.eu.lever.co/company` | Public Lever Postings API |
| Ashby | `https://jobs.ashbyhq.com/company` | Public Ashby Job Postings API |

Only the listed provider domains are accepted. This prevents accidental requests to private networks or arbitrary URLs.

### Input

Minimal example:

```json
{
  "jobBoards": [
    { "url": "https://boards.greenhouse.io/stripe", "company": "Stripe" },
    { "url": "https://jobs.eu.lever.co/lever", "company": "Lever" },
    { "url": "https://jobs.ashbyhq.com/ashby", "company": "Ashby" }
  ]
}
```

Filtered monitoring example:

```json
{
  "jobBoards": [
    { "url": "https://boards.greenhouse.io/stripe" }
  ],
  "includeKeywords": ["software", "machine learning", "data"],
  "excludeKeywords": ["manager", "director"],
  "locations": ["Singapore", "Remote"],
  "changeMode": "new-and-updated",
  "stateKey": "apac-engineering"
}
```

#### Main options

| Field | Default | Description |
|---|---:|---|
| `jobBoards` | required | Up to 100 public Greenhouse, Lever, or Ashby board URLs. |
| `includeDescriptions` | `true` | Include clean text and source HTML descriptions. |
| `includeCompensation` | `true` | Include source compensation or infer an explicit salary range. |
| `maxJobsPerBoard` | `0` | Maximum matching jobs per board; `0` is unlimited. |
| `includeKeywords` | `[]` | Keep a job if any keyword matches. |
| `excludeKeywords` | `[]` | Remove a job if any keyword matches. |
| `locations` | `[]` | Case-insensitive partial location matching. |
| `remoteOnly` | `false` | Keep only explicitly remote jobs. |
| `publishedAfter` | unset | ISO date in `YYYY-MM-DD` form. Undated jobs are retained. |
| `changeMode` | `all` | `all`, `new-and-updated`, or `all-changes`. |
| `maxConcurrency` | `15` | Concurrent boards, from 1 to 50. |
| `requestTimeoutSecs` | `15` | Per-request timeout; fails fast instead of holding idle compute. |
| `maxRetries` | `2` | Retries for timeouts, 429s, and 5xx responses. |

### Output

Every dataset item follows one normalized schema, including:

- stable `id` and provider `externalId`
- company, title, location, department, team, and employment type
- remote and workplace type indicators
- published and updated timestamps when exposed by the source
- clean description text and original description HTML
- job and application URLs
- structured salary when published or safely recognized
- source provider and board URL
- `changeStatus`: `new`, `updated`, `unchanged`, or `removed`
- content fingerprint and scrape timestamp

The `OUTPUT` key-value store record contains counts, source timings, failures, and explicit `proxyUsed: false` and `residentialProxyUsed: false` fields.

### Incremental monitoring

The Actor stores a source-and-filter-scoped snapshot in a named key-value store. Use the same board list, filters, and `stateKey` for comparable runs:

- `all`: output the complete current snapshot.
- `new-and-updated`: suppress unchanged jobs.
- `all-changes`: output new, updated, and removed jobs.

A failed board is never treated as if all of its jobs were removed. Its previous snapshot is preserved until the source succeeds again.

### Performance and proxy policy

This Actor deliberately does not create or accept an Apify proxy configuration. It uses direct HTTPS requests to public, documented endpoints. Performance comes from:

- concurrent board fetching;
- JSON APIs rather than browser rendering;
- HTTP compression and connection reuse provided by Node.js `fetch`;
- bounded request timeouts;
- exponential retry with jitter for temporary failures;
- one batched dataset write per 500 output records.

The default Actor allocation is 256 MB. Since no browser is installed or started, small runs avoid paying for unused browser memory.

If a provider later stops making its public endpoint available without a residential proxy, support for that provider should be paused or explicitly redesigned. A residential proxy must never be silently enabled.

### Local development

```bash
npm install
npm test
npm run build
```

Create `storage/key_value_stores/default/INPUT.json`, then run:

```bash
npm start
```

### Responsible use

The Actor only reads published job listings and does not submit applications or access candidate data. Users remain responsible for complying with applicable website terms, privacy rules, and laws.

### Zero-result policy

A run that produces no output job rows finishes as `FAILED`. Broaden the filters or use `changeMode: "all"`; in incremental modes, no new or changed rows is intentionally reported as a failed zero-result run.

# Actor input Schema

## `jobBoards` (type: `array`):

Public company job board URLs. Supported examples: boards.greenhouse.io/company, jobs.lever.co/company, jobs.eu.lever.co/company, jobs.ashbyhq.com/company.

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

Return both cleaned plain text and source HTML descriptions.

## `includeCompensation` (type: `boolean`):

Return structured compensation when the source publishes it, otherwise try to recognize a salary range from the description.

## `maxJobsPerBoard` (type: `integer`):

0 means no limit. The limit is applied after filtering.

## `includeKeywords` (type: `array`):

Keep a job when at least one keyword appears in its title, description, department, or team. Leave empty to include all jobs.

## `excludeKeywords` (type: `array`):

Remove a job when any keyword appears in its title, description, department, or team.

## `locations` (type: `array`):

Keep jobs whose location contains at least one supplied value. Matching is case-insensitive.

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

Keep only jobs explicitly marked remote by the source or location text.

## `publishedAfter` (type: `string`):

Optional ISO date such as 2026-01-01. Jobs with no published/updated date are retained.

## `changeMode` (type: `string`):

All jobs returns the current snapshot. New and updated suppresses unchanged jobs. All changes also emits removed jobs from successful boards.

## `stateKey` (type: `string`):

Runs with the same board list and state key compare against the same saved snapshot.

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

Each board normally needs one or two lightweight API requests.

## `requestTimeoutSecs` (type: `integer`):

Abort an individual public API request after this many seconds.

## `maxRetries` (type: `integer`):

Retries temporary timeouts, rate limits, and server errors with exponential backoff.

## `failOnBoardError` (type: `boolean`):

Disabled by default so healthy boards still return results. Failures are always included in the run summary.

## Actor input object example

```json
{
  "jobBoards": [
    {
      "url": "https://boards.greenhouse.io/stripe",
      "company": "Stripe"
    },
    {
      "url": "https://jobs.eu.lever.co/lever",
      "company": "Lever"
    },
    {
      "url": "https://jobs.ashbyhq.com/ashby",
      "company": "Ashby"
    }
  ],
  "includeDescriptions": true,
  "includeCompensation": true,
  "maxJobsPerBoard": 0,
  "includeKeywords": [],
  "excludeKeywords": [],
  "locations": [],
  "remoteOnly": false,
  "changeMode": "all",
  "stateKey": "default",
  "maxConcurrency": 15,
  "requestTimeoutSecs": 15,
  "maxRetries": 2,
  "failOnBoardError": false
}
```

# Actor output Schema

## `jobs` (type: `string`):

Normalized job records in the default dataset.

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

Counts, timing, source status, and confirmation that no proxy was used.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("funny_ground/public-job-board-aggregator").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("funny_ground/public-job-board-aggregator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call funny_ground/public-job-board-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=funny_ground/public-job-board-aggregator",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/jsChKlIzcDq4Jlih6/builds/URpcziadDVKhFMwhw/openapi.json
