# Job Listings Feed for Greenhouse, Lever & Ashby (`luxe_tasks/job-listings-feed`) Actor

Job listings from public Greenhouse, Lever and Ashby job boards you name, in one normalized feed: title, location, remote, salary where published, dates, apply link and a description snippet (max 300 characters). Public APIs only, no login. Price: $0.003/job listing + $0.005 start fee.

- **URL**: https://apify.com/luxe_tasks/job-listings-feed.md
- **Developed by:** [Luxe Tasks](https://apify.com/luxe_tasks) (community)
- **Categories:** Jobs, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 job listings

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

## Job Listings Feed

Developer: Luxe_tasks.

Get the open jobs of **public Greenhouse, Lever and Ashby job boards that you name** as one normalized feed: one row per job with the
**title, company, department, team, location(s), remote / hybrid / on-site, employment type, salary range where the employer publishes it,
posted and updated dates, job and apply links**, and a **description snippet of at most 300 characters**. The Actor reads only the
platforms' documented public job-board APIs (no login, no HTML pages).
It works for Greenhouse, Lever and Ashby job boards; it is not affiliated with or endorsed by Greenhouse, Lever or Ashby.

### How to use it

1. Open the Actor's input and list the job boards you want under **Job boards**, one per line, for example
   `https://boards.greenhouse.io/<board>`, `https://jobs.lever.co/<site>`, `https://jobs.ashbyhq.com/<board>`, or the short forms
   `greenhouse:<board>`, `lever:<site>`, `lever-eu:<site>` (Lever's EU host) and `ashby:<board>`. A link to one job on such a board also works.
2. Optionally narrow the jobs with **Title keywords**, **Locations**, **Remote jobs only** and **Posted within (days)**.
3. Run it. Jobs appear in the dataset (**Jobs** view), one row per job; the **BOARD_STATUS** record in the key-value store (**Board status** in the run's Output tab) lists every board with its status and counts.

You must name each board: the Actor does not search for, discover or list boards, and it requests nothing else apart from each API host's robots.txt.

### How it reads a board

- Greenhouse: `GET https://boards-api.greenhouse.io/v1/boards/<board>/jobs?content=true` (one request per board).
- Lever: `GET https://api.lever.co/v0/postings/<site>?mode=json&skip=<n>&limit=100` (EU boards: `api.eu.lever.co`), 100 jobs per page, at most 100 pages (10,000 jobs) per board.
- Ashby: `GET https://api.ashbyhq.com/posting-api/job-board/<board>?includeCompensation=true` (one request per board).
- Only these public GET endpoints are used: no application submissions, no customer APIs, no keys or logins.
- Jobs that the platform marks as not listed (Ashby `isListed: false`) and Greenhouse prospect posts (general applications) are skipped.
- A job listed twice in a run (on the same board or on another board you listed) is saved once.

### Input

- `boards` (required, up to 200): the job boards, as above. An entry that is not a supported Greenhouse, Lever or Ashby board stops the run before any request ("Invalid input"); only the start fee applies.
- `titleKeywords` (optional, up to 50): keep jobs whose title contains at least one of the words (case-insensitive).
- `locations` (optional, up to 50): keep jobs whose location text contains at least one of the texts (case-insensitive).
- `remoteOnly` (default off): keep only jobs marked fully remote.
- `postedWithinDays` (default 0 = any date, up to 365): keep jobs posted in the last N days; jobs without a posted date are left out when this is set.
- `maxJobsPerBoard` (default 1,000, 1-10,000): most jobs saved and charged per board.
- `includeDescriptionSnippet` (default on): add the snippet (at most 300 characters); off = `descriptionSnippet` is null.
- `timeoutSecs` (default 30, 5-60): how long to wait for one API answer.
- `maxConcurrency` (default 3, at most 3): boards read in parallel. Each platform still gets at most about 1 request per second.

Each run has a default timeout of 1 hour (3600 seconds); for very large lists you can raise the timeout in the run options.
If the run is about to reach its timeout, the Actor stops starting new requests and lists the remaining boards as `time_limit_reached`; no job-listing event is charged for them (the per-run start fee still applies).

### Output

- **Jobs** (dataset, `type: "job"`; the dataset holds job rows only, one per saved job): `platform, board, jobId, title, company, department, team, location, allLocations, country, workplaceType, remote, employmentType, salaryMin, salaryMax, salaryCurrency, salaryInterval, salarySummary, postedAt, updatedAt, jobUrl, applyUrl, descriptionSnippet, boardIndex, fetchedAt`. Fields the platform does not publish are null (for example, Greenhouse board lists carry no salary).
- **Board status** (key-value store record `BOARD_STATUS`, one JSON array): one entry per input board: `index, url, platform, board, status, jobsListed, jobsMatched, jobsSaved, duplicatesSkipped, jobsNotSavedBudget, charged, chargedEvents, httpStatus, error, key`.
- **Jobs per board** (key-value store, `jobs-<index>-<host>-<hash>-json`): the same job rows as JSON, one record per board that delivered jobs.
- `RUN_SUMMARY` (key-value store): counts by status, charged `job-listing` events since the last start or resume, budget reached, stopped platforms. Apify's billing is the official record.

Board statuses with no job-listing charge (the per-run start fee still applies): `no_open_jobs`, `no_matching_jobs`, `board_not_found`, `rate_limited`, `blocked`, `skipped_blocked`, `server_error`, `fetch_failed`, `invalid_response`, `unexpected_redirect`, `http_error`, `response_too_large`, `robots_disallowed`, `budget_reached`, `time_limit_reached`.

### Limits

- Only Greenhouse, Lever and Ashby public job boards. Other job sites, careers pages and applicant-tracking systems are not supported.
- Up to 200 boards per run; up to 10,000 jobs per board (Lever: 100 pages of 100); `maxJobsPerBoard` caps saved jobs. Lever paging stops once `maxJobsPerBoard` matching jobs are found (the board's counts then cover the pages read).
- One API answer may be at most 30 MB (Greenhouse board, Lever page) or 64 MB (Ashby board, which includes every job's full description: roughly 2,000 jobs). A larger answer gives the board the status `response_too_large`: no job is saved from it and it has no job-listing charge (the per-run start fee still applies).
- The description is a snippet only (at most 300 characters); the full description is not offered in this version.
- No change tracking between runs in this version: every run returns the jobs open at that time.
- If a run crashes or is restarted, a board is never charged twice, but the dataset and the charges can differ: a restarted run can deliver some job rows without their job-listing event, and if a run stops after a board was charged but before its rows were written and is not resumed, that board's jobs are only in its **Jobs per board (JSON)** record in the key-value store. The per-run start fee still applies.
- `remote` reflects each platform's own flag: it is true when the platform marks the job remote, or (Greenhouse, Lever without a workplace type) when the location says "remote". For Ashby jobs without a workplace type, `remote` is Ashby's own `isRemote` flag, which Ashby also sets for hybrid roles.

### Pricing

**Pricing: $0.003 per job listing ($3 per 1,000 job listings), plus the $0.005 start fee per run. Both are pay-per-event prices in Apify Console.**

- Pay-per-event **`job-listing`** ($0.003 per job listing): one event per job saved to the dataset. A job is charged once per run, after the board's jobs are saved.
- Boards that deliver no job (statuses above) have no job-listing charge; the per-run start fee still applies.
- A maximum total charge set on the run is respected: each job's event is reserved before it is saved, and when the budget is used up the remaining jobs are not saved (`jobsNotSavedBudget`) and later boards show `budget_reached`.
- **What you pay even when no board delivers a job:** the $0.005 start fee per run, charged by Apify through its synthetic `apify-actor-start` event ([Apify docs](https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event#synthetic-start-event)). This Actor's memory is fixed at 1 GB (1024 MB), so the start fee is charged once per run.
- There is no per-board event. The synthetic `apify-default-dataset-item` event is not used (the Actor refuses to run if it is priced).

### Politeness and safety

- robots.txt of each API host is honoured for every request; `Crawl-delay` is respected (Lever's API asks for 1 second).
- A robots.txt that returns 4xx (except 429) means no restrictions (Ashby's API host currently answers 401 for robots.txt); a 429 is treated as a rate limit and stops the domain; 5xx or an unreachable robots.txt means nothing is fetched.
- At most about 1 request per second per platform; fixed honest User-Agent `JobListingsFeedActor/0.1.0 (+https://apify.com/luxe_tasks/job-listings-feed; honours robots.txt)`; no proxies, no evasion.
- Server errors (5xx) and network errors are retried twice per request (after 2 and 4 seconds). A 401 or 403, a 429 (Retry-After over 60 s, or still 429 after one retry) or three boards failing with 5xx stop that platform for the rest of the run; its remaining boards show `skipped_blocked`.
- Only the three API hosts above are ever requested; private-network addresses are refused. No logins, cookies, headers or keys can be supplied.

### Acceptable use

> By running this Actor you confirm that you will use the job data lawfully and in line with the job boards' terms; that you will not use
> the Actor to get around rate limits or any other access restriction; and that you will not use it to profile or contact individuals.
> Job descriptions are the employers' own text: the Actor stores only a short snippet, and you are responsible for how you reuse it.

### Privacy

This Actor stores job facts only: it saves no recruiter, hiring-manager or other person fields from the platforms' data. The description snippet is at most 300 characters of plain text; sentences that mention a recruiter, a hiring manager or contact details are removed from it, and e-mail addresses, phone numbers and web links that the Actor detects are removed from it and from the other text fields (job titles and company names keep a bare domain such as "monday.com", which is often part of the company's name). Detection is pattern-based: names inside other sentences, and contact details written in unusual forms (for example "name at company dot com", a phone number without spaces or dashes, or a link without "http" or "www"), cannot always be detected. Beyond the snippet, the job description is not stored.

> "Results are written only to your run's default Apify storage. The developer does not copy, view or retain them. If your results contain
> personal data, you are the controller and the developer acts only as your processor under Apify's Standard Actor Contract. Apify's retention settings apply."

### Third-party software

Runtime dependencies (apify, robots-parser, tldts) are listed in NOTICE with their licences.

### Runtime

Node 24 (`engines`, `.nvmrc`, Dockerfile `apify/actor-node:24`). Memory: fixed at 1024 MB (the work is network-bound and rate-limited to about 1 request per second per platform). Tests: `npm test`; end-to-end: `sh scripts/e2e-netns.sh` (local mock APIs only).

Contact: https://apify.com/luxe_tasks · Issues tab on the actor page.

# Actor input Schema

## `boards` (type: `array`):

Public job boards to read, one per line (up to 200): a Greenhouse, Lever or Ashby board URL such as https://boards.greenhouse.io/<board>, https://jobs.lever.co/<site> or https://jobs.ashbyhq.com/<board>, or the short form greenhouse:<board>, lever:<site>, lever-eu:<site>, ashby:<board>.

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

Keep only jobs whose title contains at least one of these words (case-insensitive). Empty = all jobs.

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

Keep only jobs whose location contains at least one of these texts, e.g. "London" or "Remote" (case-insensitive). Empty = all locations.

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

Keep only jobs marked fully remote (the platform says remote, or the location says Remote).

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

Keep only jobs posted in the last N days. 0 = any date.

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

At most this many matching jobs are saved (and charged) per board.

## `includeDescriptionSnippet` (type: `boolean`):

A plain-text snippet of the job description, at most 300 characters (contact sentences and detected e-mail addresses, phone numbers and links removed). The rest of the description is not stored.

## `timeoutSecs` (type: `integer`):

How long to wait for one API answer.

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

Items processed in parallel (each site still gets at most 1 request per second).

## Actor input object example

```json
{
  "boards": [
    "https://boards.greenhouse.io/greenhouse",
    "https://jobs.lever.co/leverdemo",
    "https://jobs.ashbyhq.com/Ashby"
  ],
  "titleKeywords": [],
  "locations": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "maxJobsPerBoard": 1000,
  "includeDescriptionSnippet": true,
  "timeoutSecs": 30,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

Default dataset, 'Jobs' view: one row per saved job: platform, board, title, company, department, team, location, allLocations, country, workplaceType, remote, employmentType, salaryMin, salaryMax, salaryCurrency, salaryInterval, salarySummary, postedAt, updatedAt, jobUrl, applyUrl, descriptionSnippet, jobId, boardIndex, fetchedAt. The dataset holds job rows only.

## `boards` (type: `string`):

BOARD_STATUS record (default key-value store): one JSON array with one entry per input board: index, url, platform, board, status, jobsListed, jobsMatched, jobsSaved, duplicatesSkipped, jobsNotSavedBudget, charged, chargedEvents, httpStatus, error, key.

## `boardFiles` (type: `string`):

Default key-value store, records starting with 'jobs-': one JSON record per board that delivered jobs ({platform, board, boardIndex, fetchedAt, jobCount, jobs}), the same job rows as in the dataset.

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

RUN_SUMMARY record: totals by status, charged job-listing events since the last start or resume, budget reached and stopped platforms.

# 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 = {
    "boards": [
        "https://boards.greenhouse.io/greenhouse",
        "https://jobs.lever.co/leverdemo",
        "https://jobs.ashbyhq.com/Ashby"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("luxe_tasks/job-listings-feed").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 = { "boards": [
        "https://boards.greenhouse.io/greenhouse",
        "https://jobs.lever.co/leverdemo",
        "https://jobs.ashbyhq.com/Ashby",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("luxe_tasks/job-listings-feed").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 '{
  "boards": [
    "https://boards.greenhouse.io/greenhouse",
    "https://jobs.lever.co/leverdemo",
    "https://jobs.ashbyhq.com/Ashby"
  ]
}' |
apify call luxe_tasks/job-listings-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,luxe_tasks/job-listings-feed"
        }
    }
}
```

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/dTz0pHUhH6omtvMP0/builds/yBYe0LNlDXh1og6De/openapi.json
