# Ashby Jobs Scraper - Job Board API & New-Job Tracking (`cedarfetch/ashby-jobs-scraper`) Actor

Every open job on any company's Ashby board, read from Ashby's official public Job Posting API: title, department, team, locations, workplace type, employment type, salary where published, description and dates. Built-in change tracking reports new, updated and removed jobs.

- **URL**: https://apify.com/cedarfetch/ashby-jobs-scraper.md
- **Developed by:** [Cedarfetch Labs](https://apify.com/cedarfetch) (community)
- **Features:** Limited permissions
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 jobs

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

## Ashby Jobs Scraper - Job Board API & New-Job Tracking

Get every open job on any company's Ashby board, straight from **Ashby's official public Job Posting API** (`api.ashbyhq.com/posting-api`), the same feed that powers the company's `jobs.ashbyhq.com` page. One clean row per job: title, department, team, locations, workplace type (remote, hybrid, on-site), employment type, salary range and equity text where the company publishes compensation, description as text and HTML, publish date and links.

Turn on **change tracking** and schedule it: every row then says whether the job is `new`, `updated` (with the fields that changed), `unchanged`, or `removed` since the last run. With "Output only changes", a daily run returns just the jobs that opened and closed.

No browser, no proxies, no login. A whole Ashby board, compensation included, is **one request**, so runs take seconds.

### What you can use it for

- **Recruiters and sourcers**: watch the Ashby boards of fast-growing startups and see new roles the day they open.
- **Job boards and aggregators**: clean, normalised postings from hundreds of Ashby companies, with closed jobs flagged so you can expire them.
- **Sales and investors**: hiring as a buying or growth signal; Ashby is popular with venture-backed startups, so a board is often an early read on a company's plans.
- **Labour-market analysts**: salary ranges are published on about half of Ashby jobs, more than on most systems.
- **AI agents and pipelines**: one stable JSON shape you can rely on.

### Why this one

- **Official API only.** Reads Ashby's documented public Job Posting API. Nothing is scraped from HTML, nothing is bypassed.
- **Salary included.** Compensation is requested with every board (`includeCompensation=true`): min, max, currency and interval from the salary component, plus Ashby's summary text ("$211.4K - $290.6K, Offers Equity").
- **Every input form works.** Board URLs, single-job and application links, API URLs, links with `?ashby_jid=` on a company's own careers site, or just the board name.
- **Honest remote flag.** Ashby's own `isRemote` is true for many hybrid jobs, so `workplaceType` (Remote / Hybrid / OnSite) is used instead; see below.
- **Change tracking built in.** New, updated, unchanged and removed jobs, with the changed fields and their previous values. Removed jobs are only reported after a complete, error-free read of the board.
- **Honest failures.** Unknown boards, empty boards and typos are reported per board in plain words, never as a silent empty dataset. If nothing at all could be read, the run fails and says why.
- **Same schema as ATS Jobs Scraper.** Datasets from this Actor, the Greenhouse and Lever editions, and the multi-system [ATS Jobs Scraper](https://apify.com/cedarfetch/ats-jobs-scraper) combine without any mapping.

### How to find a company's Ashby board name

Open the company's careers page and click any job.

- If it opens on `jobs.ashbyhq.com/ramp/...`, the board name is the first part of the path: `ramp`. Paste the whole URL; the Actor reads the name from it.
- If the job opens on the company's own domain with `?ashby_jid=` in the address, paste that link. The board name is taken from the domain, and you get a warning if that job is not on the guessed board. If the guess is wrong, look for the `jobs.ashbyhq.com/<name>` link behind "Apply".
- To check a name by hand: `https://api.ashbyhq.com/posting-api/job-board/<name>` returns JSON for a real board and 404 otherwise.

### Input

Only `companies` is required. One entry per line:

| You paste | Read as |
|---|---|
| `https://jobs.ashbyhq.com/ramp` | board `ramp` |
| `https://jobs.ashbyhq.com/ramp/<job-id>` or `.../<job-id>/application` | board `ramp` (a single-job link reads the whole board) |
| `https://api.ashbyhq.com/posting-api/job-board/ramp` | board `ramp` |
| `https://www.notion.com/careers?ashby_jid=<job-id>` | board guessed from the domain (`notion`), with a warning if that job is not on it |
| `ramp` or `ashby:ramp` | board `ramp` |

Duplicate entries for the same board are merged. A URL for a different system (Greenhouse, Lever, SmartRecruiters...) is not read; it gets a free status row that names the Actor that reads it.

#### Example: engineering roles at two companies

```json
{
    "companies": ["https://jobs.ashbyhq.com/ramp", "notion"],
    "titleKeywords": ["engineer*"],
    "excludeTitleKeywords": ["intern"],
    "includeDescription": false
}
```

#### Example: remote full-time jobs posted this week

```json
{
    "companies": ["ramp", "notion", "linear"],
    "remoteOnly": true,
    "employmentTypes": ["full_time"],
    "postedAfter": "7 days"
}
```

#### All options

| Field | Default | What it does |
|---|---|---|
| `companies` | (required) | Ashby boards in any form above. Up to 2,000 per run. |
| `maxJobsPerCompany` | 0 (all) | Stop after this many matching jobs per board. |
| `includeDescription` | true | Add `descriptionText` and `descriptionHtml` (no extra request on Ashby). |
| `titleKeywords` | none | Keep titles containing these words. |
| `titleKeywordsMode` | `any` | `any` or `all` of the keywords. |
| `excludeTitleKeywords` | none | Drop titles containing any of these words. |
| `locations` | none | Keep jobs whose locations or country match any of these words. |
| `departments` | none | Keep jobs whose department or team matches. |
| `remoteOnly` | false | Keep only `isRemote: true`. |
| `postedAfter` | none | ISO date (`2026-11-01`) or relative (`7 days`, `24 hours`, `2 weeks`), compared with Ashby's publish date. |
| `employmentTypes` | none | Any of `full_time`, `part_time`, `contract`, `temporary`, `internship`, `apprenticeship`, `volunteer`, `other`. |
| `trackChanges` | false | Compare with the previous run and label every row. |
| `changesOnly` | false | With tracking on, deliver only the selected change types. |
| `changeTypes` | `new`, `updated`, `removed` | Which changes `changesOnly` delivers. |
| `snapshotStoreName` | `ashby-jobs-scraper-snapshots` | Named key-value store for snapshots. Use different names for independent feeds. |
| `concurrency` | 2 | Parallel requests per board (a board is one request, so this rarely matters). |
| `requestDelayMs` | 250 | Pause after each request to the same board. |
| `maxConcurrentCompanies` | 4 | Boards read at the same time. |
| `requestTimeoutSecs` | 60 | Timeout per request. |
| `maxRetries` | 4 | Retries for 429, 5xx and network errors. |

**Keyword matching** (titles, locations, departments) is case- and accent-insensitive and works on whole words: `engineer` matches "Senior Engineer" but not "Engineering"; `engineer*` matches both; `US` matches "Remote (US)" but not "Austin"; phrases such as `data scientist` work. Filters run before `maxJobsPerCompany`, so the cap counts matching jobs.

### Output

One row per job in the default dataset. Download as JSON, CSV, Excel or XML from the Output tab or the API. Views: **Jobs**, **Changes**, **Job details**, **Companies that could not be read**.

A real row from a local run on 2026-11-11 (`https://jobs.ashbyhq.com/ramp`; description shortened):

```json
{
    "recordType": "job",
    "ats": "ashby",
    "companyToken": "ramp",
    "companyName": null,
    "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "requisitionId": null,
    "title": "Security Engineer, Cloud",
    "department": "Engineering",
    "team": "Backend",
    "location": "New York, NY (HQ)",
    "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
    "country": "USA",
    "isRemote": true,
    "workplaceType": "hybrid",
    "employmentType": "full_time",
    "employmentTypeRaw": "FullTime",
    "compensation": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year", "text": "$211.4K – $290.6K • Offers Equity" },
    "descriptionText": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance team...",
    "descriptionHtml": "<h1><strong>About Ramp</strong></h1><p s...",
    "postedAt": "2026-04-07T17:12:35.753Z",
    "updatedAt": null,
    "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
    "changeType": null,
    "changedFields": null,
    "previous": null,
    "firstSeenAt": null,
    "removedAt": null,
    "matchedBy": "url",
    "input": "https://jobs.ashbyhq.com/ramp",
    "source": "api",
    "scrapedAt": "2026-11-12T00:41:22.793Z"
}
```

Dates are ISO 8601 in UTC; numbers are numbers; `jobId` is always a string. The schema is identical to [ATS Jobs Scraper](https://apify.com/cedarfetch/ats-jobs-scraper), which is why `ats` is a field.

#### What Ashby exposes, and what it does not

| Field | On Ashby |
|---|---|
| `companyName` | Not exposed by the Job Posting API: always `null`. Use `companyToken`. |
| `requisitionId` | Not exposed: always `null`. |
| `department`, `team` | Ashby's department and team. |
| `location`, `locations`, `country` | Primary location plus secondary locations, and the country from the job's address when set. Not geocoded. |
| `workplaceType`, `isRemote` | From Ashby's `workplaceType` (Remote, Hybrid, OnSite). `isRemote` is `true` for remote jobs, or when any listed location says Remote (the hybrid row above lists "Remote (US)"). Ashby's own `isRemote` flag is ignored when `workplaceType` is set, because Ashby sets it on many hybrid jobs with no remote location (102 of 136 hybrid Ramp jobs). |
| `employmentType` | Mapped from Ashby's employmentType (FullTime, PartTime, Contract, Intern, Temporary); present on every job seen so far. |
| `compensation` | When the company publishes compensation on Ashby: the salary component's min, max, currency and interval, plus the summary text (which can mention equity or bonus). 170 of 332 jobs in a Ramp + Notion run had it. |
| `postedAt` | Ashby's publish date. |
| `updatedAt` | Not exposed: always `null`. Change tracking compares content instead. |
| `descriptionText` / `descriptionHtml` | Yes, included in the board at no extra cost. |
| Unlisted jobs | Jobs the company marked unlisted (reachable by direct link only) are not on the public board and are skipped. |

#### Boards that could not be read

A free `company_status` row is added for every entry that could not be read, with a `status` and a plain-language `message`:

| status | Meaning |
|---|---|
| `not_found` | Ashby has no public board with that name (typo, or the company uses another system). |
| `no_jobs` | The board exists but has no open jobs. |
| `blocked` | Ashby refused the request (401/403) or answered with a bot-protection challenge. Never worked around. |
| `rate_limited` | Still rate-limited after polite retries. Try later or lower `concurrency`. |
| `unreachable` / `error` | Network failure or an unexpected answer. |
| `invalid_input` | Not an Ashby URL or board name, or a board on another system (the message names the Actor that reads it). |
| `partial` | (in `OUTPUT`) Some jobs were read, then the run hit an error or the charge limit. |

If a run finds jobs but delivers none (all filtered out, or nothing changed in changes-only mode), one free `company_status` row per board explains it.

#### Run summary

The default key-value store record `OUTPUT` has, per board: how the input was resolved, status, jobs found / matched / delivered, removed jobs reported, requests, retries, duration, whether the board was read completely, filter counts, change counts and warnings. Use it to monitor scheduled runs.

### Change tracking

Turn on **Track changes between runs** and schedule the Actor. Each run compares every matching job with the snapshot from the previous run of the same board and filters, and adds:

| Field | Meaning |
|---|---|
| `changeType` | `new`, `updated`, `unchanged` or `removed`. |
| `changedFields` | For `updated`: e.g. `["title", "compensation"]`. Tracked: title, department, team, locations, workplaceType, isRemote, employmentType, compensation, description. |
| `previous` | For `updated`: the previous values of the changed fields (for description only the change is detected; old text is not stored). |
| `firstSeenAt` | When the job first appeared in this feed. |
| `removedAt` | For `removed` rows: when the removal was detected. Removed rows carry the last known title, location, department, dates and links. |

Rules you can rely on:

- **The first tracked run labels every job `new`.** It sets the baseline.
- **`removed` needs a complete read.** If the board could not be read, nothing is labelled removed; the next complete run reports it. An Ashby board is one request, so even a run capped by `maxJobsPerCompany` sees the whole board and can detect removals.
- **Nothing is lost on a budget stop.** The snapshot only records jobs that were delivered, so a change that did not fit in this run is reported in the next one.
- **Filters define the feed.** A job that stops matching your filters is not "removed"; only jobs that disappear from the board are. Each combination of board and filters has its own snapshot.
- **Salary changes count.** A changed range shows up as `updated` with `changedFields: ["compensation"]` and the old range in `previous`.
- **Descriptions are compared by hash**, even with `includeDescription` off (Ashby sends them with the board).
- Snapshots live in the named key-value store `snapshotStoreName` in your Apify account (compressed, about 60 bytes per job). Delete it, or pick a new name, to start over.

Tested locally on 2026-11-11 with `ashby` and `notion` (232 jobs): run 1 = 232 `new`, run 2 = 232 `unchanged`; after editing the snapshot, a changes-only run delivered exactly the 2 `new`, 2 `updated` and 2 `removed` jobs.

#### Recipe: daily new-jobs feed

1. Create a task with your boards and filters, plus:

```json
{
    "companies": ["ramp", "notion", "linear"],
    "titleKeywords": ["engineer*", "data"],
    "trackChanges": true,
    "changesOnly": true,
    "changeTypes": ["new"],
    "includeDescription": false
}
```

2. Run it once to set the baseline (every job is `new` on that run).
3. Schedule it daily. From then on each run delivers only the jobs that opened since the previous run, and you pay only for those. Add `"removed"` to `changeTypes` to also get closed jobs.
4. Connect the dataset to Slack, email, a Google Sheet or a webhook through Apify integrations.

### Pricing

Pay per event: one `job` event per job row delivered to the dataset (including `removed` rows), plus Apify's small run-start fee. See the Pricing tab for the current price.

- `company_status` rows are free.
- With changes-only output, unchanged jobs are not delivered and not charged.
- Set **Maximum cost per run** in the run options to cap spending. The Actor stops cleanly at the cap, keeps everything delivered so far, and notes it in `OUTPUT`.

### Use from an AI agent or via API

Ready-to-run Python, Node.js and cURL examples: [github.com/cedarfetch/ats-jobs-scraper-examples](https://github.com/cedarfetch/ats-jobs-scraper-examples). The examples use the multi-system ATS Jobs Scraper; change the Actor id to this one.

Run synchronously and get the rows back in one HTTP call:

```bash
curl -X POST "https://api.apify.com/v2/acts/cedarfetch~ashby-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies":["ramp"],"titleKeywords":["engineer*"],"maxJobsPerCompany":50,"includeDescription":false}'
```

JavaScript:

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('cedarfetch/ashby-jobs-scraper').call({ companies: ['notion'], trackChanges: true, changesOnly: true });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Python:

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("cedarfetch/ashby-jobs-scraper").call(run_input={"companies": ["ramp"]})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

The Actor is also available to agents through the Apify MCP server, which exposes Store Actors as tools. Tips for agents: pass board names or URLs; keep `maxJobsPerCompany` small and `includeDescription` off for exploratory calls; filter on `recordType == "job"`; read `OUTPUT` for per-board status.

### Limits and honest caveats

- **Public postings only.** It reads what the company publishes on its Ashby board. Internal, confidential and unlisted postings are out of reach by design. Nothing about applicants or employees is collected; names that appear inside a description are not extracted into fields.
- **Large boards are large responses.** The whole board arrives in one answer with every description; a board with a few hundred jobs can be 10+ MB and take several seconds.
- **No company name, update date or requisition code** in the Job Posting API (see the table above).
- **Embedded careers sites.** An `?ashby_jid=` link only tells the Actor that the site uses Ashby; the board name is guessed from the domain. Check the warning in `OUTPUT` and pass the real name if the guess is wrong.
- **Language and location formats** are as each company writes them. Locations are not geocoded.

### FAQ

**Does this need an Ashby API key?** No. Ashby's Job Posting API is public and needs no key. (Ashby's other APIs, which do need a key, expose candidates; this Actor never uses them.)

**Can I read many companies in one run?** Yes, up to 2,000 boards. Four are read at a time by default; most boards take one or two seconds.

**Why does a company show fewer jobs than its website?** Unlisted jobs are skipped, and some companies run several boards or list jobs from other systems too. You get the boards you pass. Check `OUTPUT` for filters and caps.

**Do I need proxies?** No. The Job Posting API is meant to be read by anyone. Boards that refuse plain requests are reported, not worked around.

**What happens to a job that is reposted?** It usually gets a new Ashby id, so it appears as `new` and the old one as `removed`.

**Is this legal?** The Actor reads job postings that companies publish through a public API intended for that purpose, and collects no personal data about applicants or employees. You are responsible for how you use the data and for the source's terms. If in doubt, ask a lawyer.

Not affiliated with Ashby. "Ashby" is used only to describe which public API this Actor reads.

### Support

Found a board that behaves oddly? Open an issue on the Issues tab with the board name and the run ID; the `OUTPUT` record usually shows what happened.

### More Actors from Cedarfetch Labs

Built the same way: public data only, typed output, per-source error reporting, change tracking for scheduled runs.

- [ATS Jobs Scraper](https://apify.com/cedarfetch/ats-jobs-scraper): the multi-system option. Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio in one run, same schema, with auto-detection of a company's system.
- [Greenhouse Jobs Scraper](https://apify.com/cedarfetch/greenhouse-jobs-scraper): every job on any Greenhouse board, with pay ranges on request, from the official Greenhouse Job Board API.
- [Lever Jobs Scraper](https://apify.com/cedarfetch/lever-jobs-scraper): every posting on any Lever job site, global or EU, from the official Lever Postings API.
- [Remote Jobs Scraper](https://apify.com/cedarfetch/remote-jobs-scraper): remote jobs from Remote OK, We Work Remotely and five more boards in one deduplicated feed.

# Changelog

This Actor's version history is a separate document: https://apify.com/cedarfetch/ashby-jobs-scraper/changelog.md

# Actor input Schema

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

One per line. Accepts Ashby board URLs (https://jobs.ashbyhq.com/ramp), a link to a single job (reads the whole board), API URLs (api.ashbyhq.com/posting-api/job-board/ramp), a job link on the company's own site that carries ?ashby_jid= (the board name is taken from the domain, with a warning if the job is not on it), or the bare board name (ramp).

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

Stop after this many matching jobs per company. 0 means all. Keeps test runs and samples cheap; a capped run never reports removed jobs.

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

Add descriptionText (plain text) and descriptionHtml. Ashby returns the description with the job list, so this costs no extra request; turn it off for smaller rows.

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

Keep jobs whose title contains these words (case- and accent-insensitive, whole words). Add \* for a prefix: "engineer\*" also matches "Engineering". Phrases work: "data scientist".

## `titleKeywordsMode` (type: `string`):

"any": at least one keyword in the title. "all": every keyword in the title.

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

Drop jobs whose title contains any of these words (same matching rules), for example "intern" or "senior".

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

Keep jobs where any listed location (or the country code / name Ashby gives) matches one of these words, for example "London", "Germany", "US" or "Remote". Whole-word matching, so "US" does not match "Austin".

## `departments` (type: `array`):

Keep jobs whose department or team matches one of these words, for example "Engineering" or "Sales".

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

Keep only jobs with isRemote = true. On Ashby this comes from the job's workplaceType (Remote / Hybrid / OnSite), plus location names such as "Remote" (see the README).

## `postedAfter` (type: `string`):

Keep jobs first published on or after this moment: an ISO date (2026-11-01 or 2026-11-01T12:00:00Z) or a relative value such as "7 days", "24 hours" or "2 weeks". Jobs without a publish date are dropped by this filter.

## `employmentTypes` (type: `array`):

Keep only these normalised employment types, mapped from Ashby's employmentType (FullTime, PartTime, Contract, Intern, Temporary). Jobs without one have employmentType null and are dropped by this filter.

## `trackChanges` (type: `boolean`):

Compare every job with the previous run and add changeType (new, updated, unchanged, removed), the changed fields and their previous values, and firstSeenAt. Jobs that disappeared from the board are reported as rows with changeType "removed" and removedAt. The snapshot is kept in a named key-value store in your account, so it survives between runs. Schedule the Actor daily for a jobs feed.

## `changesOnly` (type: `boolean`):

Requires change tracking. Deliver (and pay for) only the change types selected below; unchanged jobs are skipped but the snapshot is still refreshed. The first tracked run labels every job "new".

## `changeTypes` (type: `array`):

With "Output only changes": which change types to deliver. A daily new-jobs feed uses \["new"]; new plus closed jobs uses \["new", "removed"].

## `snapshotStoreName` (type: `string`):

Name of the key-value store that holds the change-tracking snapshots. Leave empty for the default "ashby-jobs-scraper-snapshots" (separate from the other Cedarfetch jobs Actors, so their tracking never mixes). Use a different name to run independent tracking jobs on the same boards (for example one per client). Lowercase letters, digits and hyphens.

## `concurrency` (type: `integer`):

How many requests run at once against one board. An Ashby board is read in a single request, so this rarely matters.

## `requestDelayMs` (type: `integer`):

Pause after each request to the same company's board, with small random jitter.

## `maxConcurrentCompanies` (type: `integer`):

How many different companies are read at the same time.

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

Give up on a single request after this many seconds (it is then retried). A whole Ashby board is one response; large boards with descriptions are several MB.

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

Retries for rate-limit (429), server-error (5xx) and network failures, with jittered exponential backoff that honours Retry-After. 401/403 and bot-protection answers are never retried.

## Actor input object example

```json
{
  "companies": [
    "https://jobs.ashbyhq.com/ramp",
    "notion",
    "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245"
  ],
  "maxJobsPerCompany": 200,
  "includeDescription": true,
  "titleKeywords": [
    "engineer*",
    "product manager"
  ],
  "titleKeywordsMode": "any",
  "excludeTitleKeywords": [
    "intern",
    "principal"
  ],
  "locations": [
    "New York",
    "Remote"
  ],
  "departments": [
    "Engineering"
  ],
  "remoteOnly": false,
  "postedAfter": "7 days",
  "trackChanges": false,
  "changesOnly": false,
  "changeTypes": [
    "new",
    "updated",
    "removed"
  ],
  "concurrency": 2,
  "requestDelayMs": 250,
  "maxConcurrentCompanies": 4,
  "requestTimeoutSecs": 60,
  "maxRetries": 4
}
```

# Actor output Schema

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

All job rows (including removed rows with change tracking), plus company_status rows for boards that could not be read.

## `changes` (type: `string`):

The same rows shown with the change-tracking columns.

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

Per-board status (ok, partial, no_jobs, not_found, blocked, rate_limited, unreachable, invalid_input...), job counts, requests, duration and change counts.

# 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": [
        "https://jobs.ashbyhq.com/ashby"
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("cedarfetch/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": ["https://jobs.ashbyhq.com/ashby"],
    "maxJobsPerCompany": 20,
}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cedarfetch/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/ijHwo4H4840GDFKlo/builds/3u3T2xKuE75hchtsf/openapi.json
