# Ashby Jobs Scraper - Pay Ranges, True Remote Flag, No API Key (`maydit/ashby-jobs-scraper`) Actor

Scrape live job postings from any Ashby job board. Numeric salary ranges where the employer publishes them, a remote flag that excludes hybrid roles, company presets and a new-postings-only mode.

- **URL**: https://apify.com/maydit/ashby-jobs-scraper.md
- **Developed by:** [Brandt May](https://apify.com/maydit) (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 $0.60 / 1,000 results

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

## Ashby Jobs Scraper

Scrape live job postings from any **Ashby job board** and get them back as flat CSV, JSON or Excel rows. Give it a board slug (`openai`) or a board URL (`https://jobs.ashbyhq.com/openai`) and this Ashby scraper returns every listed opening on that board: job title, department, team, employment type, primary and secondary locations, the posting and apply links - and two things most job scrapers leave out:

- **Numeric salary ranges.** Where the employer publishes pay on the posting, you get `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryInterval` as separate columns (for example `190000`, `330000`, `USD`, `1 YEAR`), plus an `offersEquity` flag and Ashby's own summary string. Not a `"$190K - $330K"` string you have to parse yourself.
- **A remote flag that is not fooled by hybrid roles.** Ashby's own `isRemote` field is `true` on hybrid postings too (on OpenAI's board it is set on 540 of 832 postings, while only 24 have the workplace type `Remote`). This Actor's `remote` column is `true` only when the workplace type is `Remote` or the posting lists a remote location such as `Remote (US)`, and it gives you the `workplaceType` (`Remote`, `Hybrid`, `OnSite`) alongside it.

It reads the endpoint Ashby documents as its Job Postings API (developers.ashbyhq.com/docs/public-job-posting-api) and calls it **with no API key, no login and no Ashby account**. Pass one company or fifty, or pick a preset list. Built for recruiters, sourcers, compensation analysts, job-board operators and anyone tracking hiring at a list of companies.

This Actor covers **Ashby only**. A company that is not on Ashby cannot be scraped here; see the FAQ for what to use instead.

### What you get

One row per listed job posting.

| Field | What it is |
| --- | --- |
| `company` | Display name for the boards in the presets (`OpenAI`, `Ramp`, ...). Ashby's response carries no company name, so for any other slug this is the slug itself. |
| `slug` | The Ashby board the row came from, so multi-company runs stay separable. |
| `jobId` | Ashby posting ID (a UUID). Stable - use it to dedupe across runs. |
| `title` | Job title. |
| `department` | Department the employer filed the role under. |
| `team` | Team within the department. |
| `employmentType` | As published: `FullTime`, `PartTime`, `Contract`, `Intern`, `Temporary`. |
| `location` | The posting's primary location as the employer wrote it (`New York, NY (HQ)`, `US - Remote`, `Europe`). |
| `secondaryLocations` | Array of the posting's additional locations (`["San Francisco, CA", "Remote (US)"]`). Empty array when there are none - filled on 38% of rows across the three sample runs below. |
| `remote` | `true` only when `workplaceType` is `Remote` or one of the listed locations names a remote option (text containing `remote` or `distributed`). Hybrid and on-site roles with no remote location are `false`. Ashby's `isRemote` is deliberately not used - see the limits section. |
| `workplaceType` | `Remote`, `Hybrid` or `OnSite` exactly as Ashby publishes it, or empty when the employer left it unset (about 14% of rows). One value per posting, so a `Hybrid` posting that also lists `Remote (US)` has `remote` `true`. |
| `salaryMin` / `salaryMax` | Numeric pay range from the posting's published compensation. Uses Ashby's own roll-up when a posting publishes several ranges (by location or level), so it matches the range Ashby shows on the job page. Empty when the employer publishes no pay. |
| `salaryCurrency` | ISO code of the range: `USD`, `CAD`, `GBP`, `EUR`, `SGD`, `AUD`, `SEK` seen so far. |
| `salaryInterval` | `1 YEAR` for almost every range; `1 HOUR` (hourly contract roles) and `1 MONTH` also occur. Passed through as Ashby publishes it. |
| `salaryTiers` | How many separate ranges the posting publishes (`0` = none, `1` = one range, `4` = four location/level tiers rolled up into the columns above). |
| `offersEquity` | `true`/`false` when the posting publishes a compensation block (whether it lists an equity component); empty when it publishes none. Ashby carries no equity amounts on any board checked, so there is no equity-amount column - see the FAQ. |
| `compensationSummary` | Ashby's own summary string for the posting, for example `$190K - $330K`, `Offers Equity`, `Multiple Ranges` joined with Ashby's dash and bullet characters. |
| `publishedAt` | ISO 8601 UTC timestamp of when the posting went live. Rows are sorted newest first *within each board*; boards are written in the order you listed them. |
| `jobUrl` | Public link to the posting on `jobs.ashbyhq.com`. |
| `applyUrl` | Public link to the application form. |
| `descriptionText` | Only when `includeDescription` is on: the description as plain text, **capped at 5,000 characters**. Most Ashby descriptions are longer than that (1,960 of 2,604 hit the cap on a full 15-board run), so treat it as the opening 5,000 characters, not the whole text. |
| `scrapedAt` | ISO 8601 UTC timestamp of the run. |

A `SUMMARY` record is written to the run's key-value store with the boards that succeeded, were empty or failed (and why), postings seen versus saved, how many saved rows carry a salary and how many are remote, a workplace-type breakdown, and whether the run stopped early on its time budget.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `companies` | string list | empty | Board slugs or URLs, one per company. Case-insensitive. Accepts `openai`, `https://jobs.ashbyhq.com/openai`, `https://jobs.ashbyhq.com/openai/<job-id>/application` and the API URL. Added to the preset's boards. |
| `preset` | select | `starter` in the form | `starter` (Ramp, Plaid, Notion, Linear), `ai-labs` (OpenAI, Harvey, ElevenLabs, Sierra, Perplexity, Cohere, Cognition), `ai-labs-lite` (Perplexity, Cohere, Cognition), `dev-tools` (Linear, Replit, Supabase, Modal, Lovable), or `none`. If you leave both `preset` and `companies` empty, the run uses `starter` so you always get a sample. |
| `includeCompensation` | boolean | `true` | Fetch and parse the compensation block into the salary columns. |
| `includeDescription` | boolean | `false` | Add `descriptionText` (plain text, capped at 5,000 characters). |
| `titleContains` | string | empty | Keep only postings whose title contains this text. Case-insensitive, partial match. |
| `remoteOnly` | boolean | `false` | Keep only rows where `remote` is `true`. Hybrid roles without a remote location are dropped on purpose. |
| `maxResultsPerCompany` | integer | `200` | Row cap per board. Postings are sorted newest-published first, so a low cap keeps the freshest roles. |
| `maxResults` | integer | `500` | Row cap for the whole run. Your `companies` are processed before the preset's boards. |
| `onlyNewSinceLastRun` | boolean | `false` | Save only postings this Actor has not seen on that board before. See the FAQ for exactly how the first run behaves. |
| `maxRunSeconds` | integer | `240` | Wall-clock budget. When it is reached the run stops cleanly, keeps everything already saved, and logs which boards it did not reach. |

### Example output

A real row from OpenAI's board (2026-09-25). `compensationSummary` is shown here with plain ASCII punctuation; Ashby's real string uses an en dash and bullet characters.

```json
{
  "company": "OpenAI",
  "slug": "openai",
  "jobId": "6fd50ace-941b-4559-992f-e53f9b76c9a0",
  "title": "Security Program Manager",
  "department": "Security",
  "team": "Security",
  "employmentType": "FullTime",
  "location": "US - Remote",
  "secondaryLocations": ["New York City", "Seattle", "Washington, DC", "San Francisco"],
  "remote": true,
  "workplaceType": "Hybrid",
  "salaryMin": 129600,
  "salaryMax": 310000,
  "salaryCurrency": "USD",
  "salaryInterval": "1 YEAR",
  "salaryTiers": 3,
  "offersEquity": true,
  "compensationSummary": "$129.6K - $310K | Offers Equity | Multiple Ranges",
  "publishedAt": "2026-09-24T22:51:44.298Z",
  "jobUrl": "https://jobs.ashbyhq.com/openai/6fd50ace-941b-4559-992f-e53f9b76c9a0",
  "applyUrl": "https://jobs.ashbyhq.com/openai/6fd50ace-941b-4559-992f-e53f9b76c9a0/application",
  "scrapedAt": "2026-09-25T17:37:18.612Z"
}
```

Note the row: `workplaceType` is `Hybrid` and Ashby's `isRemote` is `true`, but the reason `remote` is `true` here is the primary location `US - Remote`. A Hybrid posting listing only office locations comes back `remote: false`.

### FAQ

**Where do I find a company's board slug?**
Open the company's careers page. If it is hosted by Ashby, the address is `https://jobs.ashbyhq.com/<slug>` - copy the slug, or paste the whole URL into `companies`. Companies that embed Ashby on their own domain usually still have a `jobs.ashbyhq.com/<slug>` board; the "Apply" links on their careers page normally reveal it. A slug with no board returns HTTP 404 and is reported for that board only - the rest of the run continues.

**How often is salary actually filled?**
Only where the employer publishes it on the posting. Measured on 2026-09-25: the default `starter` run had a range on 267 of 436 rows (Ramp 149 of 156, Plaid 118 of 121, Notion 0 of 129, Linear 0 of 30); OpenAI's newest 300 postings had one on 221; six AI labs (Perplexity, Cohere, ElevenLabs, Harvey, Sierra, Cognition) had one on 531 of 973 rows, with ElevenLabs publishing none. Expect the share to be highest for US employers covered by pay-transparency laws.

**Why is there no equity amount column?**
Ashby's compensation block has equity *components*, but on every board checked (15 boards, about 1,000 equity components) they carry no numbers - only the fact that equity is offered. A column of amounts would be permanently empty, so instead you get `offersEquity` (`true`/`false`) and the `compensationSummary` string.

**A posting publishes several ranges. Which one do I get?**
Ashby rolls multiple tiers into one range on the job page (for example four location/level tiers into `$190K - $330K`, flagged `Multiple Ranges`). This Actor uses that same roll-up for `salaryMin`/`salaryMax`, and `salaryTiers` tells you how many tiers stand behind it. On the rare posting where Ashby provides no roll-up, the tiers are aggregated (lowest minimum, highest maximum) in the currency and interval of the first tier.

**Does it also cover Greenhouse, Lever, Workday or SmartRecruiters?**
No. This Actor talks to Ashby and nothing else, and a company that is not on Ashby will 404 here no matter how the slug is spelled. For multi-platform coverage, use our **ATS Job Postings Scraper**, which handles Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee and Workable in one run; for Greenhouse alone, our **Greenhouse Jobs Scraper**.

**What exactly does "only postings new since the last run" do?**
With the option on, the Actor records the job IDs of every posting on each board it successfully fetched, in a named key-value store called `ashby-jobs-scraper-state`. On the **first** run for a board there is no baseline to compare against, so you get **every** posting, the baseline is written, and the log warns you that this happened. From the second run on you get only IDs that were not there before. A run with nothing new finishes successfully with an empty dataset and a warning - that is the expected result, not a failure. Two things to know: the baseline records every posting the board returned, including any that `maxResultsPerCompany`, `maxResults`, `titleContains` or `remoteOnly` kept out of the dataset, so a posting that was capped or filtered out will not turn up as "new" later - keep the caps above the board size and set the filters you intend to keep before the first run; and the store is shared by every run of this Actor on your account, so two schedules watching the *same* board will each consume the other's new postings (different board lists are safe, the baseline is kept per board slug).

**How fast is it?**
Ashby returns an entire board in a single response with no pagination, so cost per board is one request. Measured locally on 2026-09-25, end to end including writing the rows: across repeated runs the default `starter` run (4 boards, about 435 postings) took 2 to 7 seconds; OpenAI's board (832 postings, a 14 MB response) capped to the newest 300 took 1 to 7 seconds; six AI-lab boards totalling 973 rows took 3 to 12 seconds; all 15 preset boards with full descriptions (2,604 rows) took under 20 seconds. Platform runs add the time to write results over Apify Storage, so expect somewhat more there. Large runs are bounded by `maxRunSeconds`, not by the number of requests.

### Data source, access and limits

- **Source:** Ashby's job-postings endpoint, `https://api.ashbyhq.com/posting-api/job-board/{slug}`, documented by Ashby at developers.ashbyhq.com/docs/public-job-posting-api. This Actor calls it with no key, login, account or cookies, and always with `listedOnly=true`; it additionally drops any posting whose `isListed` field is `false`, which Ashby documents as "should only be available via direct link". Only postings the employer has published on their own board are returned. This Actor never requests anything from `jobs.ashbyhq.com`.
- **Ashby sets its own limits.** Ashby publishes no rate limit for this endpoint and this Actor does not promise one. It sends one request per board, retries transient errors with back-off, and pauses briefly between boards.
- **`remote` is derived, not copied.** Ashby's `isRemote` flag is `true` on hybrid postings (verified on OpenAI, Plaid and Ramp), so this Actor ignores it. `remote` is `true` when `workplaceType` is `Remote` or a listed location contains `remote`/`distributed`; a role described as remote only in the body text reads `false`.
- **Salary is passed through as published, in numbers.** Values, currency and interval come straight from the employer's published compensation block. Ranges labelled OTE (on-target earnings, common for sales roles) are what the employer published; the `compensationSummary` string keeps that wording.
- **Everything else is passed through as published.** Titles, departments, teams, locations and timestamps come from the employer's own Ashby data, including its inconsistencies - department and team names are whatever each company set up, and `workplaceType` is empty when the employer left it unset.
- **No personal data is extracted.** Every row describes a *job posting*, not a person: there is no recruiter name, email or phone field, and none is inferred. `descriptionText` is the employer's own published description reproduced as text, so if an employer wrote a contact address into its posting it stays there. Nothing is collected from candidates or applications.
- **Failures are explicit.** Boards that 404 or error are named in the log and in `SUMMARY.failedSources`, and the run still saves everything from the boards that worked. A board that responds with zero listed postings is reported as empty, not as an error. If *every* board fails, the run fails with a message naming the likely cause instead of returning an empty dataset that looks like "no jobs".
- **Billing:** this Actor is billed per result on Apify - the current rate is on the Pricing tab of this Actor's page. There is nothing else to buy; the data source itself needs no key.
- **Not affiliated with Ashby.** Ashby is a trademark of its owner; this Actor is an independent tool that reads publicly reachable job-board data and is not endorsed by Ashby.

# Actor input Schema

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

One entry per company. Paste the board slug or any Ashby URL - both work. The slug is the path segment in https://jobs.ashbyhq.com/<slug> (for example 'openai' or 'ramp'); posting URLs like https://jobs.ashbyhq.com/openai/<job-id> and API URLs are parsed too. Slugs are case-insensitive. A slug with no Ashby board returns HTTP 404 and is reported for that board only; the rest of the run continues. Boards listed here are scraped IN ADDITION to the ones in 'preset' - set preset to 'none' to scrape only your list.

## `preset` (type: `string`):

A ready-made list of boards, added to whatever you put in 'companies'. 'starter' = Ramp, Plaid, Notion, Linear (about 440 postings, a couple of seconds). 'ai-labs' = OpenAI, Harvey, ElevenLabs, Sierra, Perplexity, Cohere, Cognition (about 1,900 postings; raise the caps to get them all). 'ai-labs-lite' = Perplexity, Cohere, Cognition. 'dev-tools' = Linear, Replit, Supabase, Modal, Lovable. Every slug was verified live on 2026-09-25. If you leave both this and 'companies' empty, the run uses 'starter' so you always get a sample.

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

Ask Ashby for the compensation block and parse it into salaryMin, salaryMax, salaryCurrency, salaryInterval, salaryTiers, offersEquity and compensationSummary. Only employers that publish pay on their postings have values here - on 2026-09-25 that was 149 of 156 Ramp postings and 0 of 129 Notion postings. Turning this off leaves those columns empty and makes the response a little smaller.

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

Add descriptionText: the employer's description as plain text, capped at 5,000 characters (Ashby descriptions run up to about 12,000). Off by default because it multiplies the dataset size - OpenAI's board alone is over 800 postings.

## `titleContains` (type: `string`):

Keep only postings whose title contains this text. Case-insensitive, partial match - 'engineer' matches 'Software Engineer, Backend'. Leave empty for every title.

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

Keep only postings that can be worked fully remotely: the workplace type Ashby publishes is Remote, or one of the posting's listed locations names a remote option (for example 'Remote (US)'). Hybrid and on-site roles are dropped on purpose - Ashby's own isRemote flag is also set on hybrid roles, so it is not used.

## `maxResultsPerCompany` (type: `integer`):

Upper bound on rows saved for each board. Postings are sorted newest-published first, so a low cap keeps the freshest roles. Ashby returns a whole board in one request, so raising this costs almost nothing in time.

## `maxResults` (type: `integer`):

Hard cap on rows for the whole run, across all boards. Boards are processed in order (your 'companies' first, then the preset), so a cap below the combined total cuts off the boards at the end of the list - raise it, or split the run, when scraping many large boards.

## `onlyNewSinceLastRun` (type: `boolean`):

Save only postings this Actor has not seen before. Job IDs from each successfully fetched board are recorded in a named key-value store ('ashby-jobs-scraper-state') and compared on the next run with this option on. The FIRST run for a board has no baseline, so it returns every posting and records the baseline; from the second run on you get only new ones. A run with nothing new finishes successfully with an empty dataset and a warning in the log.

## `maxRunSeconds` (type: `integer`):

Wall-clock budget for the crawl. When it is reached the Actor stops cleanly, keeps everything already saved, and logs which boards it did not reach - instead of being killed with a failed run. Raise it when you pass a long list of boards.

## Actor input object example

```json
{
  "companies": [],
  "preset": "starter",
  "includeCompensation": true,
  "includeDescription": false,
  "remoteOnly": false,
  "maxResultsPerCompany": 200,
  "maxResults": 500,
  "onlyNewSinceLastRun": false,
  "maxRunSeconds": 240
}
```

# Actor output Schema

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

One row per listed job posting from each Ashby board, with numeric salary range where published, a hybrid-excluding remote flag, locations, department, team and links.

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

Totals for the run: boards that succeeded, were empty or failed, postings seen versus saved, how many rows carry a salary or are remote, and whether the run stopped early on its time budget.

# 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": [],
    "preset": "starter"
};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/ashby-jobs-scraper").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": [],
    "preset": "starter",
}

# Run the Actor and wait for it to finish
run = client.actor("maydit/ashby-jobs-scraper").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": [],
  "preset": "starter"
}' |
apify call maydit/ashby-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,maydit/ashby-jobs-scraper"
        }
    }
}
```

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/NwsKYMzdQADiQlj6d/builds/A9h2cJUhcXr53A7xF/openapi.json
