Skip to content

About

Ashby jobs scraper for JavaScript and Node.js: fetch the full job board of any Ashby-hosted employer, with teams, pay bands and apply URLs, for job boards, webhooks and backends. JavaScript 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 β€” JavaScript example

Ashby Jobs Scraper for Node.js: Ashby job boards inside your app or webhook

Run on Apify JavaScript MIT license CI

Here is a compact Node.js client for the Ashby Jobs Scraper, aimed at developers who put startup job data inside a product: a niche job board, a hiring-signal feed in a sales tool, a chat bot that announces new roles. Ashby-hosted employers publish their openings at jobs.ashbyhq.com/<org>. This ES module sends a list of those org identifiers to the Actor on Apify, waits for the run, and hands your code an array of postings with team, locations, workplace type, employment type, pay band and an apply URL. Run it from the command line, or lift the call into an Express handler, a queue consumer or a serverless function.

Actor: Ashby Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in Python: ashby-jobs-scraper-python

πŸ”₯ 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

Power a niche job board

Sync a curated list of Ashby employers into your database every few hours, keyed on posting_id. Insert the new ids, delete the ones that disappeared, and your listings stay in step with the source without a crawler of your own to maintain.

Feed hiring signals into a CRM

Teams selling to fast-growing startups can turn a burst of new engineering or go-to-market roles into a CRM task. A scheduled function compares counts per org_slug and team_name against the previous run and pushes only the jumps.

Announce new roles over a webhook

Keep the previous set of posting ids, diff on every run and post the newcomers to Slack, Discord or Teams. The recipe further down does exactly that with nothing but Node built-ins.

Let users filter by city in your API

Expose an endpoint that takes a city name and forwards it as locationFilter. The match runs inside the Actor across primary and secondary locations, so your service only relays the result and pays for fewer rows.

⚑ 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-javascript.git
cd ashby-jobs-scraper-javascript
npm install
export APIFY_TOKEN=your_token_here
node index.js                       # uses input.json
node index.js 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

Everything else in index.js is argument parsing and file output. The part worth copying into a service is below: call() resolves once the run has finished, and listItems() returns its dataset rows.

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('DevilScrapes/ashby-jobs-scraper').call({"orgSlugs": ["openai"], "maxResultsPerOrg": 10, "proxyConfiguration": {"useApifyProxy": true}});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);

πŸ“₯ Input

The bundled input.json points at OpenAI's board with a ten-row cap; at today's event prices that is roughly $0.22 per test. In production you will usually build this object in code: an array of slugs from your database, a maxResultsPerOrg that suits your page size, and locationFilter when the request arrived with a city.

{
  "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

Rows are flat JSON objects, so they map directly onto a table or a document store. posting_id stays the same across runs and makes a good primary key, secondary_locations is always an array that may be empty, and team_name, workplace_type, employment_type and compensation_summary can each be null.

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 JavaScript

This script turns consecutive runs into an alert. It remembers which posting ids it has seen in seen-postings.json, prints the new ones, and sends them to the URL in WEBHOOK_URL when you set that variable. Global fetch ships with Node 18 and later, so there is nothing to install.

import { readFile, writeFile } from 'node:fs/promises';

const rows = JSON.parse(await readFile('results.json', 'utf8'));
let seen = new Set();
try {
    seen = new Set(JSON.parse(await readFile('seen-postings.json', 'utf8')));
} catch {
    // first run: every posting counts as new
}

const fresh = rows.filter((row) => !seen.has(row.posting_id));
const lines = fresh.map((row) => {
    const where = [row.location_name, ...row.secondary_locations].join(' / ');
    const pay = row.compensation_summary ?? 'pay not listed';
    return `${row.org_slug}: ${row.title} (${row.team_name ?? 'no team'}, ${where}, ${pay}) ${row.apply_url}`;
});
console.log(lines.length ? lines.join('\n') : 'No new postings since the last run.');

if (fresh.length && process.env.WEBHOOK_URL) {
    await fetch(process.env.WEBHOOK_URL, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ text: lines.join('\n') }),
    });
}
await writeFile('seen-postings.json', JSON.stringify(rows.map((row) => row.posting_id)));

❓ FAQ

Can I trigger a run from a webhook or a queue?

Yes. Import the client call into the handler and await it. For long slug lists, start the Actor with start() instead of call(), return right away, and let an Apify webhook tell your endpoint when the dataset is ready.

Does it include closed or historical postings?

No. It reads the board Ashby serves to candidates at the moment of the run, so a role that closed yesterday is simply absent. History is something you build by storing each run's rows.

How do I avoid paying for rows my app throws away?

Move filtering into the input. locationFilter and maxResultsPerOrg take effect before rows are written, and charges are per written row, so a filtered run is cheaper than filtering in your own code afterwards.

What happens when a company leaves Ashby?

Its slug starts returning no rows. Track row counts per org_slug in your service; a board that drops to zero overnight means either a hiring freeze or a move to a different applicant tracking system, and both are worth a look.

🧯 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 JavaScript and Node.js: fetch the full job board of any Ashby-hosted employer, with teams, pay bands and apply URLs, for job boards, webhooks and backends. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages