# Hiring Intent Scraper — Greenhouse, Lever & Ashby ATS (`powernaner/hiring-intent-scraper`) Actor

Turn public ATS job boards into company-level hiring intent signals. Detects Greenhouse, Lever, Ashby, Workable and Recruitee from a company domain and returns one scored row per company — new roles, top department and growth velocity — not a list of jobs.

- **URL**: https://apify.com/powernaner/hiring-intent-scraper.md
- **Developed by:** [Renan Teles](https://apify.com/powernaner) (community)
- **Categories:** Lead generation, Jobs, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 company returneds

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 Intent Scraper — Greenhouse, Lever & Ashby ATS

Turn public ATS job boards into company-level hiring intent data. One scored row per company — not a job list.

***

### What does Hiring Intent Scraper do?

Most job scrapers hand you a pile of job postings and leave the analysis to you. This one answers the question you actually have: **which of these companies is scaling right now, in which department, and how fast?**

Give it a list of company domains. It finds each company's applicant tracking system (ATS), reads the public job board, and returns **one scored row per company** with a hiring intent signal — new roles in your window, the department they cluster in, seniority mix, and a 0–100 score.

No login. No API key. No browser automation. It reads the same public feeds that job aggregators read.

Hiring Intent Scraper allows you to:

- **Detect hiring surges** before they show up on job boards or LinkedIn
- **Score companies** by hiring intensity relative to their size, not raw headcount
- **See where the money is going** — the department that new roles cluster in
- **Filter out frontline noise** — store, warehouse and shift roles are classified separately from corporate hiring
- **Work from domains**, not board URLs — ATS detection is automatic
- **Feed a CRM or sequencer** with timing signals, not static firmographics

***

### Why one row per company, not one row per job?

Here is the same company, both ways.

**Gopuff** has **784 open roles**. A job scraper returns 784 rows and an SDR opens the file thinking there is a hiring surge happening.

This Actor returns **one row**:

| company | signal | score | new roles (30d) | top department |
|---|---|---|---|---|
| Gopuff | quiet | 7 | 5 | marketing |

Gopuff opened 24 roles in the window. **Nineteen were frontline** — delivery, warehouse, store. Corporate hiring: five roles. That is not a surge, and the score says so.

Compare a real signal from the same run:

| company | signal | score | new roles (30d) | top department |
|---|---|---|---|---|
| Elastic | hot | 83 | 189 | engineering |
| Stripe | growing | 51 | 248 | sales |
| Ramp | steady | 50 | 48 | sales |
| Sweetgreen | steady | 28 | 7 | operations |
| Linear | quiet | 18 | 4 | engineering |
| Gopuff | quiet | 7 | 5 | marketing |

Six rows, six decisions. Sweetgreen — a restaurant chain — scores 28 despite opening 27 roles, because 20 of them are store-level and classified as frontline. Elastic tops the list with 137 of its 189 new roles in engineering.

That table is the product.

***

### What data can I extract?

| 🏢 Company identity | 📊 Hiring volume |
|---|---|
| Company name | Total open roles |
| Domain | New roles in window |
| ATS platform | Window length (days) |
| Job board URL | Roles truncated flag |

| 🎯 Signal | 🧭 Breakdown |
|---|---|
| Signal score (0–100) | New roles by department |
| Signal label | New roles by seniority |
| Top department | Remote ratio |
| Detection confidence | Role class split (corporate / frontline) |

| 📝 Context | ⏱️ Metadata |
|---|---|
| Sample job titles | Scraped at |
| Top department count | Role classes included |

Optionally, set `includeJobRows` to also get the individual normalized job rows behind each company's score.

***

### Which ATS platforms are supported?

| ATS | Supported |
|---|---|
| Greenhouse | ✅ |
| Lever | ✅ |
| Ashby | ✅ |
| Workable | ✅ |
| Recruitee | ✅ |
| SmartRecruiters | ✅ |

**Expect roughly 50% coverage on a mixed domain list.** Measured: on a list of 179 mixed domains, 95 resolved. Many companies run Workday, Taleo, SuccessFactors or a custom careers page — those are not supported and are reported as not detected.

**Domains where no supported ATS is found are never charged.** You pay only for companies that were actually analysed.

***

### How much will it cost?

| Event | Price |
|---|---|
| Company returned | $1.50 / 1,000 |
| Company screened | $0.20 / 1,000 |
| Actor start | $0.00005 |

Platform usage (compute, storage) is **included** — no separate usage bill.

**Company screened** fires for every company successfully analysed. **Company returned** fires for every row actually delivered to your dataset after your filters.

What that looks like in practice:

| Input | Cost |
|---|---|
| 100 domains, ~50 detected | $0.09 |
| 1,000 domains, ~500 detected | $0.85 |
| 5,000 domains, ~2,500 detected | $4.25 |

The Apify Free plan's monthly credits cover a few thousand domains per month.

***

### How to use Hiring Intent Scraper

No coding required.

1. Create a [free Apify account](https://console.apify.com/sign-up)
2. Open Hiring Intent Scraper
3. Paste your company domains into the **Domains** field — one per line, `stripe.com` style
4. Set **Window days** (default 30) and, if you want, a **Minimum signal score**
5. Click **Start** and wait
6. Download the results as JSON, CSV, Excel or HTML — or pull them through the API

The output opens in a **Call list** view by default: company, signal, score, new roles, top department.

***

### How to use hiring intent data

- **Time your outbound.** A company that opened five sales roles this month is building a revenue team right now. Reach them while the budget is being spent, not after.
- **Prioritise a cold list.** Run 1,000 domains you already have and sort by score. Work the top 5% first.
- **Find agency clients.** A company with a hiring surge and no recruiting team is a staffing lead.
- **Track accounts.** Schedule a weekly run on your target list and watch which accounts turn `hot`.
- **Research a market.** Aggregate by department to see where an entire sector is investing.
- **Feed an AI agent.** Clean, typed JSON with no login, ready for your pipeline.

***

### Input parameters

To run this Actor you **must** provide one field:

**Domains** (`domains`) — an array of company domains or job board URLs, e.g. `["stripe.com", "ramp.com"]`

Everything else is optional:

- **Window days** (`windowDays`) — how far back a role counts as "new". Default `30`, range 7–180.
- **Role classes** (`roleClasses`) — `corporate`, `frontline`, or both. Default: `corporate` only.
- **Departments** (`departments`) — restrict the analysis to specific departments (engineering, sales, marketing, product, design, data, finance, hr, operations, support, legal, clinical, other). Default: all.
- **Minimum signal score** (`minSignalScore`) — only return companies scoring at or above this. Default `0`, range 0–100.
- **Max companies** (`maxCompanies`) — cap the number of companies processed. Default `100`, range 1–5,000.
- **Max roles per company** (`maxRolesPerCompany`) — cap roles collected per company. Default `500`, range 10–2,000.
- **Max concurrency** (`maxConcurrency`) — parallel companies. Default `3`, range 1–10. Kept low on purpose: these are other people's public endpoints.
- **Include job rows** (`includeJobRows`) — also write the individual normalized jobs. Default `false`.

***

### Input examples

**Basic — score a prospect list**

```json
{
  "domains": ["stripe.com", "ramp.com", "elastic.co", "linear.app"],
  "windowDays": 30
}
```

**Only the hot ones, sales hiring only**

```json
{
  "domains": ["stripe.com", "ramp.com", "elastic.co"],
  "windowDays": 30,
  "departments": ["sales"],
  "minSignalScore": 50
}
```

**Include frontline hiring (retail, warehouse, shift roles)**

```json
{
  "domains": ["gopuff.com", "sweetgreen.com"],
  "windowDays": 60,
  "roleClasses": ["corporate", "frontline"]
}
```

***

### Results

Each company becomes one item in the dataset. This is a real row, copied from a run:

```json
{
  "company": "Elastic",
  "domain": "elastic.co",
  "ats_type": "greenhouse",
  "detection_confidence": "high",
  "job_board_url": "https://job-boards.greenhouse.io/elastic",
  "total_open_roles": 358,
  "window_days": 30,
  "new_roles_window": 189,
  "new_roles_by_department": {
    "finance": 9,
    "engineering": 137,
    "other": 3,
    "data": 1,
    "marketing": 4,
    "hr": 1,
    "support": 4,
    "sales": 30
  },
  "new_roles_by_seniority": {
    "lead": 59,
    "senior": 76,
    "mid": 47,
    "junior": 1,
    "director": 5,
    "vp": 1
  },
  "new_roles_by_role_class": {
    "corporate": 189,
    "frontline": 0
  },
  "role_classes_included": [
    "corporate"
  ],
  "new_roles_remote_ratio": null,
  "top_department": "engineering",
  "top_department_count": 137,
  "signal_score": 83,
  "signal_label": "hot",
  "sample_titles": [
    "Consulting Architect - Search",
    "Consulting Architect - Security (Canberra)",
    "Consulting Architect - Security (EMEA / Public Sector eligible )",
    "Customer Architect",
    "Senior Customer Architect - West"
  ],
  "roles_truncated": false,
  "scraped_at": "2026-09-20T22:31:56.869Z"
}
```

A `RUN_SUMMARY` record is also written to the key-value store with per-ATS counts, parsing health and the list of domains that could not be resolved.

***

### Notes for developers

#### How the score works

`signal_score` is intensity relative to company size, not raw volume. A 50-person startup opening five sales roles scores far above a 5,000-person company opening five.

Three components:

- **Intensity** — how much of the board is new, with Bayesian shrinkage so a company with one total role and one new role does not score 100
- **Concentration** — whether new roles cluster in a single department
- **Seniority** — lead and director hiring suggests a team being built, and acts as a tiebreaker

Absolute volume is deliberately **excluded**. If you want to rank by raw volume, sort by `new_roles_window` — it is in the output.

Labels are calibrated by percentile against a real run of 179 mixed domains: `hot` ≈ top 8%, `growing` ≈ next 23%, `quiet` ≈ bottom 24%. `hot` is rare by design — a label that applies to a third of your list tells you nothing.

#### Intent tags

`top_department: "sales"` is a fact. `intent_tags: ["scaling-sales"]` is a
verdict — and a verdict you can filter on.

The rules are deterministic, not a language model: a department tag needs at
least 5 new roles in the window, at least 3 in that department, and either 35%
concentration **or** 20 roles in absolute terms. The absolute clause exists
because 23 sales roles is a real commercial buildout even when the company is
hiring everywhere else.

`building-leadership` requires director level or above — `Staff` and `Principal`
are senior individual contributors, and "scaling engineering" already says that.

At most 3 tags per company, strongest concentration first. Measured on a real
run of 95 companies: 61% get no tag, 28% get one, 10% get two or three. **An
empty array is the normal case** — a label that applied to most rows would tell
you nothing.

#### Corporate vs frontline

Classification uses the ATS department field first and the job title as fallback. A restaurant chain that names departments after store addresses still gets classified correctly, because the title rules catch it.

#### Limits

- Up to 500 roles collected per company by default, and an internal ceiling of 25 pagination requests per company. When either is hit, `roles_truncated` is `true`.
- Greenhouse, Lever, Ashby and Recruitee return everything in one request. Workable paginates 10 at a time and SmartRecruiters 100 at a time; both are sorted newest-first, so the Actor stops as soon as it passes your window. A 4,800-role SmartRecruiters board costs about 5 requests for a 30-day window instead of 49 for the whole board.
- **`new_roles_remote_ratio` can be `null`.** Lever, Ashby, Workable and Recruitee expose a remote flag, so the ratio is exact. Greenhouse does **not** — remote status there is inferred from the location text. When no Greenhouse location says remote, the field is `null`, meaning *unknown*, not zero. A remote-first company that lists roles as "United States" is unknowable from the feed, and the output says so rather than claiming 0% remote.
- `posted_at` is the first-published date from the ATS. Some companies refresh or repost roles; a repost can read as new.
- Department and seniority are inferred from job titles and the ATS department field. Measured on 3,100 real postings, 2% fall into `other`. Non-English titles land in `other` more often.
- One run, one snapshot. This version reads `posted_at` and does not compare against previous runs. Schedule it and diff the datasets yourself if you need trend lines.
- The run-level spend cap you set in Apify is a **stop signal**, not a hard ceiling — a charge that crosses the limit is allowed before the Actor stops. Measured: a $0.005 cap billed $0.00535, a $0.02 cap billed $0.02045. The overage is bounded by roughly the price of one company and does not scale with the cap.

#### Detection

ATS detection is cached, with a TTL, so repeat runs on the same list are cheaper. Negative results expire after a day, so a company that adopts a supported ATS — or one that was missed during a brief ATS outage — is picked up on a later run.

***

### FAQ

**Is this legal?**
The Actor reads public job board feeds that ATS platforms publish for aggregators to consume. No login, no authentication, no paywalled content. As with any scraping, review the terms of the sites you target and your own compliance requirements.

**Do I need proxies?**
No. These are public JSON feeds and the Actor does not require residential proxies.

**Why did half my domains return nothing?**
They likely use an ATS that is not supported yet — Workday, Taleo, SuccessFactors or a custom careers page. Roughly 50% coverage on a mixed list is expected. You are not charged for those domains.

**Can I get the individual job postings?**
Yes — set `includeJobRows` to `true` and the normalized job rows are written alongside the company rows.

**How fresh is the data?**
Every run reads the live job board. Nothing is served from a stale index.

**Can I schedule it?**
Yes. Schedule a weekly run on your target account list and watch which accounts move from `quiet` to `hot`.

**Can I use it through the API?**
Yes. See the API tab for code examples in JavaScript and Python, or use the `apify-client` package.

**What if an ATS changes its format?**
A daily canary run covers all five platforms — two domains each for Greenhouse, Lever and Ashby, one each for Recruitee and Workable — and fails loudly when a feed changes shape, so breakage is caught before it reaches your results.

# Actor input Schema

## `domains` (type: `array`):

Company domains (stripe.com) or job board URLs (jobs.lever.co/gopuff). The Actor detects which ATS each company uses — Greenhouse, Lever, Ashby, Recruitee or Workable. Companies on an unsupported ATS are reported as failures and are never charged.

## `windowDays` (type: `integer`):

How far back a job posting still counts as 'new'. Everything in the output — the score, the department mix, the seniority mix — is measured over this window, not over the whole job board. 30 days is the default because it matches a typical sales cycle.

## `roleClasses` (type: `array`):

Corporate roles are engineering, sales, marketing, finance and the like. Frontline roles are store, warehouse, delivery and shift work. A retailer hiring 20 store associates is replacing turnover, not scaling — so frontline is excluded by default. Excluded roles are still counted in new\_roles\_by\_role\_class, never hidden.

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

Leave empty for all departments. Filtering narrows the score to the area you sell into — pick Engineering to find companies growing their tech team, Sales to find companies building a revenue org.

## `minSignalScore` (type: `integer`):

Drop companies scoring below this. 0 returns everything, including quiet companies. Try 40 to keep only companies actually growing. Note: filtered-out companies are still analysed, so they still count towards cost.

## `maxCompanies` (type: `integer`):

Hard cap on how many companies get analysed, so a long input list cannot surprise you with a bill. The run stops cleanly when it hits this.

## `maxRolesPerCompany` (type: `integer`):

Cap on job postings counted per company, within the window. A second, non-configurable cap of 25 pagination requests also applies. On Workable — the only ATS that paginates 10 at a time — that request cap means roughly 250 roles per company. When either cap bites, the row is flagged with roles\_truncated: true.

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

How many companies to process at once. Kept low on purpose: these are free public endpoints run by other people, and hammering them is how they get closed. Raise only if you are running a large list and have checked the ATS is coping.

## `includeJobRows` (type: `boolean`):

Off by default — the product is one row per company. Turn this on to also write one row per job posting, for when you need the raw postings behind a score. This is billed as a separate, cheaper event per job row.

## `healthCheck` (type: `boolean`):

For scheduled monitoring, not for normal use. Fails the run when the ATS platforms change shape: detection drops, posted\_at goes missing, or too many roles fall into the 'other' department. The failure message names which ATS broke.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "ramp.com",
    "vercel.com",
    "linear.app"
  ],
  "windowDays": 30,
  "roleClasses": [
    "corporate"
  ],
  "minSignalScore": 0,
  "maxCompanies": 100,
  "maxRolesPerCompany": 500,
  "maxConcurrency": 3,
  "includeJobRows": false,
  "healthCheck": false
}
```

# Actor output Schema

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

One scored row per company: signal score and label, new roles in the window, top department, seniority mix and remote share.

## `runSummary` (type: `string`):

Counts per ATS, per-ATS parsing health, billing state, and every domain that could not be resolved with the reason why.

# 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 = {
    "domains": [
        "stripe.com",
        "ramp.com",
        "vercel.com",
        "linear.app"
    ],
    "windowDays": 30,
    "roleClasses": [
        "corporate"
    ],
    "minSignalScore": 0,
    "maxCompanies": 100,
    "maxRolesPerCompany": 500,
    "maxConcurrency": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("powernaner/hiring-intent-scraper").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 = {
    "domains": [
        "stripe.com",
        "ramp.com",
        "vercel.com",
        "linear.app",
    ],
    "windowDays": 30,
    "roleClasses": ["corporate"],
    "minSignalScore": 0,
    "maxCompanies": 100,
    "maxRolesPerCompany": 500,
    "maxConcurrency": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("powernaner/hiring-intent-scraper").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 '{
  "domains": [
    "stripe.com",
    "ramp.com",
    "vercel.com",
    "linear.app"
  ],
  "windowDays": 30,
  "roleClasses": [
    "corporate"
  ],
  "minSignalScore": 0,
  "maxCompanies": 100,
  "maxRolesPerCompany": 500,
  "maxConcurrency": 3
}' |
apify call powernaner/hiring-intent-scraper --silent --output-dataset

```

## MCP server setup

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

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/F02SAmCDgdup5SQcY/builds/p7odQ0OGwxtSOHauw/openapi.json
