Skip to content

About

Ashby jobs scraper for Python: pull every open posting from a jobs.ashbyhq.com board with team, locations, workplace type and salary band into JSON, CSV or pandas. Python example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

1 Commit

Folders and files

Repository files navigation

Ashby Jobs Scraper β€” Python example

Ashby Jobs Scraper in Python: startup job boards to pandas, JSON and CSV

Run on Apify Python MIT license CI

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

πŸ”₯ What you get

  • 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, or null when the role sits under no team.
  • Pay bands where employers disclose them: compensation_summary holds Ashby's summary string with its currency and any equity note; boards that keep pay private give null.
  • Every office, not only the first: location_name is the primary site, secondary_locations lists the others, and the optional locationFilter checks both.
  • Slug or URL, your choice: send openai or https://jobs.ashbyhq.com/openai; when the host is present, the path segment after it becomes the slug.
  • Apply links at no extra cost: apply_url is 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.

🎯 Use cases

Benchmark salary bands across startups

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.

Measure hiring velocity per team

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.

Map where startups hire

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.

Build a sourcing shortlist

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.

⚑ Quick start

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 2

The 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.

🧩 The core call

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

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} β€”

πŸ“€ Output

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

πŸ’° Pricing

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.

πŸ› οΈ Working with the data in Python

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())

❓ FAQ

Which slug do I use for a company?

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.

Why is compensation_summary empty for some companies?

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.

What does a wrong slug do to the batch?

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.

How do I use it from Jupyter?

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.

🧯 Troubleshooting

  • Set APIFY_TOKEN first: export the token in the same shell, or put it in a local .env file your shell loads. Never commit it.
  • Run finished with FAILED or ABORTED: open the printed Console link; the run log says why. Input validation errors name the field.
  • Fewer rows than expected: a --max-charge cap 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.

πŸ”— More from Devil Scrapes

Related Actors:


MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Ashby.

About

Ashby jobs scraper for Python: pull every open posting from a jobs.ashbyhq.com board with team, locations, workplace type and salary band into JSON, CSV or pandas. Python example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages