# Career Site Jobs: Greenhouse, Lever, Ashby, Workable, Workday (`dataforge-tools/career-site-jobs`) Actor

Every open job from any company's Greenhouse, Lever, Ashby, Workable or Workday careers page as clean, normalized JSON: salary ranges where published, new/existing/removed tracking between runs. Reads only the public job-board JSON the careers pages load themselves.

- **URL**: https://apify.com/dataforge-tools/career-site-jobs.md
- **Developed by:** [DataForge](https://apify.com/dataforge-tools) (community)
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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

## Career Site Jobs: Greenhouse, Lever, Ashby, Workday & more, with daily change tracking

Name the companies you care about and get **every open job on their careers page** as clean, consistent JSON. Each run also tells you **what changed since the last run**: new jobs, jobs still open, and jobs that were removed.

- **5 applicant-tracking systems (ATSs):** Greenhouse, Lever, Ashby, Workable and Workday (`myworkdayjobs.com`, used by many large employers). Paste a careers-page URL and the Actor detects the ATS for you.
- **Data comes straight from the source, in real time:** the Actor reads the employer's official public job-board API during your run. There is no stale index and no lag of several hours.
- **One schema for every ATS:** locations as an array, remote/hybrid/onsite, employment type, ISO dates, and a short plain-text description excerpt with a link to the full posting.
- **Salary ranges as structured numbers** (`min`, `max`, `currency`, `interval`) wherever the employer publishes them. These come from the ATS's own pay-transparency fields, not AI guesses. Workday has no pay field, so there the range is read from the posting text when the employer states one ("The base salary range is 272,000 USD - 431,250 USD").
- **Change tracking built in:** every record has `changeStatus` (`new` or `existing`) and `firstSeenAt`. Turn on **Only new jobs** to get just the new jobs on a daily schedule. The run summary lists jobs that were removed.
- **Reliable and polite:** timeouts, retries with backoff, per-host rate limits, and each API host's robots.txt (including any Crawl-delay) is checked and obeyed during the run. A wrong company name is reported in the summary and the run carries on with the rest.

### Use cases

- **Job aggregators and job boards:** feed hundreds of employer boards into your site every day. Ingest only new postings and expire the removed ones.
- **Recruiters and sourcers:** watch target companies and get alerted when they open roles that match your keywords and locations.
- **Market and competitive research:** follow hiring velocity by department, location and seniority, see where competitors are expanding, and benchmark published pay ranges.
- **Sales intelligence:** hiring is a buying signal. Find companies opening "Data Engineer" or "Security" roles this week.
- **Job seekers and communities:** a personal daily digest of new remote roles at the companies you love.

### How to use it

1. Add companies to **Companies**, one per line. Any of these forms works:
   - `https://boards.greenhouse.io/figma` or `https://job-boards.greenhouse.io/figma` or `greenhouse:figma`
   - `https://jobs.lever.co/zoox` or `lever:zoox` (EU tenants: `https://jobs.eu.lever.co/<name>`)
   - `https://jobs.ashbyhq.com/ramp` or `ashby:ramp`
   - `https://apply.workable.com/huggingface` or `workable:huggingface`
   - `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` or `workday:nvidia.wd5/NVIDIAExternalCareerSite` (Workday needs the full URL: tenant, `wd` number and career-site name; a company can have several career sites, each is one entry)
2. Add filters if you want them: title keywords, locations, remote only, posted within N days, max jobs per company.
3. For daily monitoring, schedule the Actor (for example every morning) with **Only new jobs** turned on.

> **Company on a custom domain** (for example `careers.acme.com`)? Click any job link on their site. It usually points to one of the hosts above, and that gives you the `ats:slug`.

#### Input example

```json
{
  "companies": [
    "https://boards.greenhouse.io/figma",
    "https://jobs.lever.co/zoox",
    "https://jobs.ashbyhq.com/ramp",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "workable:huggingface"
  ],
  "keywords": ["engineer", "product manager"],
  "locations": ["New York", "Remote"],
  "remoteOnly": false,
  "postedWithinDays": 30,
  "onlyNew": true,
  "includeDescription": true,
  "maxJobsPerCompany": 200
}
```

| Field | Default | What it does |
|---|---|---|
| `companies` | 4 sample companies | Careers-page URLs or `ats:slug` entries. |
| `keywords` | none | Keeps jobs whose **title** contains any of the keywords (case-insensitive). |
| `locations` | none | Keeps jobs where any location contains any of these texts (case-insensitive). |
| `remoteOnly` | `false` | Keeps only remote jobs. |
| `postedWithinDays` | any | Keeps jobs published in the last N days. Jobs with no date are dropped when this is set. |
| `onlyNew` | `false` | Outputs only jobs that earlier runs with the same companies and filters have not seen. |
| `includeDescription` | `true` | Adds `descriptionExcerpt`: the first 400 characters of the description as plain text. Full descriptions and HTML are never output; follow `url` for the complete posting. |
| `maxJobsPerCompany` | `20` | Keeps the newest N matching jobs per company. The default keeps a first run (and an API call that omits the field) to cents; set a larger number, up to 100,000, for everything. |

### Output

Every dataset item has the same shape, whichever ATS it came from:

```json
{
  "id": "greenhouse:figma:6211910004",
  "ats": "greenhouse",
  "company": "Figma",
  "companySlug": "figma",
  "jobId": "6211910004",
  "title": "Technical Program Manager, AI Tooling",
  "department": "Business Operations",
  "team": null,
  "locations": ["San Francisco, CA", "New York, NY", "United States"],
  "isRemote": false,
  "workplaceType": null,
  "employmentType": null,
  "compensation": {
    "min": 169000,
    "max": 245000,
    "currency": "USD",
    "interval": "year",
    "raw": "Annual Base Salary Range: USD 169,000 - 245,000"
  },
  "postedAt": "2026-10-02T14:59:12.000Z",
  "updatedAt": "2026-10-02T14:59:12.000Z",
  "url": "https://boards.greenhouse.io/figma/jobs/6211910004?gh_jid=6211910004",
  "applyUrl": "https://boards.greenhouse.io/figma/jobs/6211910004?gh_jid=6211910004",
  "descriptionExcerpt": "Figma is growing our team of passionate creatives and builders on a mission to make design accessible to all. Born on the Web, Figma helps entire product teams brainstorm, design and build better products — from start to finish. Whether it’s consolidating tools, simplifying workflows, or collaborating across teams and time zones, Figma makes the design process faster, more efficient, and fun while keeping everyone on the…",
  "firstSeenAt": "2026-10-04T05:32:43.000Z",
  "changeStatus": "new",
  "scrapedAt": "2026-10-04T05:32:43.797Z"
}
```

Field notes:

- `id` is stable across runs (`ats:company:jobId`), so you can use it as a primary key.
- `employmentType` is one of `full-time`, `part-time`, `contract`, `internship` or `temporary`, or the employer's own wording, or `null` when the ATS doesn't say.
- `workplaceType` is `remote`, `hybrid`, `onsite` or `null`. `isRemote` is true when the employer marks the job remote or a listed location says "Remote".
- `compensation` is `null` when the employer publishes no range. `interval` is `year`, `month`, `week`, `day` or `hour`. When an employer publishes several pay zones, `min` and `max` hold the first zone and `raw` lists all of them.
- `company` is the employer name when the API provides one (Greenhouse, Workable). Lever, Ashby and Workday give none, so it is the board name in title case (`Zoox`, `Ramp`, `Nvidia`); `companySlug` keeps the exact board name.
- Workday records: `jobId` is the requisition id from the posting URL (for example `JR2019127`; the title slug in front of it is not part of the id, so a renamed posting keeps its `id`), `department`/`team` are not published by the endpoint and are `null`, `postedAt` is the posting date (no time of day), `applyUrl` is the posting URL plus `/apply`.

The dataset's **Overview** view shows the key columns as a table, and the **With salary** view focuses on pay data. Export to JSON, CSV, Excel, or use the API or integrations.

#### Run summary (`OUTPUT` record)

Each run writes a summary to the default key-value store under the key `OUTPUT`:

```json
{
  "totals": { "companiesRequested": 8, "companiesSucceeded": 6, "companiesFailed": 2, "jobsFetched": 1276,
              "jobsMatched": 460, "jobsOutput": 104, "newJobs": 104, "existingJobs": 0, "removedSinceLastRun": 0, "withCompensation": 67 },
  "companies": [ { "input": "greenhouse:figma", "status": "ok", "jobsFetched": 162, "jobsOutput": 25, "newJobs": 25,
                   "existingJobs": 0, "removedSinceLastRun": 0, "removedJobIds": [], "truncated": true, "durationMs": 674 } ],
  "errors": [ { "input": "greenhouse:this-company-does-not-exist-xyz", "message": "Greenhouse board \"this-company-does-not-exist-xyz\" not found (HTTP 404). Check the board token in the careers URL." } ],
  "chargeLimitReached": false
}
```

`removedJobIds` lists jobs that were open at the last run and are gone now. They were probably filled or closed.

### Change tracking: how it works

- History is kept in a named key-value store, `career-site-jobs-state`, in your account. It is keyed by a hash of your **companies + filters**. Changing `onlyNew`, `includeDescription` or `maxJobsPerCompany` keeps the history. Changing companies or filters starts a new one.
- The first run treats every job as `new`. Later runs mark a job `new` only if it wasn't seen before.
- A job that disappears and comes back within 90 days is not counted as new again.
- If a run stops early because it hit your **maximum cost per run**, jobs you didn't receive stay `new` for the next run. Nothing is lost.
- If a company fails to load, its history is left untouched.

### Pricing

Pay-per-event: you pay only for the jobs you receive (event `job`, one per dataset item).

**Suggested price: $1.00 per 1,000 jobs.** The sample run (4 companies, at most 20 jobs each) costs under $0.10; the same companies without a limit (about 2,500 jobs) about $2.50. With **Only new jobs** on, a daily run over the same companies usually costs cents, because you pay only for new postings. Set a maximum cost per run in Apify and the Actor stops cleanly when it reaches it.

### FAQ

**Which companies are supported?** Any company whose careers page runs on Greenhouse, Lever, Ashby, Workable or Workday. That covers many thousands of companies, from startups to the largest enterprises. iCIMS, SuccessFactors, Taleo and other ATSs are not supported yet. SmartRecruiters is deliberately not supported: its robots.txt disallows automated access and its job API requires a key, so a SmartRecruiters URL is reported as unsupported in `OUTPUT.errors`. Recruitee is not supported either: from 10 February 2027 its job API needs a token only the employer can issue, so Recruitee URLs are reported as unsupported too.

**How fresh is the data?** Live. Every run reads the employer's job board directly.

**How fast is it?** Seconds per company for Greenhouse, Lever, Ashby and Workable (one request per board). Workday lists 20 jobs per request and needs one more request per job you receive, so a large Workday site without a limit takes a few minutes on the first run and only seconds afterwards with **Only new jobs** on. The sample input finishes in about 5 seconds.

**Anything special about Workday?** Give the full careers URL (`https://<tenant>.<wdN>.myworkdayjobs.com/<CareerSite>`); the Actor cannot guess the `wd` number. A company with several career sites (for example Salesforce has `External_Career_Site` and `Slack`) needs one entry per site. Title keywords are applied to the list, so they cost no extra requests; location, remote and "posted within more than 30 days" filters need the job details first and cost one request per listed job.

**What happens with a wrong company name?** You get a warning, an entry in `OUTPUT.errors`, and the run continues with the other companies. The run is marked failed only if *no* company could be fetched.

**Why is `compensation` empty for some jobs?** The employer didn't publish a pay range in their ATS (or, for Workday, in the posting text). The Actor never guesses salaries.

**Can I get only remote jobs, or only jobs from the last week?** Yes: `remoteOnly: true` and `postedWithinDays: 7`.

**Does it work with proxies or need login?** Neither. Only public, unauthenticated job-board APIs are used.

### Legal

This Actor uses only the employers' **public job-board APIs**, which ATS vendors provide so that job postings can be displayed and distributed, and for Workday the public JSON that the employer's careers page itself loads. It does not log in, bypass access controls, solve CAPTCHAs or scrape HTML pages, and it obeys each host's robots.txt. It collects only what employers publish in their job posts, and it republishes job descriptions only as a short excerpt (400 characters) with a link to the employer's own posting, so the full text stays with its owner. You are responsible for using the data in line with applicable laws and the terms of the sources. For example, if a job description contains a recruiter's name or email, treat it as personal data. See [LEGAL.md](./LEGAL.md) for the sources and their terms.

# Actor input Schema

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

One entry per company. Accepts careers-page URLs - e.g. https://boards.greenhouse.io/figma, https://job-boards.greenhouse.io/stripe, https://jobs.lever.co/zoox, https://jobs.ashbyhq.com/ramp, https://apply.workable.com/huggingface, https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite - or the short form 'ats:slug' (greenhouse:figma, lever:zoox, ashby:ramp, workable:huggingface, workday:nvidia.wd5/NVIDIAExternalCareerSite). The ATS is detected automatically from the URL. Workday needs the full careers URL (tenant, wd-number and career-site name); a company can have several career sites, each is one entry. Companies on a custom domain: open one of their job links to find the ATS and slug. SmartRecruiters is not supported (its robots.txt disallows automated access and its API requires a key). Recruitee is not supported (its API requires an employer token from February 2027).

## `keywords` (type: `array`):

Keep only jobs whose title contains at least one of these words or phrases (case-insensitive), e.g. 'engineer', 'product manager'. Leave empty to keep all titles.

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

Keep only jobs with at least one location containing one of these texts (case-insensitive substring), e.g. 'New York', 'London', 'Germany', 'Remote'. Leave empty for all locations.

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

Keep only jobs the employer marks as remote (or whose location says Remote/Anywhere).

## `postedWithinDays` (type: `integer`):

Keep only jobs published (or, if the ATS gives no publish date, updated) within this many days. Jobs with no date at all are excluded when this is set. Leave empty for any date.

## `onlyNew` (type: `boolean`):

Output only jobs that were not seen in previous runs with the same companies and filters. Ideal for a daily schedule: the first run outputs everything, later runs only what is new. History is kept in a named key-value store, and the OUTPUT record also lists jobs removed since the last run. Every record carries changeStatus ('new' or 'existing') and firstSeenAt either way.

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

Add descriptionExcerpt: the first 400 characters of the job description as plain text. The Actor never outputs full descriptions or HTML (employers own that text); follow url for the complete posting. Turn off for smaller, faster output.

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

Upper limit of jobs per company after filtering, newest first. Default 20, so a first run or an API call without this field costs cents; set a larger number (up to 100000) for more. For Workday, every output job costs one extra request, so a large site without a tight limit takes minutes on the first run.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/figma",
    "https://jobs.lever.co/zoox",
    "https://jobs.ashbyhq.com/ramp",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "keywords": [],
  "locations": [],
  "remoteOnly": false,
  "onlyNew": false,
  "includeDescription": true,
  "maxJobsPerCompany": 20
}
```

# Actor output Schema

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

The jobs found in this run, one item per record (the default dataset).

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

Totals, per-input status and any errors for this run (the OUTPUT record).

# 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://boards.greenhouse.io/figma",
        "https://jobs.lever.co/zoox",
        "https://jobs.ashbyhq.com/ramp",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dataforge-tools/career-site-jobs").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://boards.greenhouse.io/figma",
        "https://jobs.lever.co/zoox",
        "https://jobs.ashbyhq.com/ramp",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    ],
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dataforge-tools/career-site-jobs").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://boards.greenhouse.io/figma",
    "https://jobs.lever.co/zoox",
    "https://jobs.ashbyhq.com/ramp",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "maxJobsPerCompany": 20
}' |
apify call dataforge-tools/career-site-jobs --silent --output-dataset

```

## MCP server setup

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

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/4yQe990jcsL0lW2Om/builds/nZC6ilYxKUK41um3q/openapi.json
