# Greenhouse, Lever & Ashby Jobs Scraper (Only New) (`far003/company-jobs-feed`) Actor

Job scraper for public ATS career boards (Greenhouse, Lever, Ashby): one clean schema, optional feed mode returning only postings new since the last run.

- **URL**: https://apify.com/far003/company-jobs-feed.md
- **Developed by:** [Francesco Antonio Russo](https://apify.com/far003) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 job postings

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

## Company Jobs Feed — Greenhouse, Lever, Ashby, Workable & Teamtailor jobs scraper, new postings only

A job scraper and jobs API for public ATS career boards: get the open positions of any list of companies from **Greenhouse**, **Lever**, **Ashby**, **Workable** and **Teamtailor** in one clean schema — and, if you want, **only the postings that appeared since your last run**. It reads the vendors' documented public endpoints, not the HTML, so it does not break when a careers page is redesigned.

You give a list of companies. You get one record per job posting: title, department, locations, workplace type, pay range when the company publishes it, publication date, links, and the full description as clean text.

Supported boards: **Greenhouse**, **Lever**, **Ashby**, **Workable**, **Teamtailor**. Give a company's website or careers page and the Actor finds which of them it uses.

### Why use it

- **A feed, not a dump.** Turn on *Only new postings*, schedule the Actor, and every run returns just what is new. No diffing on your side, and you pay only for what is delivered.
- **Give a website, not a board.** Paste `careers.stripe.com` or `https://www.airbnb.com/careers`: the Actor looks for the company's board on the five supported systems and tells you which one it found.
- **One schema for every board.** The same fields in the same order whatever the source, so the output goes directly into a sheet, a database, a CRM or an LLM prompt.
- **Filter before you pay.** Title, location, remote and recency filters are applied before delivery. Postings that do not match are never charged.
- **Built on documented public endpoints.** No browser, no proxy, no login, no anti-bot workarounds. Runs are fast and cheap, and they do not break when a careers page is redesigned.
- **Job postings only.** The Actor adds no personal data: no recruiter names, no candidate data. Descriptions are delivered as the company published them.

### Typical uses

- **Hiring signals for sales:** watch target accounts and get notified when they open roles that mean budget ("Head of Data", "Salesforce Admin", "Security Engineer").
- **Job boards and newsletters:** keep a niche board fresh from a curated list of companies.
- **Recruiting and talent intelligence:** track where competitors are hiring, in which teams and locations.
- **AI agents:** give a job-search or research agent a reliable, structured source of open roles.

### Input

| Field | What it does |
|---|---|
| `companies` (required) | One entry per company: its website or careers page, the URL of its public job board, or `source:company`. |
| `onlyNew` | Return only postings that appeared since the previous run with the same feed name. |
| `baselineOnFirstRun` | With `onlyNew`: the first run records every posting that is listed and delivers nothing. |
| `stateKey` | Feed name. Use different names for independent feeds. Default `default`. |
| `titleKeywords` / `excludeTitleKeywords` | Keep / drop postings by words in the title (case-insensitive). A keyword is a word or a phrase, not a piece of text: it matches from the start of a word to the end of it, and also the usual endings of that word (`s`, `es`, `ing`, `er`, `ers`, `ed`, `ship`, `ships`). So `engineer` finds "Engineer", "Engineers" and "Engineering Manager"; `intern` finds "Intern" and "Internship" but not "Internal", "Internet" or "International"; `design` finds "Designer". A word of up to 3 letters (`AI`, `ML`, `PM`) must be the whole word, so `AI` does not find "AirCover" or "Spain". To match any ending, finish the keyword with `*` (`data*` finds "Database"). Accents are not folded: `ingénieur` does not find "Ingenieur". |
| `locationKeywords` | Keep postings whose locations contain any of these words. The country the board gives counts too (Lever, Ashby, Workable, Teamtailor): "United States", "Japan" find jobs labelled "San Francisco, California" or "London". A country is found under every way it is written: "US", "USA", "U.S." and "United States" are the same search (also in a label like "Remote - USA"), and so are "UK", "Great Britain" and "United Kingdom". A short word of up to 3 letters ("US", "NY") must be a whole word, so "US" does not find Sydney, Brussels or Columbus. Greenhouse gives no country: there only the location text is searched. |
| `remoteOnly` | Keep only postings the company marks as remote (its workplace-type field, or a location label saying Remote). |
| `postedWithinDays` | Keep only postings published in the last N days. |
| `includeDescription` | Add the description as plain text. Default on. |
| `includeDescriptionHtml` | Also add the original description HTML. Default off. |
| `maxJobsPerCompany` / `maxJobsTotal` | Caps on the number of postings delivered. Postings are ordered newest first, so a cap keeps the most recently published ones. |

**Quick start:** press *Start* with the prefilled companies and you get the 10 newest postings of each (30 in total, a few cents) in seconds; then replace them with your own list and remove the cap.

#### How to write a company

Use the board URL you see in the browser, or the short form:

| Board | URL form | Short form |
|---|---|---|
| Greenhouse | `https://boards.greenhouse.io/airbnb` or `https://job-boards.greenhouse.io/airbnb` | `greenhouse:airbnb` |
| Lever | `https://jobs.lever.co/spotify` | `lever:spotify` |
| Ashby | `https://jobs.ashbyhq.com/notion` | `ashby:notion` |
| Workable | `https://apply.workable.com/huggingface/` | `workable:huggingface` |
| Teamtailor | `https://career.teamtailor.com/jobs` (any `<name>.teamtailor.com` career site) | `teamtailor:career` |

#### Giving a company's website instead

Not sure which system a company uses? Enter its website or careers page (`stripe.com`, `careers.stripe.com`, `https://www.airbnb.com/careers`). The Actor takes the company name from the domain and asks each of the five supported systems' public endpoints whether a board with that name exists and has open postings (one polite request per system). It never requests the company's own website.

- If exactly one system has such a board, the Actor uses it. The `SUMMARY` record shows which one (`source`, `company`, `detectedFrom`).
- If the name exists on more than one system, or on none, the entry is reported as `not_found` with an explanation, and nothing is guessed. Use `source:company` or the board URL for those.
- A name can belong to an unrelated company. Every record says how its board was found in `boardMatch`: `given` (you named the board), `confirmed` (found from the website, and the postings link back to that website's domain — Greenhouse boards usually do), `name` (found from the website only because a board has that name — for example `atlas.com` matches an unrelated Ashby board called "atlas"). Filter on `boardMatch` or use the explicit form when you need to be certain; a `name` match also shows a warning in the log.
- Websites of systems this Actor does not read (for example SmartRecruiters, Recruitee, Workday, BambooHR, Personio) are reported as ignored entries.

#### Example: daily feed of new engineering roles

```json
{
    "companies": ["greenhouse:airbnb", "lever:spotify", "notion.com"],
    "onlyNew": true,
    "baselineOnFirstRun": true,
    "titleKeywords": ["engineer", "developer"],
    "excludeTitleKeywords": ["intern"]
}
```

Schedule it daily. The first run records what is already open; from the second run on, you receive only the roles that appeared since the previous run, however you change the filters in between.

### Output

One record per posting. Example (description shortened):

```json
{
    "id": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "source": "ashby",
    "company": "ramp",
    "companyName": "ramp",
    "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "title": "Security Engineer, Cloud",
    "department": "Engineering",
    "team": "Backend",
    "location": "New York, NY (HQ)",
    "locations": [
        "New York, NY (HQ)",
        "Remote (Canada)",
        "Remote (US)",
        "Miami, FL"
    ],
    "countries": [
        "United States",
        "Canada"
    ],
    "workplaceType": "hybrid",
    "employmentType": "Full-time",
    "compensation": {
        "text": "$211.4K – $290.6K • Offers Equity",
        "min": 211400,
        "max": 290600,
        "currency": "USD",
        "interval": "year"
    },
    "publishedAt": "2026-04-07T17:12:35.753Z",
    "updatedAt": null,
    "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
    "language": null,
    "descriptionText": "About Ramp\n\nRamp is building the smart infrastructure for finance teams, embedded in the t…",
    "boardMatch": "given",
    "scrapedAt": "2026-10-06T09:27:43.435Z"
}
```

| Field | Notes |
|---|---|
| `id` | Unique key: `source:company:jobId`. `company` is written as you entered it (or as detected), so keep the spelling consistent; Lever and Workable identifiers are always lower case. |
| `companyName` | Display name when the board provides one (Greenhouse, Workable, Teamtailor); otherwise the company identifier. |
| `boardMatch` | `given`, `confirmed` or `name`: how the board was found (see Input) |
| `locations` | Every location listed for the posting; `location` is the first. |
| `countries` | Countries as the board gives them, in English, one name per country ("USA", "US" and "United States" all become "United States"; 2-letter codes become names). Empty on Greenhouse, which has no country field |
| `workplaceType` | `remote`, `hybrid`, `onsite`, or `null` when the company does not state it. Greenhouse: the company's own *Workplace Type* / *Location Type* field when its board has one (many do), otherwise `remote` or `hybrid` only when the location label says so. Lever and Ashby: their workplace-type field. Workable: `remote` when the posting is flagged as telecommuting. Teamtailor: its remote status (`none` is `onsite`). |
| `compensation` | Present only when the company publishes a pay range. `min`/`max` are in currency units; `interval` is `year`, `month`, `week`, `day`, `hour`, or `null` when the board does not state the pay period. |
| `publishedAt`, `updatedAt` | ISO 8601, UTC. `updatedAt` is available on Greenhouse only. On Lever, `publishedAt` is the date the posting was created. |
| `employmentType` | As the board publishes it; always `null` on Greenhouse and Teamtailor. |
| `descriptionText` | Full description as plain text, with paragraphs and bullet lists preserved. |

A `SUMMARY` record in the run's key-value store lists, per company: status (`ok`, `baseline`, `partial`, `not_found`, `error`, `skipped`), postings listed, postings delivered, any input entries that were ignored (`invalidEntries`), and entries left out because they name a company already in your list (`duplicateEntries`: `careers.stripe.com` next to `stripe.com`, or `Lever:Spotify` next to `lever:spotify`).

### How "Only new postings" works

A posting is **new** if it was not on the company's board when your feed started (its first run for that company) and has not been delivered to you since. In practice:

- At the first run for a company, every posting open at that moment is recorded. With *Skip existing postings on the first run* it is only recorded; without it, the postings matching your filters are delivered (and charged) once.
- **Your filters never turn an old posting into a new one.** A posting that was open at the first run but did not match your filters then is recorded too: widening the filters later does not deliver it. To get the backlog under new filters, use a new feed name (a new feed has its own first run).
- A posting stays remembered for as long as the company keeps it open, however long that is. It is never re-delivered as "new".
- If a posting is closed and re-opened more than 30 days later, it counts as new.
- Postings that match your filters but were held back by a limit (`maxJobsPerCompany`, `maxJobsTotal`, your spending limit) are not marked as delivered: the next run returns them.
- If a company's board cannot be read during a run, its memory is left untouched.
- Delivery is at-least-once: if a run is interrupted between delivering postings and saving the memory, the next run delivers those postings again. A posting is never lost.
- Do not run two runs with the same feed name at the same time: both would deliver the same new postings. Give overlapping schedules different feed names.

The memory lives in a named key-value store, `company-jobs-feed-state`, in your account. Delete a record there to reset a feed.

### Pricing

Pay per event: you are charged for each **job posting delivered** to the dataset, at the price shown on the Pricing tab. There is no start fee. Companies that are not found, postings removed by your filters, and baseline runs cost nothing. Example: a run that delivers 250 postings costs $0.45 at $1.80 per 1,000; a scheduled feed that finds 12 new postings costs about $0.02. The Actor respects the maximum charge you set for a run and stops cleanly when it is reached.

### API & integrations

Everything the Console does is available through the [Apify API](https://docs.apify.com/api/v2) and the official clients, so the feed drops into any pipeline.

**Run it and read the results with the API** (replace `<token>` with your Apify API token):

```bash
curl -X POST "https://api.apify.com/v2/actors/far003~company-jobs-feed/run-sync-get-dataset-items?token=<token>" \
  -H "Content-Type: application/json" \
  -d '{"companies": ["greenhouse:cloudflare", "lever:palantir"], "onlyNew": true, "stateKey": "my-feed"}'
```

**From Node.js** with [`apify-client`](https://docs.apify.com/api/client/js/):

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<token>' });
const run = await client.actor('far003/company-jobs-feed').call({
    companies: ['greenhouse:cloudflare', 'lever:palantir'],
    onlyNew: true,
    stateKey: 'my-feed',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**From Python** with [`apify-client`](https://docs.apify.com/api/client/python/):

```python
from apify_client import ApifyClient

client = ApifyClient("<token>")
run = client.actor("far003/company-jobs-feed").call(run_input={
    "companies": ["greenhouse:cloudflare", "lever:palantir"],
    "onlyNew": True,
    "stateKey": "my-feed",
})
items = client.dataset(run.default_dataset_id).list_items().items
```

**Schedules and integrations.** Create a Schedule in the Console (for example daily) with `onlyNew` on, then attach an integration to the Actor: a webhook on `ACTOR.RUN.SUCCEEDED`, or one of the built-in connectors (Slack, Google Drive, Airtable, Make, Zapier, n8n…). Each run's dataset then contains only the postings that appeared since the previous run, so the integration receives nothing when there is nothing new. The dataset can be exported as JSON, JSONL, CSV, Excel, XML, HTML table or RSS from the run page or through the API.

### Limits

- Five systems are supported today. Companies using other systems are reported as ignored entries, not silently skipped.
- EU-hosted boards (`jobs.eu.lever.co`, `job-boards.eu.greenhouse.io`) are not supported yet and are reported as ignored entries.
- The Actor reads what the company publishes: if a field is empty at the source, it is `null` here.
- Teamtailor career sites on a custom domain are not recognised (only `<name>.teamtailor.com`); neither are Workable boards other than `apply.workable.com/<name>`.
- A website entry costs up to five lightweight requests (one per supported system) before its postings are read.
- To stay polite to the boards, requests to the same host are spaced one second apart. A list of 500 companies on the same board takes roughly 10 minutes.
- `companyName` on Lever and Ashby is the board identifier, because those boards do not expose a display name.

### FAQ

**A company returns `not_found`.** The identifier is wrong or the company moved to another system. Open its careers page and copy the board URL from the address bar.

**Can I track thousands of companies?** Yes. Memory use is low and cost scales with postings delivered, not with companies checked.

**Is this allowed?** The Actor uses only endpoints and feeds these vendors document as public for reading published job postings (Greenhouse Job Board API, Lever Postings API, Ashby job posting API, Workable's public account endpoint, Teamtailor's career site RSS feed), without authentication, and returns only information the companies have made public to attract applicants. It adds no personal data to what the company published. You remain responsible for how you use the data, including each board's terms and the laws that apply to you.

**Why are my capped results not random?** Within each company, postings are ordered newest first (undated last), so `maxJobsPerCompany` and `maxJobsTotal` keep the most recent ones. With several companies, `maxJobsTotal` fills in the order of your list.

**Which systems are supported, and why not others?** Greenhouse, Lever, Ashby, Workable and Teamtailor, because each vendor documents a public way to read published postings without a key. A system is added only when its vendor documents public, keyless access.

**Something is wrong or a board is missing.** Open an issue on the Issues tab.

***

Greenhouse, Lever, Ashby, Workable and Teamtailor are trademarks of their respective owners. This Actor is not affiliated with or endorsed by them.

# Actor input Schema

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

One entry per company: the company's website or careers page (e.g. careers.stripe.com), the URL of its public job board (e.g. https://jobs.lever.co/spotify), or source:company (e.g. greenhouse:airbnb). Supported sources: greenhouse, lever, ashby, workable, teamtailor. For a website the Actor finds the board by trying the company name on each supported ATS.

## `onlyNew` (type: `boolean`):

Return only postings that appeared since the last run with the same Feed name. Schedule the Actor and each run gives you just what is new. You are charged only for postings delivered. Note: the first run delivers (and charges) every open posting that matches your filters, unless you also turn on "Skip existing postings on the first run".

## `baselineOnFirstRun` (type: `boolean`):

With "Only new postings" on: the first run for a company only records every posting currently listed and delivers nothing (and charges nothing). Later runs deliver only what appeared after that, whatever filters you use then.

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

Name of the memory used by "Only new postings". Use different names for independent feeds (for example one per filter set): a new name starts a new feed with its own first run. Letters, digits, \_ and - only.

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

Keep a posting only if its title contains at least one of these words or phrases (case-insensitive). A keyword matches whole words, with their usual endings: "engineer" finds Engineer, Engineers and Engineering, "intern" finds Intern and Internship but not Internal. A word of up to 3 letters (AI, ML) must be the whole word. End a keyword with \* to match any ending ("data\*" finds Database). Empty = no title filter.

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

Drop a posting if its title contains any of these words or phrases (case-insensitive). Same matching as above: "intern" drops Intern and Internship, not Internal or International.

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

Keep a posting only if one of its locations contains at least one of these words (case-insensitive), e.g. Berlin, Germany, EMEA. A country counts under every spelling (US = USA = United States, UK = United Kingdom), and a word of up to 3 letters must be a whole word.

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

Keep only postings marked as remote by the company (Greenhouse "Workplace Type" field, Lever/Ashby workplace type, Workable telecommuting flag, Teamtailor remote status, or a location label saying Remote). Postings with no workplace information are dropped.

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

Keep only postings published in the last N days. Postings without a publication date are dropped when this is set.

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

Add the full job description as clean plain text (descriptionText).

## `includeDescriptionHtml` (type: `boolean`):

Also add the original description HTML (descriptionHtml).

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

Deliver at most this many postings for each company: the most recently published ones. Empty = no limit.

## `maxJobsTotal` (type: `integer`):

Stop after delivering this many postings across all companies (each company's newest first, companies in the order of your list). Empty = no limit.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:airbnb",
    "https://jobs.lever.co/spotify",
    "ashby:notion"
  ],
  "onlyNew": false,
  "baselineOnFirstRun": false,
  "stateKey": "default",
  "remoteOnly": false,
  "includeDescription": true,
  "includeDescriptionHtml": false,
  "maxJobsPerCompany": 10
}
```

# Actor output Schema

## `postings` (type: `string`):

No description

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

No description

# 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": [
        "greenhouse:airbnb",
        "https://jobs.lever.co/spotify",
        "ashby:notion"
    ],
    "maxJobsPerCompany": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("far003/company-jobs-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 = {
    "companies": [
        "greenhouse:airbnb",
        "https://jobs.lever.co/spotify",
        "ashby:notion",
    ],
    "maxJobsPerCompany": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("far003/company-jobs-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 '{
  "companies": [
    "greenhouse:airbnb",
    "https://jobs.lever.co/spotify",
    "ashby:notion"
  ],
  "maxJobsPerCompany": 10
}' |
apify call far003/company-jobs-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,far003/company-jobs-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/Fqzgicvum6w7bGezu/builds/6ET2Hs58uQNjE3u36/openapi.json
