# Hiring Signals: Greenhouse, Lever & Ashby Jobs Scraper + AI (`craftedincode/hiring-intent-signals-ai`) Actor

Paste company domains. The Actor finds their Greenhouse, Lever or Ashby board, scrapes open jobs, and can add AI seniority, tech stack, salary and a company-level hiring signal. From $1 per 1,000 jobs on paid plans.

- **URL**: https://apify.com/craftedincode/hiring-intent-signals-ai.md
- **Developed by:** [Crafted in Code](https://apify.com/craftedincode) (community)
- **Categories:** Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job scrapeds

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

## Hiring Signals: Greenhouse, Lever & Ashby Jobs Scraper + AI

Paste a **company domain** and get the open jobs behind its **Greenhouse**, **Lever** or **Ashby** board, plus an optional **AI hiring-intent signal** for B2B sales, recruiting and competitive research. The Actor finds the board for you (careers-page links, then slug guesses), filters by date, department and location, and can diff **new jobs since the last run**.

AI is optional. With **AI job enrichment** and **company signals** turned off, or with no `GEMINI_API_KEY`, the Actor still scrapes and saves jobs. You are charged the primary `job-scraped` event only after a row is stored. AI events are charged only after the enriched row is stored and the model call succeeded.

### What data can you extract?

| Field | On a job | On a company signal |
| --- | --- | --- |
| Company, domain, ATS, board token, job URL | Yes | Company, domain, ATS |
| Title, department, team, location, remote, employment type | Yes | — |
| Posted and updated time | Yes | — |
| Description text (recruiter names, emails and phones removed) | Yes | — |
| Raw salary text from the ATS, or a salary sentence in the posting | Yes | — |
| Normalized salary (min, max, currency, period) | When that text parses. AI fills these only if parsing found nothing | — |
| AI seniority, function, tech stack, tools, visa, summary | When AI is on | — |
| Open roles, roles by function, new since last run, top tech stack | — | Yes |
| Buying signals with at least one evidence job URL, relevance 0–100, outreach angle | — | When company-signal AI succeeds |

`itemType` is `job` or `company-signal`. Use the **Jobs** and **Company signals** dataset views to switch.

### How to scrape hiring signals

1. Set **Company domains** (`stripe.com`, `https://www.figma.com/careers`) and/or **Board tokens** if you already know the slug (`greenhouse` / `stripe`, `lever` / `palantir`, `ashby` / `ramp`).
2. Optionally filter with **Departments**, **Locations** and **Posted within days**. Jobs with no posted date are kept.
3. Leave **Only new since last run** off for a full pull. Turn it on for a schedule: the first successful fetch of a board stores every job id in the named key-value store `hiring-intent-seen-jobs`, and later runs return only ids that were not on that snapshot.
4. Leave AI off for a cheap scrape. Turn on **AI job enrichment** and/or **Company hiring signals**, and set **Signal focus** to what you sell (for example `we sell data-warehouse tooling`).
5. Run. Open the dataset for rows, and the key-value record `RUN-STATS` for counters, warnings and boards that came back empty.

The prefilled input is a small demo: `stripe.com` and `figma.com`, 20 jobs per company, AI off. That is what the Store's daily test runs. It should finish with a non-empty dataset in well under five minutes.

#### Input example

```json
{
  "companies": ["stripe.com", "figma.com"],
  "boardTokens": [{ "ats": "ashby", "token": "ramp" }],
  "departments": ["engineering"],
  "locations": ["remote"],
  "postedSinceDays": 30,
  "onlyNewSinceLastRun": false,
  "aiJobEnrichment": true,
  "companySignals": true,
  "signalFocus": "we sell data-warehouse tooling",
  "maxJobsPerCompany": 300,
  "maxAiJobs": 2000
}
```

Pass `"companies": []` when you only want explicit board tokens. A domain and a token that resolve to the same board are scraped once.

#### Output example

```json
{
  "itemType": "job",
  "company": "Stripe",
  "domain": "stripe.com",
  "ats": "greenhouse",
  "boardToken": "stripe",
  "jobId": "8023928",
  "title": "Account Executive, Bridge",
  "department": "Sales",
  "team": null,
  "location": "London",
  "remote": false,
  "employmentType": null,
  "postedAt": "2026-07-30T10:59:38.000Z",
  "updatedAt": "2026-08-04T11:02:31.000Z",
  "url": "https://stripe.com/jobs/search?gh_jid=8023928",
  "descriptionText": "Who we are ...",
  "salaryRaw": null,
  "isNew": true,
  "aiStatus": "skipped",
  "seniority": null,
  "function": null,
  "techStack": null,
  "summary": null
}
```

Company signal (AI on):

```json
{
  "itemType": "company-signal",
  "company": "Stripe",
  "domain": "stripe.com",
  "ats": "greenhouse",
  "openRoles": 42,
  "rolesByFunction": { "Engineering": 18, "Sales": 9 },
  "newSinceLastRun": 3,
  "topTechStack": ["Python", "Go", "Kubernetes"],
  "buyingSignals": [
    {
      "signal": "Data platform roles mention Snowflake and dbt.",
      "category": "data",
      "evidenceUrls": ["https://stripe.com/jobs/search?gh_jid=8023928"]
    }
  ],
  "relevanceToFocus": 76,
  "outreachAngle": "They are staffing a warehouse team, which lines up with a tooling conversation.",
  "aiStatus": "enriched"
}
```

`openRoles` counts jobs that match your filters, before `maxJobsPerCompany`. Saved job rows are the newest of those, up to the cap. `relevanceToFocus` is null when **Signal focus** is empty.

### How much does it cost?

Pay per event. Paid plans (Bronze, Silver, Gold, Platinum, Diamond) share one price. There is **no** `apify-default-dataset-item` charge. The platform still adds its standard Actor-start compute event.

| Event | When it is charged | Free plan | Paid plans |
| --- | --- | --- | --- |
| `job-scraped` (primary) | After a job row is saved | $0.0015 | $0.001 |
| `job-ai-enriched` | After that job row is saved and Gemini filled the AI fields | $0.006 | $0.003 |
| `company-signal` | After the company row is saved and the AI signal was produced | $0.08 | $0.03 |

Worked examples on a **paid** plan:

| Run | Events | Price |
| --- | --- | --- |
| 1,000 jobs, AI off | 1,000 × job-scraped | **$1.00** |
| 1,000 jobs, all AI enriched | 1,000 × job-scraped + 1,000 × job-ai-enriched | **$4.00** |
| 10 company signals | 10 × company-signal | **$0.30** |
| 1,000 AI jobs + 10 signals | all three | **$4.30** |

On the free plan the same 1,000-job sticker would be $1.50, but a free run is capped (below) so it cannot reach that volume. Free-plan users do not generate developer revenue; the caps exist so Gemini spend stays bounded. A stats-only company row (AI off, or the model call failed) is saved and **not** charged as `company-signal`.

The run also stops when your **max total charge** (`maxTotalChargeUsd` / the charging manager) cannot fit the next event. Rows already saved stay in the dataset. The run ends successfully with `stoppedReason: "charge-limit"` in `RUN-STATS`.

The finish log and `estimatedChargeUsd` use the Actor price tier, not the paid sticker by default. A paying Apify account (`APIFY_USER_IS_PAYING=1`) can still be on the Actor **FREE** tier (`APIFY_USER_PRICING_TIER=FREE`), which is $0.0015 per job. Forty jobs on that tier are **$0.0600**, not $0.0400. The log names the tier and the unit price. If the charging manager reports a numeric event price, that price is used instead. Volume caps (200 / 20 / 2) still follow the free Apify plan, separate from this price.

### Free-plan limits

Detected with `APIFY_USER_IS_PAYING` (`1` means a paid plan, `0` means free). A platform run with the variable missing is treated as free. Local runs without the variable are not capped.

| Cap | Per run on the free plan |
| --- | --- |
| Jobs saved | 200 |
| AI-enriched jobs | 20 |
| Company signals | 2 |

Companies are handled in input order. Put the accounts you care about first. When a cap is hit the Actor logs a clear warning, writes it into `RUN-STATS`, and finishes without failing the run.

`maxAiJobs` is split across companies. The remainder goes to earlier companies. If a company has fewer jobs than its share, the unused slots are redistributed the same way.

### Limits and behaviour

- One HTTP call per board. No browser. Greenhouse uses `content=true`, Lever uses `mode=json`, Ashby uses `includeCompensation=true`.
- Descriptions are plain text, capped at 30,000 characters. Recruiter emails, phone numbers and names in recruiting sentences are replaced with `[redacted]`.
- If a confirmed board is empty or errors, the Actor retries 3 times with backoff (400ms, 800ms), logs a warning that names the company and ATS, and records it under `emptyCompanies` in `RUN-STATS`. That company does not fail the run.
- Newest `postedAt` is kept when a cap applies. Jobs with no date sort last and are kept when a date filter is on.
- The seen-jobs snapshot is the full board id list when a company finishes. "New" means the id was not on the previous board snapshot, not "it did not fit in maxJobsPerCompany last time".
- Default memory is **512 MB**. Raise it only if you pass a very long company list with full descriptions.
- AI model defaults to `gemini-3.5-flash-lite` (`GEMINI_MODEL`). The client sends JSON schema output and does **not** send `thinkingConfig` (that field returns 400 on gemini-3.5). Failures retry 3 times, then the row is saved with `aiStatus: "failed"` and the AI event is not charged.

### FAQ

**Is this legal?** The Actor calls the public, documented Greenhouse Job Board API, Lever Postings API and Ashby Job Posting API. It does not log in, does not use a browser, and does not submit applications. Recruiter names and emails found in descriptions are stripped before storage and before any model call. This is not legal advice; you are responsible for how you use the data.

**How is this different from other ATS scrapers?** Several Store Actors scrape one ATS, or require you to already know the board token, and they often have no date filter. This Actor starts from a domain, covers Greenhouse, Lever and Ashby, diffs new jobs for scheduled monitoring, and adds an optional company-level buying signal priced below older $0.05 "intent" Actors. Raw job price on paid plans is $0.001, under typical $0.0015–$0.0025 competitors.

**What if the company does not use these three ATS products?** The run continues. `RUN-STATS.warnings` explains that no board was detected. Other systems (Workday, Lever-hosted custom domains, iCIMS) are out of scope.

**Will AI run without a key?** No. Scraping still runs. Set the secret `GEMINI_API_KEY` on the Actor to enable enrichment. Use a paid Gemini key so prompts are not used to train Google's models.

**Can I schedule "only new jobs"?** Yes. Turn on **Only new since last run**, save a task, and schedule it. Leave it **off** for the Store demo and for any backfill. The first run returns the current matching jobs and writes the baseline.

**Does one bad company fail the run?** No. Empty boards, HTTP errors and detection misses are warnings. Other companies are still saved.

**How do I connect it?** Run it from the Apify API, schedules, Make, n8n, Zapier or the Apify MCP. The dataset is the integration output. `RUN-STATS` in the default key-value store is the run summary. Seen-job ids live in the named store `hiring-intent-seen-jobs`, which persists across runs for that user.

### RUN-STATS

```json
{
  "jobsPushed": 40,
  "jobsAiEnriched": 0,
  "companySignalsCharged": 0,
  "warnings": [],
  "emptyCompanies": [],
  "chargeLimitReached": false,
  "stoppedReason": "completed",
  "freePlan": false,
  "chargeTier": "FREE",
  "jobScrapedPriceUsd": 0.0015,
  "chargePriceSource": "pricing-tier",
  "estimatedChargeUsd": 0.06
}
```

`stoppedReason` is `completed`, `charge-limit` or `free-plan-cap`.

# Changelog

This Actor's version history is a separate document: https://apify.com/craftedincode/hiring-intent-signals-ai/changelog.md

# Actor input Schema

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

Company websites, for example stripe.com or https://www.figma.com/careers. The Actor sniffs the careers page for a Greenhouse, Lever or Ashby link, then guesses the board slug. Pass an empty array if you only want board tokens.

## `boardTokens` (type: `array`):

Explicit public board slugs when you already know the ATS. Example: Greenhouse token stripe, Lever token palantir, Ashby token ramp. Duplicates of a domain that resolves to the same board are scraped once.

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

Optional case-insensitive substrings. A job is kept when its department, team or title contains any of them. Leave empty to keep every department.

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

Optional case-insensitive substrings matched against the job location. The word remote also matches jobs flagged as remote. Leave empty to keep every location.

## `postedSinceDays` (type: `integer`):

Keep jobs whose posted date is within the last N days. Jobs with no posted date are kept. Leave empty to keep every open job.

## `onlyNewSinceLastRun` (type: `boolean`):

When true, return only jobs that were not on this board the last time this Actor saved a snapshot in the named store hiring-intent-seen-jobs. The first run for a board returns every matching job and stores the snapshot. Leave this off for the Store's daily test.

## `aiJobEnrichment` (type: `boolean`):

Call Gemini for each output job and add seniority, function, tech stack, tools, normalized salary, visa sponsorship and a short summary. Requires the GEMINI_API_KEY secret. The Actor still scrapes jobs when this is off or the key is missing.

## `companySignals` (type: `boolean`):

Save one company-level row per board: open roles, roles by function, new-since-last-run count, tech stack, and (when Gemini is available) buying signals with evidence job URLs, a 0–100 relevance score and an outreach angle. The AI company-signal event is charged only after that AI row is saved.

## `signalFocus` (type: `string`):

What you sell, in plain language. Used to score relevanceToFocus from 0 to 100 and to write the outreach angle. Example: we sell data-warehouse tooling. Ignored when company signals are off.

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

Cap on jobs saved per company after filters, newest posted date first. The prefilled demo uses 20 so the daily test stays small. When the field is omitted, the Actor uses 300.

## `maxAiJobs` (type: `integer`):

Maximum jobs to send to Gemini in this run, split fairly across companies. Remainder goes to earlier companies. Unused share is redistributed. Free-plan runs are also capped at 20 AI jobs. When the field is omitted, the Actor uses 2000.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "figma.com"
  ],
  "boardTokens": [],
  "departments": [],
  "locations": [],
  "onlyNewSinceLastRun": false,
  "aiJobEnrichment": false,
  "companySignals": false,
  "maxJobsPerCompany": 20,
  "maxAiJobs": 2000
}
```

# Actor output Schema

## `dataset` (type: `string`):

Job rows (itemType job) and company hiring-signal rows (itemType company-signal).

## `runStats` (type: `string`):

Per-run counters, warnings, and companies whose ATS board was empty or failed.

## `keyValueStore` (type: `string`):

Default key-value store for this run. RUN-STATS is stored here. Seen-job snapshots live in the named store hiring-intent-seen-jobs.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "stripe.com",
        "figma.com"
    ],
    "boardTokens": [],
    "departments": [],
    "locations": [],
    "onlyNewSinceLastRun": false,
    "aiJobEnrichment": false,
    "companySignals": false,
    "signalFocus": "",
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("craftedincode/hiring-intent-signals-ai").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companies": [
        "stripe.com",
        "figma.com",
    ],
    "boardTokens": [],
    "departments": [],
    "locations": [],
    "onlyNewSinceLastRun": False,
    "aiJobEnrichment": False,
    "companySignals": False,
    "signalFocus": "",
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("craftedincode/hiring-intent-signals-ai").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "stripe.com",
    "figma.com"
  ],
  "boardTokens": [],
  "departments": [],
  "locations": [],
  "onlyNewSinceLastRun": false,
  "aiJobEnrichment": false,
  "companySignals": false,
  "signalFocus": "",
  "maxJobsPerCompany": 20
}' |
apify call craftedincode/hiring-intent-signals-ai --silent --output-dataset

```

## MCP server setup

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

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/2Fa1MHVl3rhxR9Aab/builds/3kN05IJepDXPx1A60/openapi.json
