# Company Careers Jobs Scraper: Workday, Greenhouse, Lever (`solidfetch/ats-jobs-feed`) Actor

Job listings and job openings from company career pages: Workday jobs (myworkdayjobs), Greenhouse, Lever, Ashby, SmartRecruiters. Paste careers page URLs, e.g. engineering jobs at Stripe, Notion, NVIDIA. Free company hiring row with hiring signals and seniority. Only new jobs. Jobs API.

- **URL**: https://apify.com/solidfetch/ats-jobs-feed.md
- **Developed by:** [Solid Fetch](https://apify.com/solidfetch) (community)
- **Categories:** Agents, Jobs, Lead generation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 90.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Company Careers Jobs Scraper: Workday, Greenhouse, Lever, Ashby

Job listings and job openings from company career pages, for example all open engineering jobs at Stripe, Notion or NVIDIA (a Workday careers site, myworkdayjobs). Paste company names or careers page URLs from Workday, Greenhouse, Lever, Ashby or SmartRecruiters and get one row per open job, with title, department, location, remote, seniority and salary when the company shows it. You also get a free company hiring row with hiring signals: the open roles by department, new roles in the last 7 days and a hiring score. Give only a company domain such as stripe.com and the actor finds its careers page and counts how many people the company is hiring and at what seniority. A job row costs $2.00 per 1,000 on the free plan.

```json
{
    "companies": ["https://boards.greenhouse.io/airbnb", "lever:zoox", "ashby:ramp"],
    "keywords": ["engineer"],
    "maxItems": 100
}
```

One output row per open job (shortened from the full row under Output):

```json
{
    "rowType": "job",
    "ats": "ashby",
    "company": "ramp",
    "title": "Security Engineer, Cloud",
    "department": "Engineering",
    "location": "New York, NY (HQ)",
    "remote": true,
    "salaryText": "$211.4K - $290.6K",
    "postedAt": "2026-04-07T17:12:35.753Z",
    "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245"
}
```

### Workday, Greenhouse, Lever and Ashby jobs feed: hiring signals and job postings API

Workday jobs, Greenhouse jobs, Lever jobs, Ashby jobs and SmartRecruiters jobs of the companies you choose, in one schema. Give a list of company names or careers-page URLs and get the open jobs at those target companies, plus a free hiring-signal row per company: open roles, roles by department, remote roles, postings in the last 7 days, and what is new or closed since the last run. The actor works out the vendor from each address. Filter by keyword, location, remote, department and posting date, or switch on the monitor to get only the jobs that are new since your last run. Each job is one row with the title, department, team, locations, remote and workplace type, employment type, salary when the vendor shows it, posting date, job and apply URLs and the description.

**Read this before you schedule the monitor:** with `onlyNew` on, the first run for a company only records what it has open and returns no jobs (its free summary row is still saved). New jobs come from the second run on. Set `emitOnFirstRun` to `true` to get, and pay for, every open job on the first run as well.

The actor reads only the public job-board APIs that the five vendors offer for publishing jobs (for Workday, the JSON that a career site's own pages load). There is no login, no browser, no proxy and no scraping of LinkedIn. The one place the actor reads a company's own website is the hiring-signals mode (see "Hiring signals by company domain"), which fetches a careers page with a plain request and respects robots.txt.

### What people use it for

- **Hiring signals for sales and recruiting.** A company that opens ten engineering roles in a week is growing a team. Run the monitor daily over a list of target accounts and get the new roles, plus a free row per company with its open roles by department.
- **Job boards and alerts.** Feed a niche job board or a Slack channel with fresh remote roles from the companies you care about. Each run returns only what is new, so the feed has no duplicates.
- **Market and salary research.** Collect open roles with their salary ranges (Ashby, Lever and Greenhouse show them when the company turns pay transparency on) across many companies in one table, without learning four different APIs.
- **Competitor watch.** Follow which teams a competitor is hiring for and where.

### How to use it

Read three companies on three vendors. The vendor follows from the address, or from the prefix of a board id:

```json
{
    "companies": [
        "https://boards.greenhouse.io/airbnb",
        "https://jobs.lever.co/zoox",
        "ashby:ramp",
        "https://careers.smartrecruiters.com/Canva",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
    ],
    "maxItems": 500
}
```

Workday jobs of one company, the newest 100, without descriptions (see "Workday career sites"):

```json
{
    "companies": ["workday:nvidia/NVIDIAExternalCareerSite/wd5"],
    "maxItems": 100,
    "includeDescription": "none"
}
```

Remote engineering jobs posted in the last two weeks, in Europe or the US:

```json
{
    "companies": ["greenhouse:stripe", "lever:spotify", "ashby:notion"],
    "keywords": ["engineer", "developer"],
    "excludeKeywords": ["intern"],
    "remoteOnly": true,
    "locations": ["Europe", "United States", "Poland", "Germany"],
    "postedSince": "2 weeks",
    "includeDescription": "none"
}
```

The monitor, scheduled daily. The first run records what each company has open and returns no jobs, only the free summary rows. That is the baseline, not an error. From the next run on, you get only the jobs that are new since the run before:

```json
{
    "companies": ["greenhouse:airbnb", "lever:zoox", "ashby:ramp"],
    "departments": ["Engineering", "Product"],
    "onlyNew": true
}
```

The same monitor, but the first run also returns every open job that passes the filters, and charges each one. Later runs return only the new ones, as above:

```json
{
    "companies": ["greenhouse:airbnb", "lever:zoox", "ashby:ramp"],
    "departments": ["Engineering", "Product"],
    "onlyNew": true,
    "emitOnFirstRun": true
}
```

A field name the actor does not know stops the run with a hint such as `"keyword" (did you mean "keywords"?)`, so a typo cannot turn into a run of unfiltered jobs that you pay for.

### Input

| Field | What it does |
|---|---|
| `companies` | The companies to read, up to 2,000. Each entry is a careers-page address (`boards.greenhouse.io/stripe`, `job-boards.greenhouse.io/stripe`, `jobs.lever.co/spotify`, `jobs.eu.lever.co/acme`, `jobs.ashbyhq.com/ramp`, `careers.smartrecruiters.com/Canva`, `nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`, an address with a job id or a language such as `/en-US/` in it works too), a board id with its vendor (`greenhouse:stripe`, `lever:spotify`, `ashby:ramp`, `smartrecruiters:Canva`, `workday:nvidia/NVIDIAExternalCareerSite/wd5`, short forms `gh` and `sr` work), or a bare id such as `stripe`. A bare id is never read as a Workday site, because Workday needs the tenant, the site name and the data-centre label. A start-URL object with a `url` is also accepted. |
| `ats` | Which vendor to read for bare ids. `auto` (default) reads Greenhouse, Lever, Ashby and SmartRecruiters (not Workday, see the `companies` row) and takes the one vendor with open jobs. When more than one vendor has open jobs under the id, nothing is saved or charged for it and it gets a free `ambiguous-company` error row that names the vendors. Name the vendor if you know it, to save requests and to avoid a same-named board on another vendor. |
| `keywords`, `excludeKeywords` | Keep jobs whose title has any of the keywords, and drop jobs that have any of the excluded ones. Matching ignores case and accents and starts at a word: `engineer` finds "Engineering Manager", `go` does not find "Django". |
| `keywordScope` | `title` (default) or `titleAndDescription`. |
| `locations` | Keep jobs that have any of these in one of their locations or in the country. Locations are free text as the company wrote them ("Remote (US)", "NYC"), so list the variants you need. |
| `departments` | Keep jobs whose department or team contains any of these. |
| `remoteOnly` | Keep jobs marked remote, or with a remote location. |
| `seniority` | Keep jobs of any of these levels: `intern`, `junior`, `mid`, `senior`, `lead`, `manager`, `director`, `exec`. A job with no level is dropped. See "Seniority, work arrangement and skills". |
| `skills` | Keep jobs that name any of these skills, for example `["Python", "Kubernetes"]`. See "Seniority, work arrangement and skills". |
| `postedSince` | A date (`2026-10-15`) or a period back from today (`7 days`, `2 weeks`, `1 month`). Jobs with no posting date are dropped. |
| `onlyNew` | Monitor mode. See "How the monitor works". |
| `emitOnFirstRun` | With `onlyNew`, also return (and charge) every job of a company that passes the filters the first time the actor sees it. Default off, so the first run returns no jobs and only records the baseline. |
| `includeDescription` | `text` (default), `html` or `none`. |
| `maxItems` | Stop after this many jobs in the run, up to 1,000,000. Default 1,000. |
| `maxJobsPerCompany` | Save at most this many jobs, the newest, from each company. |
| `mode` | `jobs` (default) returns the job rows of `companies`. `hiringSignals` takes `companyDomains` and returns one row per domain. See "Hiring signals by company domain". |
| `companyDomains` | Only with `mode` set to `hiringSignals`, up to 500. Company domains such as `stripe.com`, or a full address such as `https://www.stripe.com/careers`. In that mode `companies`, the job filters (`keywords`, `excludeKeywords`, `locations`, `departments`, `remoteOnly`, `seniority`, `skills`, `postedSince`), `emitOnFirstRun` and `maxJobsPerCompany` are refused with a message instead of being ignored. `onlyNew` and `maxItems` still apply. |
| `stateStoreName` | Default `ats-jobs-feed-state`. The key-value store on your account that holds the monitor's memory. Use another name to keep a separate watch, for example per client. |

### Output

The dataset has three kinds of rows, told apart by `rowType`: `job` (charged), `summary` (one per company, free) and `error` (one per entry that gave no jobs, free). The dataset views "Jobs", "Hiring signals per company" and "Entries that gave no jobs" show each kind as a table. To keep only jobs in an export, filter on `rowType` being `job`.

A job row, built from a recorded Ashby posting:

```json
{
    "rowType": "job",
    "id": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "ats": "ashby",
    "companyId": "ramp",
    "company": "ramp",
    "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "title": "Security Engineer, Cloud",
    "department": "Engineering",
    "team": "Backend",
    "employmentType": "full-time",
    "location": "New York, NY (HQ)",
    "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
    "country": "USA",
    "remote": true,
    "workplaceType": "hybrid",
    "workArrangement": "hybrid",
    "seniority": null,
    "skills": ["DevOps", "AWS", "Terraform", "Python", "Flask", "Azure", "GCP"],
    "skillsSource": "description",
    "salaryMin": 211400,
    "salaryMax": 290600,
    "salaryCurrency": "USD",
    "salaryPeriod": "year",
    "salaryText": "$211.4K - $290.6K",
    "postedAt": "2026-04-07T17:12:35.753Z",
    "updatedAt": null,
    "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
    "language": null,
    "description": "About Ramp\nRamp is building the smart infrastructure for finance teams...",
    "descriptionError": null,
    "source": "Ashby public job-board API",
    "fetchedAt": "2026-10-22T09:00:00.000Z"
}
```

| Field | Meaning |
|---|---|
| `id`, `jobId` | `id` is `vendor:company:jobId` and is unique across vendors. `jobId` is the vendor's own id. |
| `ats`, `companyId`, `company` | The vendor (`greenhouse`, `lever`, `ashby`, `smartrecruiters`, `workday`), the board id you gave, and the company name. Greenhouse and SmartRecruiters send the company name, Lever, Ashby and Workday do not, so for those `company` is the board id (for Workday the tenant, for example `nvidia`). A Workday `companyId` is `tenant/site/wdN`. |
| `title`, `department`, `team` | As the company wrote them. SmartRecruiters has no team, and its department falls back to the job function (Sales, Engineering) when the company sets none. Greenhouse has no team. |
| `employmentType` | `full-time`, `part-time`, `contract`, `internship`, `temporary` or `other`, read from the vendor's label. `null` when the vendor sends none (Greenhouse sends it only on some boards). |
| `location`, `locations`, `country` | The main location, all locations of the job, and the country where the vendor gives one (Lever: two-letter code. Ashby: as written, "USA" or "United States". SmartRecruiters: two-letter code. Greenhouse: not given, `null`). |
| `remote`, `workplaceType` | `remote` is true when the vendor marks the job remote or lists a remote location. `workplaceType` is `remote`, `hybrid` or `onsite` where the vendor says it, `null` where it does not. Ashby marks a hybrid job remote when it also lists a remote location, so check `workplaceType` when the difference matters. |
| `workArrangement`, `seniority`, `skills`, `skillsSource` | Worked out from the posting's words with fixed rules, free of charge and with no AI. See "Seniority, work arrangement and skills". |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText` | The salary the vendor shows. Greenhouse: the pay range (no period, so `salaryPeriod` is `null`, ranges for several locations are merged when they share a currency). Lever: the salary range and its period. Ashby: the salary component and its period, with the vendor's short summary in `salaryText`. SmartRecruiters has no salary field. `null` when the company shows no pay. Bonus and equity are not included. |
| `postedAt`, `updatedAt` | When the job was first published (Greenhouse `first_published`, Lever `createdAt`, Ashby `publishedAt`, SmartRecruiters `releasedDate`) and, for Greenhouse only, when it was last updated. Lever and Ashby give no update time. |
| `postedAtApproximate` | `true` when `postedAt` was worked out from a relative text (Workday: "Posted 3 Days Ago"). Always `false` for the other vendors. |
| `url`, `applyUrl` | The job page and the apply page. |
| `jobReqId` | The requisition number where the vendor shows one (Workday, for example `JR2015813`), otherwise `null`. |
| `description` | Plain text, HTML or `null`, as set by `includeDescription`. E-mail addresses are replaced with `[email removed]`. |
| `descriptionError` | `null` on a charged job. When a description was asked for and could not be fetched (SmartRecruiters needs one extra request per job), the job is saved free with `description` `null` and the reason here. |
| `language` | The language code where the vendor gives one (Greenhouse, SmartRecruiters). |
| `source`, `fetchedAt` | The vendor API the row comes from, and when it was read. |

A summary row, saved free for each company that was read:

```json
{
    "rowType": "summary",
    "ats": "ashby",
    "companyId": "ramp",
    "company": "ramp",
    "boardUrl": "https://jobs.ashbyhq.com/ramp",
    "openRoles": 4,
    "postedLast7Days": 0,
    "remoteRoles": 4,
    "newestPostedAt": "2026-08-12T20:22:32.685Z",
    "byDepartment": [
        { "department": "Engineering", "openRoles": 3 },
        { "department": "Risk", "openRoles": 1 }
    ],
    "seniorityMix": { "intern": 0, "junior": 1, "mid": 0, "senior": 0, "lead": 0, "manager": 0, "director": 0, "exec": 0, "unknown": 3 },
    "hiringScore": 19,
    "hiringScoreParts": { "volume": 11, "freshness": 0, "breadth": 8 },
    "matchedThisRun": 1,
    "savedThisRun": 1,
    "newSinceLastRun": null,
    "closedSinceLastRun": null,
    "baseline": false,
    "truncated": false,
    "fetchedAt": "2026-10-22T09:00:00.000Z"
}
```

`openRoles` counts every posting on the company's board, whatever your filters. `matchedThisRun` counts what passed the filters (in monitor mode, among the new postings only) and `savedThisRun` what was saved and charged. `newSinceLastRun` and `closedSinceLastRun` are filled in monitor mode only. `baseline` is true on the run that recorded the starting point. `truncated` is true when a SmartRecruiters board has more than 10,000 postings, which is the most the actor reads for one company.

An error row, saved free for each entry that gave no jobs:

```json
{
    "rowType": "error",
    "input": "ashby:nope",
    "ats": "ashby",
    "companyId": "nope",
    "company": null,
    "outcome": "not found",
    "error": "Ashby has no job board \"nope\"",
    "fetchedAt": "2026-10-22T09:00:00.000Z"
}
```

`outcome` is `invalid` (not a job-board address or id, for example a company's own careers site), `not found` (the vendor has no such board), `ambiguous-company` (a bare id with open jobs on more than one vendor: give `vendor:id` or the careers-page URL) or `failed` (the vendor did not answer after retries, or its format changed). A company with a board and no open roles gets a normal summary row with `openRoles` 0.

The run also saves a `SUMMARY` record in the key-value store: the companies read with their counts, the entries not found, failed or invalid, the bare ids found on more than one vendor, the companies not read because the run stopped, and how many descriptions could not be fetched.

### Seniority, work arrangement and skills

Every job row carries four columns that recruiters and sales teams filter on. They come from the title and the description with fixed rules and a fixed list, so the same posting always gives the same answer. There is no AI call and no outside service, and they add nothing to the price.

| Column | Values | How it is found |
|---|---|---|
| `seniority` | `intern`, `junior`, `mid`, `senior`, `lead`, `manager`, `director`, `exec`, or `null` | The title decides, from the highest level down: Intern, Chief or VP, Director or Head of, Manager, Lead, Staff or Principal, Senior or Sr, Junior or Associate, Mid-level or "II". A job function that ends in "Manager" (Product Manager, Account Manager) is not a management level, so "Senior Product Manager" is `senior`. When the title says nothing, a plain statement in the description fills in ("entry-level position", "mid-level", "senior-level"). A plain "Software Engineer" stays `null`, because guessing would be wrong too often. |
| `workArrangement` | `remote`, `hybrid`, `onsite` or `null` | The vendor's own field first (`workplaceType`). Then the title and locations ("Remote - US", "Hybrid"). Then clear statements in the description ("this role is hybrid", "fully remote", "this is an on-site position"). A passing mention such as "we build tools for remote teams" does not count. `workplaceType` stays as it was, `null` where the vendor says nothing. |
| `skills` | A list such as `["Python", "PostgreSQL", "Kubernetes"]` | The title and description are matched against a curated list of about 300 tech and business skills (languages, frameworks, cloud, data, security, design, product, sales, marketing, finance, HR). A skill matches as a whole word in any case, with common aliases (`k8s`, `golang`, `postgres`). Words that are also ordinary English (Go, R, Excel, React, Swift, Spark) match only in a skill context, so "go-to-market" is not Go. Short acronyms that are also words (ML, RAG, SAP) must be written in capitals. The list is in the order the skills first appear. |
| `skillsSource` | `description` or `title` | `description` when the skills were read from the title and the description. `title` when the posting has no description to read (see below), so the list is short. |

SmartRecruiters and Workday lists carry no description. With `includeDescription` on, the actor fetches it before the row is saved, so those rows have `skillsSource` `description`. With `includeDescription` set to `none` they are read from the title alone and say so. Greenhouse, Lever and Ashby send the description with the list, so their skills are always read from it, even with `includeDescription` set to `none`.

Filter on them. Senior and lead engineers who name Python or Go:

```json
{
    "companies": ["greenhouse:airbnb", "lever:zoox", "ashby:ramp"],
    "keywords": ["engineer"],
    "seniority": ["senior", "lead"],
    "skills": ["Python", "Go"]
}
```

Any manager or director who knows Salesforce or HubSpot, in a monitor that returns only new jobs:

```json
{
    "companies": ["greenhouse:stripe", "ashby:notion"],
    "seniority": ["manager", "director"],
    "skills": ["Salesforce", "HubSpot"],
    "onlyNew": true
}
```

`seniority` keeps a job when its level is any of the listed ones. `skills` keeps a job when any listed skill is found. A name that is not in the curated list is searched as a whole word in the title and description, so `"skills": ["Tailscale"]` works too. A `skills` filter reads the description, so for SmartRecruiters and Workday it costs one extra request per job that passes the other filters, the same as `keywordScope` set to `titleAndDescription`. A `seniority` filter uses the level found in the title and the description the list already has, and does not fetch descriptions on its own, so on SmartRecruiters and Workday it sees the title. Each combination of filters keeps its own monitor memory.

These are rules, not understanding. Titles such as "Software Engineer" have no level, a skill named only in an image or an attachment is not found, and a posting that mentions "Python" as something it does not want is still tagged Python. Check a sample of your own boards before you build on the columns.

### Workday career sites

Workday jobs are the biggest source of corporate postings (NVIDIA, Salesforce, Adobe and thousands more), and each company runs its own career site at `https://<tenant>.<wdN>.myworkdayjobs.com/<site>`. Give that address, with or without the language part (`/en-US/`) or a job in it, or write `workday:<tenant>/<site>/<wdN>`:

```json
{
    "companies": [
        "https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite",
        "workday:salesforce/External_Career_Site/wd12"
    ],
    "keywords": ["engineer"],
    "postedSince": "7 days",
    "includeDescription": "none"
}
```

The actor reads the JSON that the career site's own page loads, `POST /wday/cxs/<tenant>/<site>/jobs`, 20 postings per request, and for a description `GET /wday/cxs/<tenant>/<site>/job/<path>`. The `robots.txt` of the career sites checked does not disallow that path, and the actor sends one request at a time with a pause between them. A career site that answers 401 or 403, or with a bot-check page, is not read and not worked around. It gets a free `failed` error row and the other companies go on.

What the Workday rows hold, and what they do not:

- **Posting date.** Workday's list says only "Posted Today", "Posted 3 Days Ago" or "Posted 30+ Days Ago". `postedAt` is that day at 00:00 UTC and `postedAtApproximate` is `true`. "30+ days" has no date (`postedAt` is `null`), so `postedSince` drops those postings. With `includeDescription` on, the posting's own page gives the exact day, and then `postedAtApproximate` is `false`.
- **Locations.** The list gives one location, or only "3 Locations". For the second case the actor takes the first location from the posting's address, for example `Israel Yokneam`. With `includeDescription` on, `locations` holds all of them, and `country` (two-letter code), `employmentType` (from Workday's "time type") and `workplaceType` and `remote` (from its remote type, where the company sets one) are filled in too. A `locations` filter is applied to the list entry, so it sees only the first location of such a posting.
- **No department, team or salary.** Workday's list has none, so those fields are `null`.
- **Cost of descriptions.** Each saved Workday job takes one extra request for its page, paced, so `includeDescription` `none` is much faster. A posting whose page cannot be read is saved free with `descriptionError`.
- **Size.** Workday lists at most 2,000 postings of one site. A site at that size has `truncated` set to `true` in its summary row. Without any filter and without the monitor, the actor reads only as many postings as `maxItems` (or `maxJobsPerCompany`) allows, 20 per request, newest first, and sets `truncated` for the rest. With a filter or `onlyNew` it reads the whole list (up to 100 requests for a big site) so that nothing is missed.
- **Job id.** `jobId` is the requisition part of the posting's address (`JR2015813`, or `JR355160-1` for a repeated posting), `jobReqId` is the number Workday shows.

Hiring-signals mode also finds Workday: a careers page that links to `*.myworkdayjobs.com` gives the domain a row with `ats` `workday`. A Workday site is never found by the name guess, because the name does not say the tenant, site and data centre.

### Hiring signals by company domain

People ask for the "job opening count" of a company, and they have its website, not its Greenhouse or Lever address. Set `mode` to `hiringSignals` and give domains:

```json
{
    "mode": "hiringSignals",
    "companyDomains": ["ramp.com", "stripe.com", "notion.so"]
}
```

For each domain the actor finds the company's job board and returns one row with the open-role count. It does this in two steps.

1. **The careers page.** It asks the company's own site for `/careers`, then `/jobs`, then `careers.<domain>`, then the home page, and stops at the first page that links a Greenhouse, Lever, Ashby, SmartRecruiters or Workday board (`boards.greenhouse.io`, `job-boards.greenhouse.io`, `jobs.lever.co`, `jobs.ashbyhq.com`, `careers.smartrecruiters.com`, `*.myworkdayjobs.com`). It is one plain GET per page, at most four pages per domain, with robots.txt read first. A page that robots.txt disallows is not requested, a site whose robots.txt cannot be read is left alone, and a 403 or any other refusal is taken as the answer. There is no proxy, no browser and no way around a block. The row says `matchedBy: "careers-page"` and gives the page in `careersUrl`.
2. **The name, only when the pages name no board.** The actor tries the domain's name (`stripe` for `stripe.com`) as a board id on each of the four vendors. A board with that id is used only when it belongs to the domain, which means the company name the vendor sends is the domain's name (Greenhouse and SmartRecruiters send it, so `Stripe, Inc.` counts for `stripe.com`), or at least two of the board's postings write the domain in their text or addresses. A board that fails this check is dropped, so `notion.so` does not get the counts of some other company called Notion. The row says `matchedBy: "name-guess"`, and `careersUrl` is `null`.

A row has these fields:

| Field | Meaning |
|---|---|
| `domain` | The domain, lower case, without `www.`. |
| `ats`, `boardId`, `company`, `boardUrl` | The vendor, the board id, the company name where the vendor sends it, and the board address. All `null` when no board was found. |
| `openRoles` | Every posting on the board. |
| `byDepartment` | Open roles per department, largest first. |
| `seniorityMix` | Open roles per level: `intern`, `junior`, `mid`, `senior`, `lead`, `manager`, `director`, `exec` and `unknown`. The nine numbers add up to `openRoles`. See "Seniority mix and hiring score". |
| `hiringScore`, `hiringScoreParts` | A number from 0 to 100 and the three parts it is the sum of. See "Seniority mix and hiring score". `null` when no board was found. |
| `remoteCount` | Postings marked remote or with a remote location. |
| `postedLast7Days`, `newestPostedAt` | Fresh postings, and the date of the newest one. |
| `newSinceLastRun`, `closedSinceLastRun` | With `onlyNew` on. Postings the last run did not know, and postings that are gone. `null` on the first run for a board. These use a memory of their own, so a hiring-signals watch never hides jobs from a jobs-mode monitor of the same board. |
| `careersUrl`, `matchedBy` | The page that linked the board, and `careers-page` or `name-guess`. |
| `charged` | `true` when the row was saved as a `job` event. |
| `note` | What the match rests on. For a domain with no board, why none was found (a 404, a robots.txt rule, a vendor that did not answer). |
| `truncated` | The board has more postings than the actor reads, so the count is a lower bound. |

A row for `ramp.com`, built from the recorded careers page and Ashby board:

```json
{
    "rowType": "hiringSignal",
    "domain": "ramp.com",
    "charged": true,
    "ats": "ashby",
    "boardId": "ramp",
    "company": null,
    "openRoles": 4,
    "byDepartment": [
        { "department": "Engineering", "openRoles": 3 },
        { "department": "Risk", "openRoles": 1 }
    ],
    "seniorityMix": { "intern": 0, "junior": 1, "mid": 0, "senior": 0, "lead": 0, "manager": 0, "director": 0, "exec": 0, "unknown": 3 },
    "hiringScore": 19,
    "hiringScoreParts": { "volume": 11, "freshness": 0, "breadth": 8 },
    "remoteCount": 4,
    "postedLast7Days": 0,
    "newestPostedAt": "2026-08-12T20:22:32.685Z",
    "newSinceLastRun": null,
    "closedSinceLastRun": null,
    "careersUrl": "https://ramp.com/careers",
    "boardUrl": "https://jobs.ashbyhq.com/ramp",
    "matchedBy": "careers-page",
    "note": "Ashby board \"ramp\" is linked from https://ramp.com/careers",
    "truncated": false,
    "fetchedAt": "2026-10-28T09:00:00.000Z"
}
```

**What it costs.** Each company with a board is charged one `job` event, the same event as a job row. Think of it as one job-event per company with a board. A domain with no board found, an invalid entry and a domain whose lookup failed are free. Nothing new is added to the pricing. `maxItems` counts the charged rows, and the run stops before a row your cost limit cannot pay for. A restarted run does not charge a domain twice (`RUN_STATE` records each charged domain).

A weekly watch over a list of target accounts: run the same domains on a schedule with `onlyNew` on. From the second run each row carries `newSinceLastRun` and `closedSinceLastRun`. The dataset view "Hiring signals by company domain" shows the rows as a table.

**Pair it with two other Solid Fetch actors.** Run [Website Technology Lookup](https://apify.com/solidfetch/marketing-stack-scanner) on the same domains to see which ad tags, pixels and marketing tools each company uses, and [Google Ads Transparency Scraper](https://apify.com/solidfetch/google-ads-transparency-scraper) to see who is advertising on Google. A company that is hiring five marketers, runs an ad pixel and has live Google ads is a company with a marketing budget. All three take the domain as the key, so the tables join on it.

### Hiring signals from the free summary rows

Every company that was read gets one summary row (`rowType` `summary`), saved free on every run and on the baseline run too. Open the `companies` view of the dataset to see only these rows. They are the hiring signal:

| Signal | Field | How to read it |
|---|---|---|
| Size of the hiring push | `openRoles` | Every posting on the company's board, whatever your filters. |
| Which teams are growing | `byDepartment` | Open roles per department, largest first. Ten open Engineering roles at a 200 person company is a team being built. |
| Hiring right now | `postedLast7Days`, `newestPostedAt` | Fresh postings. A board whose newest posting is months old is quiet. |
| Level of the roles | `seniorityMix` | Open roles per seniority level. Many `director` and `exec` roles next to few `junior` ones is a leadership build-out. |
| One number for "how hard are they hiring" | `hiringScore`, `hiringScoreParts` | 0 to 100, with its three parts, see below. |
| Remote friendliness | `remoteRoles` | Postings marked remote or with a remote location. |
| Change since last time | `newSinceLastRun`, `closedSinceLastRun` | Monitor mode only (`onlyNew`). Counts every new and every closed posting on the board since the run before, whatever your filters. |

#### Seniority mix and hiring score

`seniorityMix` and `hiringScore` are on the free summary row and on the hiring-signal row of a domain (null on a domain with no board). They do not change what is charged. Both are computed from the postings the actor just read, with fixed rules and no AI call, so the same board on the same day gives the same numbers.

**Seniority mix.** Every posting counts once, under the level `seniority` gives it on a job row: the title decides (Senior, Staff, Director, Intern and similar words, in English), and the description only fills in when the title says nothing. Postings with no level in either count under `unknown`. SmartRecruiters and Workday lists carry no description, so for them only the title is read and `unknown` is larger. `lead` is a technical lead, principal or architect. `manager`, `director` and `exec` are the leadership levels. Titles in other languages mostly land in `unknown`.

**Hiring score.** The sum of three parts, rounded to whole numbers:

| Part | Range | Formula |
|---|---|---|
| `volume` | 0 to 40 | 40 x log10(1 + open roles) / log10(301), at most 40. Full marks at 300 open roles or more. 10 roles give 17. |
| `freshness` | 0 to 40 | 40 x (roles posted in the last 7 days / roles with a posting date) / 0.25, at most 40. Full marks when a quarter of the dated roles are from this week. 0 when no posting has a date. |
| `breadth` | 0 to 20 | 20 x departments with at least one role / 5, at most 20. Postings without a department are not counted. |

An empty board scores 0. The score measures how large, how fresh and how broad the open roles on one board are, nothing else: it does not know about funding, layoffs, roles on other boards or the company's size, so a big stable board with old postings scores on volume and breadth only. Workday posting dates are worked out from "Posted 3 Days Ago" and are right to the day at best. The thresholds (300 roles, 25%, 5 departments) are choices of this actor, not a standard. The parts are given so you can recompute your own weighting.

A snapshot of open roles for a list of target accounts, without paying for the job rows: run the monitor once with a fresh `stateStoreName`. It is a baseline run, so it saves no jobs and charges none, and every company gets its summary row with `openRoles`, `byDepartment`, `postedLast7Days` and `remoteRoles`. You pay only the start of the run.

```json
{
    "companies": ["greenhouse:stripe", "lever:spotify", "ashby:ramp", "ashby:notion"],
    "onlyNew": true,
    "stateStoreName": "hiring-snapshot-2026-10",
    "includeDescription": "none"
}
```

A weekly hiring-signal watch: schedule the same companies weekly with `onlyNew` on. The summary rows then carry `newSinceLastRun` and `closedSinceLastRun` for each company, and the job rows are only the new postings. To keep the paid job rows small while still counting every new posting on the board, add a `departments` or `keywords` filter for the roles you sell to, for example `["Engineering"]`, and set `includeDescription` to `none`.

A company that is not on one of the five vendors gets a free error row, not a summary row (see the error row below).

### How the monitor works

With `onlyNew` on, the actor keeps a record of each company's open jobs in a named key-value store on your Apify account (`stateStoreName`). A run returns only the jobs that were not in the record, then updates the record.

- **The first run for a company records the starting point and returns no jobs.** The status message says so. This is on purpose: adding 200 companies must not return and charge for their whole job lists. Set `emitOnFirstRun` to get them anyway.
- Jobs that pass the filters are saved. A new job that fails a filter is still remembered, so it does not come back later as new.
- A job left out because of `maxJobsPerCompany`, `maxItems` or your cost limit stays unseen, and the next run returns it.
- A job whose description could not be fetched is saved free with `descriptionError` and stays unseen, so the next run tries again and charges it once it has its description.
- Each set of filters (`keywords`, `excludeKeywords`, `keywordScope`, `locations`, `departments`, `remoteOnly`, `seniority`, `skills`) keeps its own record of a board, so two scheduled tasks with different filters on the same company do not hide jobs from each other. Order and case of the values do not matter, and `postedSince` is not part of it. Changing the filters starts a new record and a new starting point. A watch with no filters uses the same record as earlier versions of the actor.
- The summary row says how many postings were new and how many closed since the last run.
- Run it from a schedule (daily is usual). Two runs in a row over the same board return no jobs the second time.
- Jobs are matched by the vendor's job id. A job that the company deletes and posts again gets a new id and counts as new.
- If you change `stateStoreName`, the actor starts a new record and records a new starting point.

### Send results to Make, Zapier, n8n, Slack or a CRM

Start the actor or a saved task on a schedule. Trigger the next step on the finished run: in Make the Apify module "Watch Actor Runs", in Zapier the Apify trigger for a finished actor run, in n8n the Apify node. Read the rows with "Get Dataset Items" using the run's dataset ID. For your own endpoint, open the actor's Integrations tab and add an HTTP webhook on "Run succeeded". The payload contains `resource.defaultDatasetId`, which you can read at `https://api.apify.com/v2/datasets/<datasetId>/items?format=json`. Add `&view=jobs` for the job columns only, or `&format=csv` for a spreadsheet.

### Pricing

You pay per job saved (event `job`), and the price is on the Pricing tab. In hiring-signals mode you pay the same `job` event once per company domain with a board, and nothing for a domain with none. The summary rows and error rows are free and do not count against `maxItems`. So is a job saved without the description you asked for (`descriptionError` set), and a bare id found on more than one vendor is not saved or charged at all. A run that finds no new jobs costs only the start of the run. When a run reaches the maximum cost you set, it stops and keeps what was saved, and it never saves a job the remaining budget cannot pay for. If the platform migrates or restarts a run, the run picks up its own record of what it saved (`RUN_STATE` in the key-value store) and does not save or charge those jobs again.

### Source and legal

The actor calls only the public job-board APIs that the vendors publish for showing a company's own open jobs on its website:

- Greenhouse: `boards-api.greenhouse.io/v1/boards/{board}/jobs`
- Lever: `api.lever.co/v0/postings/{company}` (and the EU site `api.eu.lever.co` for a company not found on the global site)
- Ashby: `api.ashbyhq.com/posting-api/job-board/{board}`
- SmartRecruiters: `api.smartrecruiters.com/v1/companies/{company}/postings`
- Workday: `{tenant}.{wdN}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs` (the JSON that the career site's own pages load) and `.../job/{path}` for a description

These need no login or key. The actor sends one request at a time with a pause between requests, retries politely, and honours `Retry-After`. In jobs mode it does not open company websites, career pages behind a login, or any page that is not one of these APIs. In hiring-signals mode it also makes up to four plain GET requests per domain for the company's own public careers page and robots.txt, never past a login or a block, and it follows robots.txt. It reads nothing from LinkedIn, Indeed or other job boards.

A company that runs its own careers site on top of one of these vendors usually still has a board on the vendor. Use the vendor address (for example `boards.greenhouse.io/stripe` for Stripe's jobs page) and not the company's own address, which the actor cannot read.

### Personal data

Job postings are company information. The actor does not output the names or e-mail addresses of recruiters or hiring managers, even where a vendor's API returns them (SmartRecruiters returns a posting creator on some postings, and the actor ignores it). Descriptions sometimes contain a recruiter's e-mail address, and every address is replaced with `[email removed]`. Names written inside a description text are not detected.

When a run fails, an error report with the error message and the shape of the input (which fields are set, how long the lists are), never your input values or your results, is sent to the developer to fix bugs.

### Limits

- A vendor's API shows what the company chose to publish. A job that is not on the public board is not in the feed. Ashby jobs marked unlisted are left out.
- Posting dates mean "first published" on Greenhouse, Lever, Ashby and SmartRecruiters (Workday: see "Workday career sites"), and Lever and Ashby give no update time, so an edited job does not move.
- Salary appears only where the company turned it on. Greenhouse gives no pay period. SmartRecruiters gives no salary at all.
- A bare id looked up with `ats` set to `auto` takes the vendor that has open jobs under that name. If two vendors have jobs under it, nothing is saved for it, and if a vendor does not answer, the entry fails rather than guessing. One vendor with jobs can still be a different company of the same name, so name the vendor to be sure.
- Hiring-signals mode needs the careers page to link a board, or the domain's name to be the board id. A company with its own applicant system, a careers page that builds its links in the browser, or a board id unlike the domain's name gets a free row with no board. Pass its board address in `companies` in jobs mode instead.
- A name-guess match is a guess that passed a check. The `matchedBy` field says which rows are guesses, so keep only `careers-page` rows if you want none.
- Greenhouse boards on the EU site (`boards.eu.greenhouse.io`) are not read yet and get an `invalid` row.
- SmartRecruiters answers with an empty list both for a company it does not know and for one with no open jobs, so both give a `not found` row.
- SmartRecruiters descriptions need one extra request per saved job, so a run over a big SmartRecruiters board with descriptions is slower. Use `includeDescription: "none"` for counts and titles.
- A full run is as fast as the vendors answer. Greenhouse and Lever send a whole board in one response (several MB for the largest boards), and the actor waits a short time between requests.
- The actor stops with an error row for a company when its board comes back in a format the actor does not read, before saving anything from it, and the other companies continue.

If this actor saved you time, a short review on the actor page helps other people find it.

### More from Solid Fetch

- [Google Ads Transparency Scraper](https://apify.com/solidfetch/google-ads-transparency-scraper): Competitor Google ads, domain checks and new-ad monitoring from the Ads Transparency Center.
- [Businesses Without Website Leads](https://apify.com/solidfetch/overture-places-leads): Local businesses without a website, with phone and address, by city.
- [FMCSA New Trucking Companies](https://apify.com/solidfetch/fmcsa-carriers): New US trucking companies and insurance leads from the FMCSA register.
- [EU Company Registry](https://apify.com/solidfetch/eu-company-registry): Company and VAT number lookup and new-company feeds from several European registers.
- [Google Trends Monitor](https://apify.com/solidfetch/google-trends-monitor): Keyword comparison, spike alerts and trending searches from Google Trends.
- [Core Web Vitals Benchmark](https://apify.com/solidfetch/crux-competitor-benchmark): Page speed of your site and its competitors from the Chrome UX Report.
- [Polish IT Jobs Scraper](https://apify.com/solidfetch/poland-it-jobs): IT job offers from solid.jobs with salaries in PLN, plus salary reports.
- [UK Operator Licences](https://apify.com/solidfetch/uk-operator-licences): UK goods vehicle and bus operator licences, with market lists and weekly changes.
- [Bulk Email Validator](https://apify.com/solidfetch/email-list-checker): Email list cleaning with MX, disposable-domain and typo checks.

Did this help? A review on this page, or an issue saying what is missing, helps other people find it.

# Changelog

This Actor's version history is a separate document: https://apify.com/solidfetch/ats-jobs-feed/changelog.md

# Actor input Schema

## `mode` (type: `string`):

Jobs returns the job rows of the companies you list. Hiring signals takes company domains such as stripe.com, finds each one's job board from its careers page, and returns one row per domain: open roles, roles by department, remote roles, newest posting. A domain with a board costs one job event, a domain with no board is free.

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

The companies to read. Each entry is a careers-page address on Greenhouse, Lever, Ashby, SmartRecruiters or Workday (boards.greenhouse.io/stripe, jobs.lever.co/spotify, jobs.ashbyhq.com/ramp, careers.smartrecruiters.com/Canva, nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite), or a board id with its vendor (greenhouse:stripe, lever:spotify, ashby:ramp, smartrecruiters:Canva, workday:nvidia/NVIDIAExternalCareerSite/wd5). A Workday career site needs its address or the workday:tenant/site/wdN form. A bare id such as stripe is looked up on each vendor in turn. Up to 2,000 per run. An entry that is not a job board, or not found, gets a free row.

## `companyDomains` (type: `array`):

Only with mode "Hiring signals". Company website domains such as stripe.com, notion.so or a full address (https://www.stripe.com/careers). For each, the actor reads the company's careers page (plain GET, robots.txt respected) and takes the Greenhouse, Lever, Ashby, SmartRecruiters or Workday board it links to. When the page names none, it tries the domain's name as a board id and keeps the board only if the board's company name or postings show the domain. Up to 500 per run. Leave "Companies" empty in this mode.

## `ats` (type: `string`):

Which vendor to read for entries that are only a name, without an address or a vendor prefix. Entries with an address or a prefix already say it.

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

Keep jobs whose title has any of these words, without regard to case or accents. A keyword matches where it starts a word: engineer finds Engineering Manager, go does not find Django. Empty keeps every job.

## `excludeKeywords` (type: `array`):

Drop jobs whose title has any of these words, without regard to case or accents, matched where a word starts as with Keywords. For example intern or staff. Empty drops nothing.

## `keywordScope` (type: `string`):

Searching descriptions finds more but is noisier. For SmartRecruiters and Workday it costs one extra request per job that passes the other filters.

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

Keep jobs with any of these in one of their locations or in the country, for example Berlin, Poland, United States. Locations are free text as the company wrote them, so list the variants you care about (NYC, New York).

## `departments` (type: `array`):

Keep jobs whose department or team contains any of these words, without regard to case, for example Engineering, Sales, Customer Success. Department names are free text as each company wrote them, so list the variants you care about. Empty keeps every department.

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

Keep jobs the company marks as remote, or with a remote location. Hybrid jobs that also list a remote location count.

## `seniority` (type: `array`):

Keep jobs of any of these levels. The level is read from the title with fixed rules (Senior, Staff, Engineering Manager, Director, VP...), and from the description only when the title says nothing, so a plain Software Engineer has no level and is dropped by this filter. SmartRecruiters and Workday lists have no description, so there the title decides.

## `skills` (type: `array`):

Keep jobs that name any of these skills, for example Python, Kubernetes, Salesforce. A skill is looked up in a curated list of about 300 tech and business skills (aliases work: k8s, golang, postgres) and read from the title and the description. A name that is not in the list is searched as a whole word. For SmartRecruiters and Workday this costs one extra request per job that passes the other filters, because their lists have no description.

## `postedSince` (type: `string`):

Keep jobs posted on this date or later: a date (2026-10-15) or a period back from today (7 days, 2 weeks, 1 month). The posting date is the one the vendor shows (first published, or created). Jobs with no posting date are dropped.

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

Monitor mode. The actor remembers each company's open jobs in a named key-value store on your account and returns only the jobs it did not know at the end of the last run. The first run for a company records the starting point and returns no jobs (see the next option). Schedule the run daily or weekly.

## `emitOnFirstRun` (type: `boolean`):

With the monitor on, also return every job of a company the first time the actor sees it. Off by default so that adding 200 companies does not return and charge their whole job lists at once.

## `includeDescription` (type: `string`):

E-mail addresses in descriptions are replaced with \[email removed]. For SmartRecruiters and Workday the description costs one extra request per saved job, and Workday's description request also fills in locations, hours and the exact posting day.

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

Stop after this many jobs in the whole run. You pay per job saved, and the run stops before it would save a job your cost limit cannot pay for. Newest jobs of each company come first.

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

Save at most this many jobs (the newest) from each company. In monitor mode the jobs left out stay unseen and are returned by the next run.

## `stateStoreName` (type: `string`):

Name of the key-value store on your account where the monitor keeps what it has seen. Use a different name to keep a separate watch, for example per client.

## Actor input object example

```json
{
  "mode": "jobs",
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "lever:spotify",
    "ashby:ramp"
  ],
  "companyDomains": [
    "stripe.com",
    "notion.so"
  ],
  "ats": "auto",
  "keywords": [
    "engineer",
    "developer"
  ],
  "excludeKeywords": [
    "intern"
  ],
  "keywordScope": "title",
  "locations": [
    "Berlin",
    "Poland",
    "United States"
  ],
  "departments": [
    "Engineering",
    "Sales"
  ],
  "remoteOnly": false,
  "seniority": [
    "senior",
    "lead"
  ],
  "skills": [
    "Python",
    "Kubernetes"
  ],
  "postedSince": "7 days",
  "onlyNew": false,
  "emitOnFirstRun": false,
  "includeDescription": "text",
  "maxItems": 25,
  "stateStoreName": "ats-jobs-feed-state"
}
```

# Actor output Schema

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

Table of the open job postings saved by the run, one row per job: title, department, locations, remote and workplace type, seniority, skills, salary, posting date, job and apply URLs, description. Open this first in mode jobs.

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

Free hiring-signal row per company read in mode jobs: open roles, postings in the last 7 days, remote roles, roles by department, newest posting and, in monitor mode, new and closed jobs since the last run.

## `hiringSignalsByDomain` (type: `string`):

Hiring signals per company domain in mode hiringSignals: board found, open roles, roles by department, remote roles, newest posting and how the board was matched. Empty in mode jobs.

## `errors` (type: `string`):

Free rows for entries that gave no jobs: not a job board, not found at the vendor, a bare id that exists on several vendors, or a vendor that did not answer, each with a reason.

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

JSON record SUMMARY in the key-value store: companies read, entries not found and which limits were reached.

# 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/airbnb",
        "lever:zoox",
        "ashby:ramp"
    ],
    "maxItems": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("solidfetch/ats-jobs-feed").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/airbnb",
        "lever:zoox",
        "ashby:ramp",
    ],
    "maxItems": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("solidfetch/ats-jobs-feed").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/airbnb",
    "lever:zoox",
    "ashby:ramp"
  ],
  "maxItems": 25
}' |
apify call solidfetch/ats-jobs-feed --silent --output-dataset

```

## MCP server setup

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

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/NEqfFeY9sjPkmjRc4/builds/UCNX4MNDCeEUO7bn3/openapi.json
