# Greenhouse, Lever, Ashby & Workday Jobs API | $1/1K (`siftsmith/company-jobs`) Actor

Open jobs from a company's own job board in one schema or as an XML job feed for job boards. ATS: Greenhouse, Lever, Ashby, Workday, Workable, Breezy HR, Personio, Rippling. Give a name, domain or careers URL (Ashby/Rippling: board URL). $0.001 per job; companies not found or unconfirmed are free.

- **URL**: https://apify.com/siftsmith/company-jobs.md
- **Developed by:** [Siftsmith](https://apify.com/siftsmith) (community)
- **Categories:** Jobs, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 job rows

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

## Greenhouse, Lever, Ashby & Workday Jobs API

Get a company's open jobs straight from its own job board, in one schema across 8 applicant tracking systems (ATS): Greenhouse, Lever, Ashby, Workday and 4 more. Give a company name, a domain or a careers URL (for Ashby, the board URL or `ashby:<slug>`); mix them freely in one run.

**$1 per 1,000 job rows ($0.001 each), no subscription. Companies not found are free.** You need no ATS account, API key or proxy: the jobs come from each company's public job board. Run it from the Apify Console, the Apify API or an AI agent.

### Who it's for

- **Recruiters and staffing agencies** tracking openings at a list of client or target employers.
- **Job boards and aggregators** that need fresh listings from company career pages in one format.
- **Sales and RevOps teams** who want to see which target accounts are hiring, and for which roles, before reaching out.
- **Analysts and investors** following hiring across a portfolio or a market.

### Paste your whole employer list

Put every company in `companies`, one per line: names, domains, careers URLs or rows pasted from a spreadsheet. There is no cap on the length of the list and no keyword limit, and one run covers it all.

- **A count for every company:** each company gets one free status row with `found`, the ATS and board, `openJobs` and the jobs posted in the last 30 days, so you can see at a glance which employers on your list are hiring and which have no board we can read.
- **A cap per company:** `maxJobsPerCompany` keeps one big employer from filling the run, and `maxItems` caps the whole run's spend.
- **Give domains, not just names:** many companies' job boards don't use their brand name (Glean's Greenhouse board is `gleanwork`). A domain also avoids another company with the same name: the name "Motive" matches a creative agency's board, not Motive's (`gomotive`). If you already know a company's board, pass it (`greenhouse:gleanwork`) and it's read as given. An Ashby board is matched only when you give it, as `ashby:<slug>` or its URL (Clay's is `ashby:claylabs`): a board found on Ashby from a name or domain comes back as a free low-confidence status row that names the board to pass.
- **Paste the careers page URLs you already have:** a link like `stripe.com/careers` or `figma.com/careers` is read as that company, matched by its domain. Some companies use a board name the link doesn't lead to; for those, pass the board: `greenhouse:tripactions` for Navan, `greenhouse:talkdesk2` for Talkdesk, `ashby:checkout.com` for Checkout.com, or a Rippling board's URL.
- **Not found is free:** a company with no board on the 8 supported ATSes costs nothing, and its status row says why.

### Quick start

```json
{ "companies": ["stripe.com", "gong.io", "https://jobs.lever.co/spotify"] }
```

You get one row per open job at each company, plus one free status row per company. Workday jobs are found by company name or domain too (see below), or give the board URL, for example `https://acme.wd1.myworkdayjobs.com/External`.

> **Using CSV, Excel or Google Sheets? Filter the `type` column to `job`.** Every run writes two kinds of row to one dataset: job rows (`type` = `job`) and one status row per company (`type` = `company`). Status rows have empty job columns. The Console's **Jobs** and **Companies (status)** views pick the columns for each, but an export or API call still includes both kinds of row, so filter on `type`.

### What you get: one schema for ATS jobs

- **One row per open job** (`type` = `job`): title, location (on Workday, also `locations` and `locationsCount` for a job posted in several places), remote, department, posted date with `postedAtPrecision` (`exact`; `day` for Workable and Workday dates; `fallback-updated` when Greenhouse gives only the last-updated time), job URL, the ATS and board it came from, and a stable `globalId` (`ats:slug:jobId`). A field the board doesn't give is empty, never guessed.
- **`country`, `countries`, `countrySource`**: `country` is an ISO code (`US`, `GB`) set only when the job board's own country field or the location clearly names one country for every place of the job (`London, United Kingdom`, `Remote - US`, `United States (Remote)`). A bare city (`Dublin`) or a US state (`San Francisco, CA`) gives no country unless the board's own field names one. A region (`Europe`, `EMEA`, `North America`) or a bare `Remote` gives none, even when the board's field names a country (that is often the company's head office). The field is also not used when the location text clearly names a different country (`Mumbai, India`). An unclear country is left empty, never guessed. `countries` lists every country of a job posted in several countries (`Remote, Canada; Remote, United States` gives `CA`, `US`) and is empty when any of its places names none. `countrySource` says where it came from: `ats` (the board's own country field) or `location` (read from the location text).
  - **How often it's filled (measured 2026-10-11 on 82 job boards from the 2025 Forbes Cloud 100, 10,329 jobs):** 64% of jobs have a `country`. By ATS: Ashby 88% (31 boards), Greenhouse 50% (44), Workday 33% (5); Rippling 49% comes from one board. The 82 include one SmartRecruiters board (100%), which we no longer read (see Supported ATS job boards). Greenhouse, Workday and Rippling locations often name only a city or a US state, so those jobs get no country. A second count, written separately and run on the raw job board APIs, matched within 0.1 point for every ATS (for Rippling, on the location text alone), and a hand check of 100 jobs found no wrong country. On 2026-10-12 locations such as `United States (Remote)`, `Ireland - Dublin` and `London (UK)` started giving a country; two separate counts over the same jobs gave the figures above (64.4% overall), with no job moved to a different country. Lever, Workable, Breezy HR and Personio aren't measured yet: no board in the sample uses them.
- **`boardJobUrl` (Greenhouse jobs)**: the job's own page on Greenhouse, with the title, description and apply form. On many Greenhouse boards, `url` is the company's careers page (`?gh_jid=`), and some of those pages show the job only after a script runs or refuse requests that don't come from a browser, so a link checker or job feed gets no job from them. `boardJobUrl` opens the job on Greenhouse for as long as it is posted; once the job closes, the link redirects to the company's job list on Greenhouse. It is empty on the other ATSes, where `url` is already the job's page on the ATS.
- **Match confidence on every job row**: `explicit` (you gave the board), `high` or `medium`, plus `matchedBy`: `board` (you gave it), `name` (from the company's name or its domain's name) or `site` (a board whose name only partly matches, kept because the company's own site links it or lists its jobs). A job from the wrong company is worse than no job, so you can see the match and filter on it.
- **One free status row per company** (`type` = `company`): found or not, why not, the board to pass explicitly next time, the board size, and hiring signals (jobs posted in the last 7, 30 and 90 days, a hiring score, top departments and technologies). An input whose board is already in the run (`Stripe` and `stripe.com`) gets a free row with `found: true`, `duplicateOf` and no jobs, so each job is delivered and charged once.

### Example output

A job row from a run on `stripe.com` (abridged; read 2026-10-08):

```json
{
  "type": "job",
  "globalId": "greenhouse:stripe:8172508",
  "title": "Abuse Investigator",
  "companyName": "Stripe",
  "companyDomain": "stripe.com",
  "ats": "greenhouse",
  "boardUrl": "https://boards.greenhouse.io/stripe",
  "matchConfidence": "high",
  "matchedBy": "name",
  "url": "https://stripe.com/jobs/search?gh_jid=8172508",
  "boardJobUrl": "https://job-boards.greenhouse.io/embed/job_app?for=stripe&token=8172508",
  "location": "Dublin",
  "remote": false,
  "department": "Security Analytics",
  "postedAt": "2026-09-03T13:32:53-04:00",
  "postedAtPrecision": "exact"
}
```

The same run's free status row for Stripe said `found: true`, `openJobs: 727` and `postedLast30d: 288`, with the top departments and technologies.

### Supported ATS job boards (8): Greenhouse, Lever, Ashby, Workday and more

- **Matched from a name or domain:** Greenhouse, Lever, Workable, Breezy HR, Personio.
- **Board needed for Ashby:** an Ashby board is read only from `ashby:<slug>` or its URL. One found from a name or domain comes back as a free status row naming the board to pass.
- **Workday by company name: postings checked first; unconfirmed sites are free.** A name or domain with no confirmed board on the other six ATSes, and no Ashby board found for it, is also looked up on Workday: the company's Workday site is found from its public robots.txt, and a few of its postings are read to check they name this company as the employer (or, for a domain, that the domain's own site links the Workday site or its postings give the domain). Only then is it matched (`medium`, or `high` for a domain tie). A Workday site that exists under the name but isn't shown to be this company ("Liberty" finds Liberty University's) comes back as a free status row naming the site, never charged. Known gaps: companies whose Workday name differs from their brand (Johnson & Johnson is `jj`), domains whose homepage doesn't link the Workday site in its HTML, and only the main public site is read. A single common word can match another organisation's Workday site when its postings use the same name: "Prudential" matches Prudential plc's site for Asia and Africa at `medium`, not Prudential Financial's. Check `workdayEvidence` on name-only matches, give the domain with the name and set `minMatchConfidence` to `high` so a Workday match is charged only when the domain confirms it (this also drops medium matches on the other ATSes), or pass the board URL, which always works.
- **Board URL needed for Rippling:** Rippling is never guessed. Give the board URL or `rippling:slug`. A Workday board URL (`https://acme.wd1.myworkdayjobs.com/External`) or `workday:tenant:wdN:site` is read as given.
- **Workday remote:** `remote` follows the employer's own work type on Workday when the job list sets one (some employers set it only on the job's own page, and that page's work type is used only for jobs in several places or with no place): Remote is remote. Any work type that mentions hybrid ("Hybrid", "Hybrid (Remote & On-site)", "Remote or Hybrid", "Remote/Hybrid") is not, and neither is an on-site, office or field work type, even when the place says "Remote - US". Otherwise the place decides, and a place that says hybrid doesn't count as remote.
- **Workday jobs in several places, or with no place in the list:** Workday's job list shows only "5 Locations" for a job posted in several places, and some employers' lists show no place at all. For these, each job's own page is read for the real places. `location` then lists them all, `locations` has them one per item and `locationsCount` counts them. Each such job takes one extra request, made one at a time, and only for jobs the run can return (with descriptions on, every Workday job's page is read this way; see Descriptions below). If a job page can't be read, or Workday asks us to slow down or stop, `location`, `locations` and `remote` are empty, and `locationsCount` still gives the number of places when the list gave one. A Workday job in one place has its place in `location`, and `locations` and `locationsCount` are empty, as on every other ATS. With `locationKeywords` or `remoteOnly`, a job whose places couldn't be read is left out (and not charged), and job pages stop being read once `maxJobsPerCompany` or `maxItems` is reached or the run nears its timeout. The status row counts the jobs left out this way in `jobsLocationUnchecked`; when the reading stopped early, it also says `jobsTruncated`, `jobsAvailable` is a minimum, and `reason` says what stopped it (`sourceBlocked` when Workday did).
- **Up to 1,000 jobs per Workday board:** Workday lists jobs page by page, and a run reads at most 1,000 from each board. A bigger board's status row says `jobsTruncated` with `reason` `sourceCap` and gives the board's own total in `jobsAvailable` (on some Workday boards the total Workday reports stops at 2,000, so there `jobsAvailable` can say 2,000 for a bigger board). Rippling reads up to 1,000 listings (a job is listed once per place), and its status row has no board total.
- **Any of the 8 by name:** `greenhouse:stripe`, `lever:palantir`, `ashby:ramp` and so on skip matching.
- **SmartRecruiters links are recognised, not read:** SmartRecruiters asks automated tools not to read its job API (robots.txt), so we don't. A SmartRecruiters board, link or `smartrecruiters:` entry gets one free status row that says so (`notRead`), with no job rows.

Other systems (iCIMS, Taleo, SuccessFactors, Oracle and the rest) are not supported. A company with no board found on the 8 gets a status row saying so, free.

### Input

| Field | What it does |
|---|---|
| `companies` (required) | Names, domains, careers URLs or job board URLs, one company per line. An entry that holds several lines is read as one company per line. A pasted spreadsheet row is looked up by, in order: a job board in any field (a board URL or `greenhouse:acme`); a website or work email that agrees with the company name; otherwise the first field, skipping row numbers and company pages on directories (LinkedIn, Crunchbase). So `1; Adyen; Amsterdam; adyen.com` is looked up as adyen.com. A row with no website, email or job board whose first two fields are each one word written like a name (`Jane`, `Doe`; not an acronym like `IBM` or a word like `Corp` or `Labs`) reads as a person's name. A row whose first field is a common US first name, in any letter case, also reads as a person's name, even when the next field is an acronym or several words: `David; IBM` is free, and `TAYLOR; BROWN; ACME CORP` is looked up as ACME CORP. This also frees a company named like a person, such as `Harper; San Francisco`. A row whose second field is a common US first name after a one-word surname also reads as a person's name, in any letter case: `FERGUSON; JOHN; IBM` is free, and `parsons; mary; acme corp` is looked up as acme corp. This also frees a company whose second field is a city or other word that is also a first name, such as `TESLA; AUSTIN`. A row of three or more fields whose first field is one word and whose second field is empty or reads like a given name, initials, titles, suffixes and nicknames included (`Dr. John`, `John Q. Jr.`, `John (Jack)`), is never looked up by that first field, whatever the given name: `Ferguson; John Q.; IBM` and `Stevens;; IBM` are free. A company row of that shape, such as `IBM;; Armonk` or `STRIPE; DUBLIN; FINTECH`, is free too, unless a later field is the same name plus a legal form or company word: `STRIPE; DUBLIN; STRIPE INC` is looked up as STRIPE. A person's row is looked up only by a later field of two or more words that ends in a legal form or a company word such as Inc, Corp, Ltd, GmbH, Group, Technologies or AI: `Jane; Doe; Acme Corp` is looked up as Acme Corp. Otherwise the row is free: `Jane; Doe; Stripe`, `Jane; Doe; London`, `Stripe; Dublin; Fintech` and `Stripe; Plaid; Ramp` read the same way. So is a two-field company row like `Adyen; Amsterdam` or `Goldman; Sachs`. So is a row whose first field is one word written like a name followed by a company field (two or more words ending in a legal form or company word, as above), unless that field is the same name plus those words: `David; Acme Corp` and `Notion; Software Company` are free; `Stripe; Stripe Inc` and `Notion; Notion Labs` are looked up as Stripe and Notion. A LinkedIn profile link (`linkedin.com/in/…`) also makes a row a person's row, looked up only by a company field: `David; linkedin.com/in/dsmith; Acme Corp` is looked up as Acme Corp. Add the company's website to have such a row looked up. If the line also has a company website or work email, a job board found by that name counts only when it ties to that website: the board lists it, the board's jobs link to it or mention it, or the website links the board. Otherwise the line is free: `Plaid, adyen.com` is free, because Plaid's board doesn't point to adyen.com. A line looked up by its name that has no website, only a personal email address (Gmail, Outlook, Yahoo and the like), is free too, because nothing in it confirms the name is a company: lead lists often put a person's first name first. So `David, Smith, david@gmail.com` and `Stripe, jane@gmail.com` are both free; add the company's domain (`Stripe, jane@gmail.com, stripe.com`) and the line is looked up by that domain. Names like `Acme, Inc.` contain a comma, so plain commas split a row the same way only when it has a job board, a website or work email that agrees with the name, or a name first and a website or work email after it: `Alphabet, abc.xyz` is looked up as Alphabet, while `Acme, Inc.` stays one name. |
| `minMatchConfidence` | `medium` (default) or `high`. A board below it returns no jobs and costs nothing; its status row gives the board to pass explicitly. A low-confidence match is never charged. |
| `titleKeywords`, `excludeTitleKeywords`, `locationKeywords`, `departmentKeywords` | Case-insensitive "contains any" filters. `locationKeywords` matches the `location` text; for a Workday job in several places that is all of its places. |
| `remoteOnly` | Remote jobs only. A job whose remote status is unknown (`remote` empty) is left out. |
| `countries` | Jobs in any of these countries: ISO codes (`US`, `DE`) or English names (`Germany`). A job is kept when one of its `countries` is in the list. A job whose country is unknown is left out and not charged; each company's status row counts the jobs left out in `countryUnknown` (no country known) and `countryOther` (another country). An entry that isn't a country stops the run before anything is looked up or charged. |
| `postedWithinDays` | Jobs posted in the last N days. Jobs with no date are kept unless `requirePostedDate` is on. Workday's job list says only "Posted 30+ Days Ago" for older jobs; those jobs get their date from the job's own page, which is read for every Workday job when descriptions are on, and otherwise only for a job in several places or with no place (and not when `requirePostedDate` is on). A Workday job whose page isn't read has no date. |
| `requirePostedDate` | With `postedWithinDays`, also drops jobs that have no posted date. With descriptions off, that includes every Workday job listed as "Posted 30+ Days Ago", because its page isn't read for a date. With descriptions on, a Workday job is dropped this way only when its page couldn't be read in that run or gave no valid date. |
| `maxJobsPerCompany` | Default 1,000 (the Console form starts at 10 for a quick first run). Jobs past it are not returned or charged. |
| `maxItems` | Cap on job rows, and so on spend, for the whole run. |
| `includeDescription` | Adds the description text on Greenhouse, Lever, Ashby, Workable, Rippling and Personio, and on Workday, where it is read from each job's own page, one job at a time per employer and for up to 15 minutes per employer per run, and also gives the job its posted date. Workday descriptions have contact details removed by pattern (see Descriptions below). Off by default to keep rows small; always on with `feedOutput`. |
| `maxConcurrency` | Companies processed in parallel (default 5, up to 20). |

### Job feed for job boards

Turn on `feedOutput` and each run also keeps an XML job feed at one URL that doesn't change. Paste that URL into your job board software's XML import, schedule the Actor daily, and the board stays in step with your employer list. Jboard, Job Boardly, JobBoard.io, SmartJobBoard, Niceboard and Cavuno can import a feed from a URL. Cavuno includes recurring XML feeds on every plan, from $39 a month, and checks the URL every 1, 3, 6, 12 or 24 hours. Its docs say it accepts a flat `<source><job>` feed like this one and uses a job's reference number as its identity, so map `referencenumber` to that field on the first import. The others offer it only on some plans: Jboard's XML importers are on its Advanced plan ($449 a month), and Niceboard's Core plan allows 10 import runs a month, which is too few for a daily refresh. Jboard and JobBoard.io only accept jobs that have a description (see Descriptions below). JobBoard.io wants posting dates as MM/DD/YYYY, and the feed's `date` is in RFC 1123 form (`Mon, 12 Oct 2026 08:00:00 GMT`), so leave the date unmapped there; JobBoard.io then uses the import time.

- **Where the URL is:** in the run's `OUTPUT` record (`feedUrl`, with the number of `jobs`), its status message and its log. The feed is the same URL on every run with the same key.
- **What's in it:** job rows only, never the status rows, in one Indeed-style layout: `title`, `company`, `url`, `referencenumber` (the job's `globalId`, a stable unique ID), `date`, `location`, `country`, `remote`, `department`, and `description` (always on with `feedOutput`, where the ATS gives one). A field the board leaves empty is left out. `company` is the name the board gives; when it gives none (Ashby boards never do), it is the company as you typed it, but only if that is a plain name or domain (`Stripe`, `stripe.com`). For a board, a URL, an email or a pasted row it is left out, and `OUTPUT` counts those jobs in `jobsWithoutCompany`, with a warning in the log. To fill it, set the company in your job board's import, or, for a board not on Ashby, give the company's name or domain on its own line instead. `date` is left out when the board gives no posted date, or gives only a last-updated time.
- **How it updates:** each run keeps the jobs that are still open, drops the ones that closed, and adds the new ones. A company whose lookup failed or was skipped keeps its jobs until a later run reads its board. A company that is not found, or found below your match confidence, keeps its jobs for up to 7 days after they were last seen, so one missed run doesn't remove them; after that they leave the feed. A company you remove from the list, or that is now found on a different board, leaves the feed at once. On a board past the source's 1,000-job limit (large Workday boards), a job beyond that limit can't be checked, so it stays for up to 30 days after it was last seen. It works with or without `onlyNewJobs`, and writing it costs nothing extra: the feed holds job rows already charged. With `onlyNewJobs`, a daily run charges only the jobs that are new since the last run, and the feed still keeps every one that is open.
- **Keys:** give each list its own `feedKey`. With `onlyNewJobs`, the feed uses the `stateKey` instead (leave `feedKey` empty or set it to the same value). Otherwise, without a `feedKey`, a saved task uses its task ID and other runs use `default`. **Use a new key when you change the filters:** jobs already in the feed stay while they are open, even if a new filter would leave them out. If you turn the feed on for a `stateKey` that earlier runs already used, the jobs those runs returned aren't in the feed; use a new `stateKey` to start with every open job.
- **Anyone with the URL can read the feed.** The URL is also in the run's log and status message, so share a run only with people who may read the feed. It needs no Apify token and gives access to that one record only, and the jobs in it are already public on the employers' own boards. To retire a URL, use a new key, then delete the old key's two records (`<key>.xml` and `<key>.json`) from the key-value store `company-jobs-feed` (Apify Console, Storage, Key-value stores). The old URL stops working once its record is deleted; don't reuse that key, or the old URL works again.
- **Storage:** the feed is kept in that key-value store in your own Apify account, which outlives the runs and counts toward your account's storage.
- **Size:** the feed keeps every job that a run under this key delivered, for as long as the job stays open; with `onlyNewJobs` that grows to every open job on the listed boards that passes your filters. `maxJobsPerCompany` and `maxItems` limit what one run adds, not the feed. The form starts `maxJobsPerCompany` at 10, so clear it for full boards. Jboard's first import of a new feed takes the first 1,000 jobs and brings in the rest on its next daily run; on a Jboard trial, an importer stops at 1,000 jobs. Descriptions are always included in full, so a job is about 6.5 KB of XML and the feed holds about 1,200 jobs within its 8 MB limit. Workday descriptions vary more in length by employer, so a feed of Workday jobs holds about 800 to 2,200 within that limit (measured on live jobs, October 2026). Past that, the newest jobs are kept and the oldest are left out of the XML (`OUTPUT` reports `jobsOmitted`); split the companies across new feed keys to keep every job. A much larger feed (roughly 5,000 to 13,000 jobs with descriptions, depending on how well they compress) is over the limit of the feed's saved record: the XML still follows that run, but the saved feed isn't updated, so `OUTPUT` has a `feedWarning`, and with `onlyNewJobs` the jobs that run returned stay in the dataset but leave the feed on the next run. Split the companies across new feed keys well before that.
- **Descriptions** are always on with `feedOutput`, in the feed and in the dataset rows. They come from Greenhouse, Lever, Ashby, Workable, Rippling, Personio and Workday boards. In Workday descriptions, email addresses, phone numbers and labelled recruiter or contact lines are removed automatically. The scrub matches patterns, so a contact written an unusual way, such as a name inside a sentence, can remain. Check descriptions if your board must show none. Descriptions from other ATSes are delivered as the employer wrote them. Workday jobs also get their posted date from the job's own page. Breezy HR jobs have no description: they stay in the feed without a `description`, and Jboard and JobBoard.io reject them. Workday job pages are read one at a time per employer, for up to 15 minutes per employer per run. With `onlyNewJobs`, a Workday job whose page couldn't be read in that run (that budget or the run's timeout ran out, or Workday asked us to slow down) is held and read again on the next run (the status row counts it in `jobsDescriptionPending`, not charged); without `onlyNewJobs`, it is delivered without a description. A Workday job that closed between the list and its own page isn't delivered or charged (`jobsClosed`). `OUTPUT` reports `jobsWithoutDescription`, and the log names their ATSes. For a Jboard or JobBoard.io feed, leave those employers out of that feed's list. A feed key written before descriptions were always on keeps its older jobs without a description while they stay open (with `onlyNewJobs` they are never refreshed), so start such a feed on a new key.
- **Jobs without a location:** a job whose board gives no location is written without a `location`, and `OUTPUT` counts it in `jobsWithoutLocation`. JobBoard.io uses the feed's backup location for such a job, if you set one. Jboard can mark jobs remote when the location is missing; its general location replaces the location of every imported job. Set a fallback that is true of those jobs, or leave those employers out of the feed.
- **Posting dates on Jboard:** Jboard can expire an imported job as soon as it arrives if its original posting date is more than "Job expires in days" ago, and employers often keep a job open for months. To prevent that, set the importer's "Posting date" to the import date, or set a higher "Job expires in days".
- **Republishing is your call:** each job's `url` points to the posting on the employer's own board. Keep that link so applicants apply at the source. The descriptions are the employers' own text, so make sure publishing them fits your board's terms and the employers' wishes, and don't present a job as posted by or partnered with the employer unless it is.

### Use it by API or from an AI agent

- **Apify API:** start a run with your Apify token and read the dataset; keep rows where `type` = `job`.
- **AI agents:** like any Store Actor, it can be called through Apify's MCP server.

### How much does it cost to get a company's open jobs?

$0.001 per job row, nothing else. Each job row delivered is one charged event, so Stripe's 727 open jobs (on 2026-10-08) cost about $0.73, and `maxItems` caps the spend. These are free:

- Status rows, and companies that aren't found or whose match is below your confidence setting.
- Jobs your filters remove.
- With `onlyNewJobs`, jobs an earlier run already returned.
- Duplicates: a job is returned once, even when the same company is typed as a name and a domain, or a board lists a job twice.
- "General application", talent-pool and test postings, and postings with no title or no valid link on the board. They are dropped before charging, and the status row counts them in `rowsDropped`.
- Workday jobs that closed between the job list and the job's own page (`jobsClosed` on the status row).
- With `onlyNewJobs` and descriptions on, Workday jobs whose page couldn't be read in that run: they are held and offered again on the next run (`jobsDescriptionPending`).
- Jobs past `maxJobsPerCompany`, `maxItems`, the run's maximum charge or its timeout. They are not returned; the status row says `jobsTruncated` and gives the `reason`. A company not reached at all gets a free status row marked `skipped` or `timedOut`.

### FAQ

**Do I need a Greenhouse, Lever or Workday account or API key?**
No. The jobs come from each company's public job board. You only need an Apify account to run the Actor.

**How do I find out which ATS a company uses?**
Give the company's name or domain. The status row names the ATS and the board it found (`ats`, `boardUrl`), or says that no board was found on the 8 supported systems, for free.

**Can I get only new jobs?**
Yes. Put the Actor on an Apify [schedule](https://docs.apify.com/platform/schedules) and turn on `onlyNewJobs`. The first run returns every job as usual. After that, each run returns and charges only jobs that an earlier run with the same `stateKey` hasn't returned, whether or not the board gives a posted date. Those rows have `isNew: true`, and the status row counts the skipped ones in `rowsDropped.seenBefore`, free. The job IDs are kept in a key-value store named `company-jobs-state` in your own Apify account; give each schedule or list its own `stateKey`. Jobs cut by a cap, the charge limit or the timeout are offered again on the next run. So `isNew` means "not returned by this state before", not "recently posted". Jobs come in the board's own order, so with `maxJobsPerCompany` smaller than a board, each later run returns more of the jobs that were already on it, with `isNew: true`, until every job has been returned once. Once a run returns a board in full (its status row has no `jobsTruncated`), later runs return only jobs added to that board since. To get there on the first run, set `maxJobsPerCompany` above your largest board (the default is 1,000, but the Console form starts at 10) and allow enough charge limit and timeout. A Workday or Rippling board with more than 1,000 jobs is never read in full, so jobs past the first 1,000 can still come back with `isNew: true`. For fresh postings only, add `postedWithinDays`, with `requirePostedDate` to drop jobs that have no posted date; `postedAt` gives the posted date when the board has one.

Without saved state, `postedWithinDays` matched to the schedule (7 for a weekly run) returns the jobs posted in that window. Jobs with no posted date are kept unless `requirePostedDate` is on.

**Can I get only remote jobs?**
Turn on `remoteOnly`. A job whose remote status the board doesn't give is left out and not charged.

**Does it scrape LinkedIn or Indeed?**
No. It reads the company's public job board on the 8 supported ATSes, and sometimes the company's own website to confirm a match, never job aggregators or sites that need a login.

### How-to guides

- [Workday jobs by company name](https://siftsmith.com/workday-jobs-by-company-name/): how Workday careers URLs are built, and how to read jobs for a whole company list with this Actor.
- [Greenhouse, Lever and Ashby jobs API](https://siftsmith.com/greenhouse-lever-ashby-jobs-api/): the public endpoints and the traps in each, and how to read them for a whole company list with this Actor.

### Related tools

- [Hiring Signals](https://apify.com/siftsmith/hiring-signals): one row per company with its open jobs, recent posting velocity and a 0–100 hiring score, instead of one row per job.

### Support

Open an issue on the Actor's Issues tab or email hello@siftsmith.com. Issues are read daily and fixed promptly.

# Actor input Schema

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

One per line, mixed freely: a company name (Figma), a domain (stripe.com), a careers page URL, or a job board URL (https://jobs.lever.co/spotify). Names and domains are matched to Greenhouse, Lever, Workable, Breezy HR and Personio boards (Ashby only by board URL or 'ashby:slug'), then Workday (charged only when its postings confirm the company; otherwise a free status row). Rippling is never guessed: give the board URL or 'rippling:slug'; a Workday URL or 'workday:tenant:wdN:site' also works. Explicit boards also work: 'greenhouse:stripe', 'lever:palantir', 'ashby:ramp'.

## `minMatchConfidence` (type: `string`):

How sure the company-to-board match must be before its jobs are returned and charged. 'medium' (default) or 'high'. Boards below it return no jobs and cost nothing; the free status row for that company (type 'company') says why and gives the board to pass explicitly. Board URLs you type are always used. Low-confidence matches are never charged.

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

Keep jobs whose title contains any of these (case-insensitive). Empty = all.

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

Drop jobs whose title contains any of these, e.g. 'intern'.

## `locationKeywords` (type: `array`):

Keep jobs whose location contains any of these, e.g. 'London', 'Remote'.

## `departmentKeywords` (type: `array`):

Keep jobs whose department contains any of these, e.g. 'Engineering'.

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

Keep only remote jobs.

## `countries` (type: `array`):

Keep jobs in these countries: ISO codes (US, GB, DE) or English names (Germany). A job's country is set only when its location clearly names it (or the job board gives it); jobs whose country is unknown are dropped, never charged, and counted in each company's free status row (countryUnknown, countryOther).

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

Keep jobs posted in the last N days. Jobs with no posted date are kept unless 'Require a posted date' is on. Workday's '30+ days ago' jobs get a date from the job's own page when descriptions are on, or when the job lists several places or none and 'Require a posted date' is off.

## `requirePostedDate` (type: `boolean`):

With 'Posted within days', also drop jobs whose board gives no posted date.

## `onlyNewJobs` (type: `boolean`):

For scheduled runs: return (and charge) only jobs that an earlier run with the same state key hasn't already returned. The first run returns every job as usual. Job IDs are kept in a key-value store named 'company-jobs-state' in your own Apify account. Jobs cut by a cap, the charge limit or the timeout are offered again next time, so 'new' means not returned before, not recently posted: with a cap smaller than a board, later runs return jobs that were already on it. Once a run returns a board in full (no jobsTruncated on its status row), later runs return only jobs added since. To get there, set 'Max jobs per company' above your largest board (this form starts at 10) and allow enough charge limit and timeout; Workday and Rippling boards over 1,000 jobs are never read in full. For fresh postings only, use 'Posted within days', with 'Require a posted date' to drop undated jobs.

## `stateKey` (type: `string`):

With 'Skip jobs an earlier run returned': which saved state to use. Give each schedule or employer list its own key so they don't share what counts as already returned. Letters, digits and - \_ . only.

## `feedOutput` (type: `boolean`):

Also keep an XML job feed (Indeed-style: title, company, url, referencenumber, date, location and more) at one stable URL that job board software such as Jboard can import on a schedule. Each run updates it: jobs still open stay, closed jobs leave, new jobs are added; status rows are never in it. The URL is in the run's OUTPUT record, status message and log. Anyone with the URL can read the feed. It is kept in a key-value store named 'company-jobs-feed' in your own Apify account (it counts toward your storage). Descriptions are always included (Breezy HR jobs have none). No extra charge.

## `feedKey` (type: `string`):

With 'Job feed for job boards': which feed to update. Give each employer list its own key, and use a new key when you change the filters. Empty = the saved task's ID, or 'default'. With 'Skip jobs an earlier run returned', the feed uses the state key: leave this empty or set it to the same value. Letters, digits and - \_ . only.

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

Jobs past this cap are not returned and not charged. The company's free status row still reports the full board size. This form starts at 10 so a first run is quick and costs about $0.07; raise it or clear it for more (up to 1,000 per company when left empty).

## `maxItems` (type: `integer`):

Cap on job rows (and so on spend) for the whole run. Empty = no cap beyond the run's maximum charge.

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

Adds the job description text where the board provides it (Greenhouse, Lever, Ashby, Workable, Rippling, Personio), and on Workday (read from each job's own page, one job at a time per employer). Off keeps rows small for spreadsheets. Always on with 'Job feed for job boards', since Jboard and JobBoard.io reject jobs without a description.

## `maxConcurrency` (type: `integer`):

How many companies to process in parallel.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "databricks.com",
    "Figma",
    "ashby:ramp",
    "https://jobs.lever.co/spotify",
    "lever:palantir",
    "https://clio.wd3.myworkdayjobs.com/ClioCareerSite",
    "https://us-erac.icims.com/jobs"
  ],
  "minMatchConfidence": "medium",
  "remoteOnly": false,
  "requirePostedDate": false,
  "onlyNewJobs": false,
  "stateKey": "default",
  "feedOutput": false,
  "maxJobsPerCompany": 10,
  "includeDescription": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

No description

## `companies` (type: `string`):

No description

# 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": [
        "stripe.com",
        "databricks.com",
        "Figma",
        "ashby:ramp",
        "https://jobs.lever.co/spotify",
        "lever:palantir",
        "https://clio.wd3.myworkdayjobs.com/ClioCareerSite",
        "https://us-erac.icims.com/jobs"
    ],
    "maxJobsPerCompany": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("siftsmith/company-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": [
        "stripe.com",
        "databricks.com",
        "Figma",
        "ashby:ramp",
        "https://jobs.lever.co/spotify",
        "lever:palantir",
        "https://clio.wd3.myworkdayjobs.com/ClioCareerSite",
        "https://us-erac.icims.com/jobs",
    ],
    "maxJobsPerCompany": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("siftsmith/company-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": [
    "stripe.com",
    "databricks.com",
    "Figma",
    "ashby:ramp",
    "https://jobs.lever.co/spotify",
    "lever:palantir",
    "https://clio.wd3.myworkdayjobs.com/ClioCareerSite",
    "https://us-erac.icims.com/jobs"
  ],
  "maxJobsPerCompany": 10
}' |
apify call siftsmith/company-jobs --silent --output-dataset

```

## MCP server setup

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