This repository wraps the Ashby Jobs Scraper in a short Python script you can run from a terminal, a notebook or a scheduler. Ashby is the applicant tracking system behind the careers pages of many venture-backed startups, and each of those pages lives at jobs.ashbyhq.com/<org>. Give the script a list of those org slugs and it returns one tidy row per open role: title, team, office locations, remote or hybrid status, employment type and, when the company discloses it, the salary band. Apify does the fetching, so what lands on your disk is results.json and results.csv, ready for a DataFrame.
Actor: Ashby Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in JavaScript: ashby-jobs-scraper-javascript
- A whole board per request: Ashby answers a single GraphQL query with every live posting an organization has, so a large board costs one round trip rather than a loop over pages.
- Readable team names: a posting only carries a team id; the Actor looks it up in the board's own team list and writes the name to
team_name, ornullwhen the role sits under no team. - Pay bands where employers disclose them:
compensation_summaryholds Ashby's summary string with its currency and any equity note; boards that keep pay private givenull. - Every office, not only the first:
location_nameis the primary site,secondary_locationslists the others, and the optionallocationFilterchecks both. - Slug or URL, your choice: send
openaiorhttps://jobs.ashbyhq.com/openai; when the host is present, the path segment after it becomes the slug. - Apply links at no extra cost:
apply_urlis assembled from the slug and the posting id, so it adds no requests on top of the board fetch. - Failures stay with one organization: rate limits and server errors are retried with backoff, and an org that still fails is counted in the status message while the rest of the batch is written.
Ashby boards often print a pay range next to each role, which makes them a rare public source of compensation data. Collect a peer group of companies, split compensation_summary into low and high numbers, and compare medians by team or by a title keyword in a notebook.
Run the script daily from cron into dated folders. Counting rows per org_slug and team_name across those folders shows which functions a company is growing, and a posting id that vanishes marks a role that was filled or withdrawn.
Explode secondary_locations next to location_name and you get every city a role can be done from. Combined with workplace_type, a few lines of pandas show the remote, hybrid and office split for a whole sector.
Recruiting agencies can filter a batch of boards by title words and location, then hand their sourcers a CSV of matching roles with apply links, refreshed as often as the cron entry says.
You need an Apify account and an API token from Console β Settings β Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.
git clone https://github.com/devilscrapes/ashby-jobs-scraper-python.git
cd ashby-jobs-scraper-python
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export APIFY_TOKEN=your_token_here
python main.py # uses input.json
python main.py my-input.json --out data --max-charge 2The script starts the Ashby Jobs Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.
Strip away the command-line flags and file handling and the integration is a few lines: apify-client starts the run, blocks until it ends, then iterates over the dataset it produced.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("DevilScrapes/ashby-jobs-scraper").call(run_input={'orgSlugs': ['openai'], 'maxResultsPerOrg': 10, 'proxyConfiguration': {'useApifyProxy': True}})
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item)input.json is the Actor's own prefill: the openai board, capped at ten postings, which costs about $0.22 at the prices listed below. Swap in your own list of slugs, raise maxResultsPerOrg, or set locationFilter to a city such as London to keep only roles that mention it. The cap is applied after the filter, so a cap of ten with a filter set means ten matching roles, not ten roles minus the misses.
{
"orgSlugs": [
"openai"
],
"maxResultsPerOrg": 10,
"proxyConfiguration": {
"useApifyProxy": true
}
}| Field | Type | Required | Default | Allowed values |
|---|---|---|---|---|
orgSlugs |
array | yes | ["openai"] |
β |
maxResultsPerOrg |
integer | no | 500 |
β |
locationFilter |
string | no | β | β |
proxyConfiguration |
object | no | {"useApifyProxy": true} |
β |
Each row is one posting. Three fields deserve a note before analysis: compensation_summary is free text in Ashby's own format, team_name is null for postings filed under no team, and org_slug echoes your input rather than a company display name, because the board query does not return one.
| Field | Type | Always present |
|---|---|---|
posting_id |
string | yes |
title |
string | yes |
org_slug |
string | yes |
team_name |
string or null | no |
location_name |
string | yes |
workplace_type |
string or null | no |
employment_type |
string or null | no |
secondary_locations |
array | yes |
compensation_summary |
string or null | no |
apply_url |
string | yes |
scraped_at |
string | yes |
| Event | Price (USD) |
|---|---|
| Actor start | $0.2 |
| Result emitted | $0.0015 |
Prices as configured on 2026-08-20. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.
The snippet below turns pay summaries into numbers and rolls them up by organization and team with pandas (pip install pandas). It pulls the currency amounts out of each compensation_summary, keeps the lowest and highest, counts remote roles and writes the per-team table to teams.csv.
import json
import re
import pandas as pd
rows = pd.DataFrame(json.load(open("results.json", encoding="utf-8")))
SCALE = {"": 1, "K": 1_000, "M": 1_000_000}
def band(text):
"""(low, high) from a summary such as '$342K β $555K β’ Offers Equity'."""
amounts = [float(n) * SCALE[s.upper()] for n, s in re.findall(r"[$β¬Β£](\d+(?:\.\d+)?)([KkMm]?)", text)]
return (min(amounts), max(amounts)) if amounts else (None, None)
rows[["pay_low", "pay_high"]] = rows["compensation_summary"].fillna("").apply(lambda t: pd.Series(band(t)))
rows["team_name"] = rows["team_name"].fillna("(no team)")
teams = (
rows.groupby(["org_slug", "team_name"])
.agg(
openings=("posting_id", "count"),
remote=("workplace_type", lambda s: int((s == "Remote").sum())),
median_low=("pay_low", "median"),
median_high=("pay_high", "median"),
)
.sort_values("openings", ascending=False)
)
teams.to_csv("teams.csv")
print(teams.head(15).to_string())Open the company's careers page. If it links to or embeds jobs.ashbyhq.com/something, then something is the slug. Slugs are sent exactly as written, capital letters included, so copy them rather than retyping.
Ashby shows a pay range only where the employer chose to publish it. Ramp, for one, publishes none, so its rows carry null in that column. Treat it as missing data, never as zero, when you compute medians.
A slug whose board comes back empty or absent contributes zero rows and the next org is processed. Request failures are handled the same way, and the run still ends as succeeded; the status message says how many orgs actually returned data, so read it in scheduled jobs instead of trusting the exit code alone.
In a notebook cell, import main and pass the argument list ["input.json", "--out", "data"] to main.main. The files land in data/, and pd.read_json("data/results.json") gives you the frame.
Set APIFY_TOKEN first: export the token in the same shell, or put it in a local.envfile your shell loads. Never commit it.- Run finished with
FAILEDorABORTED: open the printed Console link; the run log says why. Input validation errors name the field. - Fewer rows than expected: a
--max-chargecap also limits how many results can be charged. Raise the cap or lower the limits in your input. - Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.
- Ashby Jobs Scraper on Apify Store
- JavaScript version of this example
- All Devil Scrapes Actors
- Apify web scraping cookbook
Related Actors:
- Lever Jobs Scraper β the same per-company board export for employers that hire through Lever.
- Workday Jobs Scraper β enterprise career sites on myworkdayjobs.com, for the larger end of the market.
- Wellfound Startup Jobs Scraper β startup roles searched by keyword and location across Wellfound, with funding stage.
MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Ashby.