# ATS Hiring Intelligence for Your Account List (`yourname_mahi/ats-hiring-intelligence`) Actor

Give it your account list (a dataset or ATS board links). It reads each company's PUBLIC Greenhouse, Lever or Ashby job feed and adds hiring columns next to your original ones: open roles, role families, remote/salary coverage, tech mentions, and new/changed/removed roles between runs.

- **URL**: https://apify.com/yourname\_mahi/ats-hiring-intelligence.md
- **Developed by:** [MST MORIUM AKTHER MAYA](https://apify.com/yourname_mahi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 company processeds

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

Hiring intelligence for your account list. Give this Actor a list of companies (typed in, or an existing Apify dataset such as the output of a lead scraper) and it reads each company's **public** job board on **Greenhouse, Lever or Ashby**, then adds hiring columns to **your own row**: open roles, role families, departments, locations, remote share, how many roles publish pay, and, if you run it again, which roles are new, changed or gone.

It is not a job-board scraper. You get **one row per company you gave it**, with your own columns kept, so the result drops straight into your CRM, spreadsheet, n8n or Make workflow.

### What it does

- Reads only the public job-posting feeds of Greenhouse, Lever (including the EU tenant) and Ashby. Plain HTTP and JSON: no browser, no proxy, no login, no cookies.
- Deterministic. No AI/LLM is called; the same board gives the same answer.
- Never touches candidates, applications, hiring managers or any private ATS data, and never uses an authenticated API.
- Does not republish job descriptions. Descriptions are read only if you turn on technology mentions; only the matched term plus a snippet of at most 120 characters is output.
- Tells you honestly when it could not look. "No ATS found" and "source unavailable" are never reported as "no jobs".

### Who it is for

- **Sales and RevOps teams** who want a hiring signal next to every account in their CRM export.
- **Recruiting and staffing agencies** who track which target accounts are opening roles in a given team.
- **Market and competitor analysts** who monitor a fixed list of companies week after week.
- **Automation builders** who need a predictable, one-row-per-company JSON that an AI agent or workflow can consume.

### Supported applicant tracking systems

| ATS | Accepted forms | Notes |
|---|---|---|
| Greenhouse | `greenhouse:airbnb`, `https://boards.greenhouse.io/airbnb`, `https://job-boards.greenhouse.io/airbnb` | The public list feed carries no pay data, so pay columns are `null` (unknown), not 0. |
| Lever | `lever:weride`, `https://jobs.lever.co/weride`; EU tenant `lever-eu:acme` | Pay appears only when the posting publishes it. |
| Ashby | `ashby:ramp`, `https://jobs.ashbyhq.com/ramp` | Publishes workplace type and, often, pay ranges. |

Other ATS vendors, authenticated APIs and jobs-per-row output are not part of this version.

### Input modes

1. **Boards typed in (`boards`)**: one entry per line, either a short form or a public board link. An entry can also be an object with your own columns plus a board column, for example `{"company": "Ramp", "board": "ashby:ramp"}`.
2. **An existing dataset (`datasetId` + `boardField`)**: pick a dataset holding your account list. Your dataset is only read, never modified. `boardField` names the column with the board link; if empty, the Actor looks for `board`, `careersUrl`, `jobsUrl`, `ats`, `greenhouse`, `lever` or `ashby`.
3. **Find the board from the company website (`detectFromWebsite`, optional, best effort)**: for rows with no board, the Actor reads at most 2 public pages of the company site (`/careers`, then the home page) and looks for a Greenhouse, Lever or Ashby link. Board names are never guessed. The company's `robots.txt` is respected here; if a site disallows access, cannot be confirmed, or shows no board, the row is `ATS_NOT_DETECTED`.

Other options: `includeTechnologies` (default off), `trackChanges` and `stateStoreName` (history for change detection), `maxRows` (a cheap test cap), `maxConcurrency` (1 to 4).

### Enriching an existing dataset

Your original columns come first, in their original order, and the `ats*` columns are appended. If one of your columns already has an `ats*` name, ours is stored with an `enrichment_` prefix so nothing of yours is overwritten. Rows are never dropped: an unusable row stays in the output with its status and the reason. If two rows point to the same board, the second row repeats the first row's result, `atsDuplicateOf` holds the first row's `inputIndex`, and you are charged once.

Keep in mind that Apify deletes unnamed (default) datasets after a retention period (7 days at the time of writing, per Apify's storage rules), so export or forward the results you want to keep.

### Output

One row per input row. Example for `ashby:ramp`, produced by running this Actor's code on a recorded real Ashby response (top roles shortened):

```json
{
  "company": "Ramp",
  "website": "ramp.com",
  "atsStatus": "SUCCESS",
  "atsProvider": "ashby",
  "atsBoardKey": "ashby:ramp",
  "atsBoardUrl": "https://jobs.ashbyhq.com/ramp",
  "atsOpenRoles": 2,
  "atsRemoteRoles": 0,
  "atsRolesWithSalary": 1,
  "atsSalaryCoverage": 0.5,
  "atsRolesPublishedLast30Days": 0,
  "atsPublishedDateBasis": "publishedAt",
  "atsRoleFamilies": { "Sales": 1, "Security": 1 },
  "atsDepartments": [{ "name": "Engineering", "count": 1 }, { "name": "Sales", "count": 1 }],
  "atsWorkplaceTypes": { "remote": 0, "hybrid": 1, "onsite": 1, "unknown": 0 },
  "atsTopRoles": [{ "title": "Account Executive", "roleFamily": "Sales", "location": "London", "workplaceType": "onsite" }]
}
```

Column groups:

- **Identity**: `atsProvider`, `atsBoardToken`, `atsBoardKey`, `atsBoardUrl`, `atsCheckedAt`, `inputIndex`, `atsDuplicateOf`.
- **Result**: `atsStatus`, `atsStatusDetail`, `atsHttpStatus`, `atsNotes`.
- **Facts from the board**: `atsOpenRoles`, `atsRoleFamilies`, `atsDepartments`, `atsLocations`, `atsWorkplaceTypes`, `atsRemoteRoles`, `atsRolesWithSalary`, `atsSalaryCoverage`, `atsRolesPublishedLast30Days`, `atsPublishedDateBasis`, `atsTopRoles`.
- **Optional technology mentions**: `atsTechnologyMentions`.
- **Changes (needs history)**: `atsChangeComparison`, `atsBaselineAvailable`, `atsNewRoles`, `atsChangedRoles`, `atsRemovedRoles`, `atsChangeDetails`, `atsPreviousCheck`.

Unknown is `null`, never `0`.

#### How each number is calculated

| Column | Raw input | Rule | Result |
|---|---|---|---|
| `atsOpenRoles` | Postings listed on the public board | Count of postings returned right now | A fact about the board, **not** headcount and not "hiring momentum". |
| `atsRoleFamilies` | Posting title (and department) | Fixed keyword rules (Engineering, Sales, Security, ...). A bracketed qualifier never decides the family. | Counts per family. Rules are heuristic; unclear titles fall in `Other`. |
| `atsRemoteRoles` | The workplace type the board states | Number of postings declared remote | `null` when the board states no workplace type at all (for example Greenhouse list feeds). Otherwise it is a lower bound: postings with no stated type are not counted as remote. |
| `atsRolesWithSalary`, `atsSalaryCoverage` | Pay ranges published on postings | Postings with a range, and that share of open roles | `null` for Greenhouse (the feed has no pay data). |
| `atsRolesPublishedLast30Days` | Publish date per posting | Postings published in the 30 days before the check | `atsPublishedDateBasis` says which date the vendor provides. `null` if the vendor gives none. |
| `atsTechnologyMentions` | Posting descriptions (opt-in) | Per tool: `coreRoles` (named as required), `optionalRoles` (plus/preferred), `exampleRoles` (in an "e.g." or alternatives list) | A mention in a job posting is **not** evidence that the company uses the tool. |

### Statuses

| Status | Meaning | Does NOT mean | Charged |
|---|---|---|---|
| `SUCCESS` | Board read, roles found. | | Yes |
| `NO_PUBLIC_JOBS` | Board exists and lists zero roles right now. | Not proof the company is closed or not hiring. | Yes |
| `PARTIAL_SUCCESS` | Read, but truncated or incomplete (see notes). | | Yes |
| `ATS_NOT_DETECTED` | The value is not a supported board link, or nothing was found on the company site (detail: `NOT_AN_ATS_BOARD_URL`, `NO_ATS_EVIDENCE_FOUND`, `SKIPPED_ROBOTS_TXT`). | **Not** "not hiring". The company may use another ATS or a board we cannot see. | No |
| `SOURCE_UNAVAILABLE` | Board or host unreachable or not found (detail says which, for example `BOARD_NOT_FOUND`). | **Not** "no jobs". | No |
| `RATE_LIMITED` | The host throttled the request. Run again later. | | No |
| `INVALID_INPUT` | The row has no board value and website detection is off (detail: `NO_BOARD_VALUE`), or is otherwise unusable. The row is kept. | | No |
| `NOT_PROCESSED` | Skipped because of a time or cost limit. Run it again. | | No |
| `DUPLICATE_ROW` | Same board as an earlier row, and its result was no longer in memory (very large lists). `atsDuplicateOf` points to the first row. | | No |

### Change detection

Turn on `trackChanges` (default) and run the same list again. The first run stores a **baseline**: `atsChangeComparison` is `baseline` and `atsBaselineAvailable` is `false`, so no changes are reported. From the second run on, `atsBaselineAvailable` is `true` and `atsNewRoles`, `atsChangedRoles` and `atsRemovedRoles` are filled. `atsBaselineAvailable` is empty when history is off.

- A role counts as removed only after it is missing in **two** consecutive healthy runs (first it is `not_seen`).
- If more than half of at least 10 known roles disappear at once, the board is flagged `suspect` instead of reporting removals.
- A failed, partial or unavailable run never writes to history, so a source failure can never turn into mass closure.
- Greenhouse `updated_at` is ignored on purpose because it is bulk-touched.

History lives in a named key-value store in your own account (`stateStoreName`). Use a different name per account list.

### Limitations

- Only Greenhouse, Lever and Ashby. A company on another ATS is `ATS_NOT_DETECTED`.
- Role-family and technology extraction are keyword rules, audited on real boards but not guaranteed accurate. There is no labelled benchmark.
- Raw counts are facts about a public board, not a measure of company growth.
- Website detection is best effort and finds only boards linked from the checked pages.
- Greenhouse has no pay data in the public list feed; technology mentions on Greenhouse need heavier requests, so they are opt-in.
- Vendors publish no rate limits for these public GET endpoints; the Actor stays conservative (below).

### Pricing

Pay-per-event: **$0.003 per company processed** (event `company-processed`), plus Apify's standard Actor-start event of $0.00005 per run. Apify platform usage (compute, storage) is covered by the publisher, so these events are the whole price. A company is charged once per distinct board that was actually read (`SUCCESS`, `NO_PUBLIC_JOBS`, `PARTIAL_SUCCESS`). Nothing is charged for `ATS_NOT_DETECTED`, `SOURCE_UNAVAILABLE`, `RATE_LIMITED`, `INVALID_INPUT`, `NOT_PROCESSED` or `DUPLICATE_ROW`.

| Companies in your list | Maximum company charge (before the start event) |
|---|---|
| 10 | $0.03 |
| 100 | $0.30 |
| 1,000 | $3.00 |
| 10,000 | $30.00 |

These are upper bounds: if 40% of your rows are unavailable or have no ATS, you pay for the other 60%. Set a maximum cost per run in Run options to cap spending.

Measured on the Apify platform (owner runs, 1 GB memory): 10 companies about 6 s and 74 MB peak; 100 companies about 20 s and 101 MB; 1,000 companies about 5 minutes and 207 MB. Your own runs will vary with the boards you list.

### Use it from code and tools

Replace `<YOUR_API_TOKEN>` with your own Apify API token. The Actor ID is `yourname_mahi/ats-hiring-intelligence`.

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("yourname_mahi/ats-hiring-intelligence").call(run_input={
    "boards": ["ashby:ramp", "greenhouse:airbnb", {"company": "WeRide", "board": "lever:weride"}],
    "trackChanges": True,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["atsBoardKey"], row["atsStatus"], row["atsOpenRoles"])
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });
const run = await client.actor('yourname_mahi/ats-hiring-intelligence').call({
    boards: ['ashby:ramp', 'greenhouse:airbnb', 'lever:weride'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/yourname_mahi~ats-hiring-intelligence/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"boards": ["ashby:ramp", "greenhouse:airbnb", "lever:weride"]}'
```

#### n8n

Add the **Apify** node, choose **Run Actor**, select this Actor and paste your input JSON, then add a second Apify node, **Get dataset items**, using the run's default dataset. Each item is one company row you can send to your CRM.

#### Make

Use the **Apify** app: **Run an Actor**, then **Get Dataset Items**, or trigger a scenario from the run's webhook. Map `atsBoardKey`, `atsStatus` and `atsOpenRoles` into your spreadsheet or CRM module.

#### AI agents and MCP

Apify Actors can be called by AI agents through Apify's MCP server (`mcp.apify.com`) once an Actor is public. The input is a plain list of boards and the output is one predictable row per company, which suits tool use. Agentic payments (x402, Skyfire) need the publisher's identity verification; that status is **not verified** at the time of writing, so rely on a normal Apify account and token.

### Responsible use

- Only public job postings are read, over the vendors' public JSON feeds, at a low request rate with per-host limits, retries and a circuit breaker.
- Allowed hosts only: `boards-api.greenhouse.io`, `api.lever.co`, `api.eu.lever.co`, `api.ashbyhq.com`. HTTPS only, no redirects, private and local network addresses refused, 12 MB response cap.
- Website detection refuses IP-literal hosts and ports other than 80/443, follows at most 4 redirects, scans the first 3 MB of at most 2 pages, and respects `robots.txt`.
- You are responsible for how you use public job data and for the terms of the sites involved. Do not use the output to contact individuals; it contains no personal data about candidates or hiring managers.

### FAQ

**Is this a job scraper?** No. It returns one summary row per company in your list, not a list of jobs.

**Why is a company `ATS_NOT_DETECTED`?** You gave no board and none could be found, or the company uses another system. It does not mean the company is not hiring.

**Why is `atsRemoteRoles` empty?** The board states no workplace type (common on Greenhouse). Unknown is `null`, not 0.

**Why is `atsSalaryCoverage` empty on Greenhouse?** Greenhouse's public list feed has no pay data.

**Why were no changes reported on my second run?** Check `atsBaselineAvailable`. If it is `false`, that run created the baseline or the comparison was skipped because the board was not read healthily.

**Does it need a proxy or login?** No. It uses plain public HTTP requests.

**Will I be charged for failures?** No. Unavailable sources, undetected ATS, invalid rows and duplicates are free.

**Can I run it on a schedule?** Yes. Use Apify schedules with the same `stateStoreName` and `trackChanges` on to get weekly change reports.

# Actor input Schema

## `datasetId` (type: `string`):

Pick an existing dataset (for example the output of a lead scraper). Every row is kept with all its original columns; the hiring columns are added next to them. Your dataset is only read, never changed.

## `boardField` (type: `string`):

The column with a board link (https://jobs.lever.co/acme, https://boards.greenhouse.io/acme, https://jobs.ashbyhq.com/acme) or a short form such as ashby:acme, greenhouse:acme, lever:acme (EU Lever: lever-eu:acme). Leave empty to detect columns called board, careersUrl, jobsUrl, ats, greenhouse, lever or ashby.

## `detectFromWebsite` (type: `boolean`):

Only used for rows with no board. Reads at most 2 public pages per company (its /careers page, then its home page) looking for a Greenhouse, Lever or Ashby link. Finding nothing is reported as ATS\_NOT\_DETECTED and does NOT mean the company is not hiring. Explicit boards are always more reliable. The company site's robots.txt is respected; the public ATS APIs themselves are not affected by it.

## `websiteField` (type: `string`):

Column holding the company website or careers URL. Leave empty to auto-detect columns such as website, domain or url.

## `boards` (type: `array`):

One per line: a public board link or a short form like ashby:ramp, greenhouse:airbnb, lever:weride. Use this or the dataset above, or both.

## `includeTechnologies` (type: `boolean`):

Adds which tools (Python, AWS, Salesforce, ...) open roles MENTION, with evidence snippets. This is not proof of what the company uses. Slower for Greenhouse boards (full descriptions are large).

## `trackChanges` (type: `boolean`):

Keeps a small history in a storage of YOUR account so the next run can compare. The first run only creates the baseline. A role is reported as removed only after it is missing in two consecutive healthy runs.

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

Lowercase letters, digits and dashes. Use a different name to keep separate histories for separate account lists.

## `maxRows` (type: `integer`):

Optional cap, handy for a cheap test (for example 5). Empty or 0 = process everything.

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

How many boards are read at the same time. Lower it to be extra gentle; it cannot be raised above 4.

## Actor input object example

```json
{
  "detectFromWebsite": false,
  "boards": [
    "ashby:ramp",
    "greenhouse:airbnb",
    "lever:weride"
  ],
  "includeTechnologies": false,
  "trackChanges": true,
  "stateStoreName": "ats-hiring-intelligence-state",
  "maxConcurrency": 4
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "boards": [
        "ashby:ramp",
        "greenhouse:airbnb",
        "lever:weride"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yourname_mahi/ats-hiring-intelligence").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 = { "boards": [
        "ashby:ramp",
        "greenhouse:airbnb",
        "lever:weride",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("yourname_mahi/ats-hiring-intelligence").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 '{
  "boards": [
    "ashby:ramp",
    "greenhouse:airbnb",
    "lever:weride"
  ]
}' |
apify call yourname_mahi/ats-hiring-intelligence --silent --output-dataset

```

## MCP server setup

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

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/MNQWJRIGNpNcoSN9O/builds/kelaHcbRfL5ZjCqbK/openapi.json
