# Job Board Hiring Signals: Greenhouse, Lever, Ashby (`propheto/job-board-hiring-signals`) Actor

Find companies hiring for the tools you sell or support. Reads public Greenhouse, Lever and Ashby job boards, flags posts that mention your tools with the matching sentence as proof, and tracks changes week over week. Also returns clean, filtered job listings.

- **URL**: https://apify.com/propheto/job-board-hiring-signals.md
- **Developed by:** [Will Emery](https://apify.com/propheto) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 hiring signals

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

### What is Job Board Hiring Signals: Greenhouse, Lever, Ashby?

Job Board Hiring Signals: Greenhouse, Lever, Ashby is an Apify Actor that reads **public job listings** from company **career sites** through the official, unauthenticated job-board APIs of Greenhouse, Lever, and Ashby. Only those three ATS platforms are supported.

The default mode is **signals** (hiring-signal mode): you name a tool or technology (for example, Snowflake or Salesforce), and the Actor flags companies whose posts mention it, with the matching sentence, a signal type, and a confidence score. A second **jobs** mode returns a normalized job feed from the same boards, without the matcher.

This Actor is not affiliated with Greenhouse, Lever, Ashby, or any tool vendor named in an example. It only reads what those public APIs already publish.

**Try it:** open the Actor, leave the prefilled hiring-signal example (Duolingo on Greenhouse, Spotify on Lever, Linear and Supabase on Ashby, plus a short tool list), and click Start. The prefilled run finishes in well under a minute (about 10–12 seconds on Apify at 512 MB). You can also call it from the Apify API, schedule it weekly, attach a webhook, or export the dataset as JSON or CSV.

Main features:

- Official Greenhouse, Lever, and Ashby job-board APIs (no API key)
- Hiring-signal mode is the default: tool aliases, section-aware matching, confidence, and a snippet of evidence
- Jobs mode: a normalized listing feed from the same boards
- Filters for keywords, location, remote, department, posting date, published pay, and inferred experience
- Optional company-name discovery that only probes official board endpoints (no search-engine scraping)
- Scheduled runs with a `stateKey` so only new or changed signals are emitted
- In signals mode, `removeRepeatedText` (on by default) skips repeated employer boilerplate so it does not inflate signal counts
- Pay-per-event pricing: $0.003 per hiring signal, $0.001 per job, $0.01 per company rollup when the company has at least one signal (see pricing below)

### How to try it

1. Open this Actor in Apify Console. A free Apify account is enough to run the prefill.
2. Leave the prefilled input: `mode` is `signals` (the default), `boards` is Duolingo (`greenhouse` / `duolingo`), Spotify (`lever` / `spotify`), Linear (`ashby` / `linear`), and Supabase (`ashby` / `supabase`), `tools` includes `snowflake`, `dbt`, `salesforce`, `workday`, and `go`, and `maxItems` is 20. These are verified public boards. This run takes about 10–12 seconds on Apify and writes signal and rollup rows.
3. Click **Start** and wait for a Succeeded run.
4. Open the **Output** tab. In signals mode, use the **Signals** view; company totals appear in the **Company rollups** view. Jobs mode uses the **Jobs** view. A free summary row is always included (**Run summary** view).
5. Export JSON or CSV, or pass the dataset URL into a spreadsheet, webhook, or your own job.

For a weekly watch, save the input with a fixed `stateKey` and add an Apify Schedule (for example Monday 06:00 in your timezone). Attach a webhook on `ACTOR.RUN.SUCCEEDED` if you want a Slack or email ping.

The Actor's default memory is 512 MB (minimum 256 MB, maximum 1024 MB). We recommend 1024 MB for lists of 50 or more boards.

### Use cases

- **Sales prospecting:** find companies whose posts mention a tool you sell against, including phrases about replacing or migrating from it.
- **Partnership and integration research:** find companies standing up a stack you complement (for example Snowflake plus dbt).
- **Recruiting and agencies:** watch named employers or a department and location for roles that mention a platform you staff or implement.
- **Job-board and data pipelines:** use jobs mode to pull a clean feed from boards you already have tokens for.

### Input

`mode` defaults to `signals`. You can pass boards as `boards` objects, as career-site URLs or slugs in `companies`, or both. Unknown input fields are rejected. Passing a field this Actor does not support will fail the run.

| Field | Type | What it does |
|---|---|---|
| `mode` | `signals` or `jobs` | Default is `signals` (hiring-signal mode). `jobs` is the plain feed |
| `boards` | array of `{ats, token, companyName?}` | `ats` is `greenhouse`, `lever`, `lever_eu`, or `ashby`. `token` is the board slug (the last part of the public board URL). `companyName` is an optional label you supply; it is stored separately and never overwrites the name the ATS returns |
| `companies` | string or object array | Board URLs or slugs, or `{url` or `slug, platform, companyName}`. Supported hosts include boards.greenhouse.io, job-boards.greenhouse.io, jobs.lever.co, jobs.eu.lever.co, and jobs.ashbyhq.com |
| `platform` | string | Default `auto`: try Greenhouse, then Lever, then Ashby, and use the first board that exists (up to 3 requests per slug) |
| `discovery` | object | `{enabled, companyNames[], maxProbesPerCompany}`. Builds slug guesses and probes only official board APIs; 404s are skipped. Default is off, 6 probes |
| `tools` | string array | Signals mode. Built-in ids (for example `salesforce`, `snowflake`, `dbt`) or any other name, matched as written |
| `toolAliases` | object | Extra aliases, or `{"contextRequired": false}` to relax context checks for ambiguous names |
| `requireAllTools` | boolean | When true, a post must match every listed tool |
| `signalTypes` | string array | Empty = all. Options: `replacing`, `migrating_from`, `migrating_to`, `implementing`, `experience_with`, `nice_to_have`, `mention` |
| `minConfidence` | number 0–1 | Signals below this are dropped and not charged (default 0.6) |
| `outputLevel` | `signals`, `rollups`, or `both` | Which charged rows to write in signals mode. Default `both`. A free summary row is always added |
| `onlyNewOrChanged` | boolean | Default true. Uses the store named by `stateKey` (default `default`). Later runs emit only new or changed signals (post text changed, or confidence change ≥ 0.1) |
| `stateKey` | string | Names the persistent store `hiring-signals-state-{stateKey}` |
| `includeSelfPostings` | boolean | Default false. Off drops hits where the employer is the tool's own vendor |
| `removeRepeatedText` | boolean | Signals mode, default true. Skip sentences that appear in at least half of a company's posts (at least 3 posts; boards with 4+ posts), or in 25+ of its posts, so boilerplate does not inflate signal counts. Job rows and descriptions are not changed |
| `keywords` | string array | Case-insensitive whole-word phrases, in order and adjacent |
| `keywordsMode` | `any` or `all` | Default `any` |
| `keywordsIn` | string | Where keywords and exclude keywords are looked for. Default `title_or_description` |
| `excludeKeywords` | string array | Drop a job if any of these phrases is found |
| `locations` | string array | Case-insensitive substring of any location string as the ATS gives it. `Remote` also matches when `isRemoteDerived` is true |
| `remote` | string | Default `any`. Uses `isRemoteDerived`. Hybrid alone is not remote |
| `departments` | string array | Case-insensitive substring on department or team |
| `lookbackDays` | integer | Uses `updatedAt`, falling back to `postedAt`. Empty = 30 in signals mode, no limit in jobs mode. `0` = no limit. Jobs with no date are kept |
| `postedAfter` | string | Keep jobs whose `postedAt` is on or after this date (`YYYY-MM-DD` or relative, for example `30 days`). Jobs without `postedAt` are dropped when this filter is set |
| `postedBefore` | string | Keep jobs whose `postedAt` is before this date |
| `salaryMin` | integer | Keep jobs whose published yearly pay range reaches at least this amount. Uses only pay Lever and Ashby publish; Greenhouse job lists carry no pay |
| `salaryMax` | integer | Keep jobs whose published yearly pay range starts at or below this amount |
| `salaryCurrency` | string | Default `USD`. Only pay in this currency is compared; nothing is converted |
| `includeUnknownSalary` | boolean | Default false. When a salary filter is set, off drops jobs with no published yearly pay in that currency (including all Greenhouse jobs) |
| `experienceBands` | string array | Keep jobs whose inferred minimum years fall in `0`, `1-2`, `3-4`, `5-7`, `8-10`, or `11+` |
| `includeUnknownExperience` | boolean | Default false. When bands are set, off drops jobs where no requirement could be read |
| `includeDescription` | boolean | Jobs mode: add the plain-text `description` (larger dataset) |
| `maxItems` | integer | Cap on charged rows (jobs, signals, and rollups together). Default 500; prefill 20. `0` = no cap. The free summary row is not counted |
| `maxRequestsPerMinute` | integer | Global throttle. Each host is also limited to 1 request per second |
| `debugLog` | boolean | Verbose logs |

This Actor does not take start URLs, a browser crawler, or a proxy: it is plain HTTP JSON to official APIs. robots.txt is always respected.

Example input (real public boards; this is the same set used in a local signals run whose example output is below):

```json
{
  "mode": "signals",
  "companies": [
    "https://job-boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/linear",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "tools": [
    "snowflake",
    "dbt",
    "salesforce",
    "workday",
    "go",
    "looker",
    "tableau",
    "zendesk",
    "C++",
    "Node.js"
  ],
  "toolAliases": {
    "salesforce": ["SFDC"]
  },
  "minConfidence": 0.6,
  "outputLevel": "both",
  "onlyNewOrChanged": true,
  "stateKey": "e2e-demo",
  "maxItems": 0
}
```

The Store prefill (what the Try button and Apify's daily test run) uses the same four boards as the how-to-try steps — Duolingo, Spotify, Linear, and Supabase — with `maxItems` 20.

### Output

Each dataset row has a `recordType` of `signal`, `companyRollup`, `summary`, or `job`. Fields the source does not provide are `null`; they are never invented. Recruiter and hiring-manager emails or phone numbers are stripped from snippets even if a post includes them.

A value is copied verbatim from the ATS unless the field name ends in `Derived` (a fixed rule) or `Inferred` (read from the post text, with evidence).

**Signal rows** use: `company` (a label: your display name, else the ATS company name, else `companyNameDerived`), `companyName` (ATS value; null for Lever and Ashby), `source`, `boardToken`, `jobId`, `jobTitle`, `jobUrl`, `location`, `isRemote`, `department`, `postedAt`, `updatedAt` (Greenhouse only), `tool`, `toolName`, `aliasHit`, `snippet`, `section`, `signalType`, `confidence`, `matchCount`, `firstSeen`, `isNew`, `changed`, `reposted`, `contentHash`, `scrapedAt`.

**Company rollup rows** use: `company`, `source`, `boardToken`, `tools`, `matchingPosts`, `matchingPostsLastRun`, `trend` (`new`, `up`, `down`, or `flat`), `strongestSnippet`, `strongestConfidence`, `topSignalType`, `jobUrls` (up to 5), `billed`, `scrapedAt`. `billed` is true when the company has at least one current signal (the $0.01 event fires); it is false on a free "down to zero" row.

**Job rows** (jobs mode) use different names: `id`, `platform`, `companySlug`, `companyName`, `companyNameInput`, `companyNameDerived`, `companyNameSource`, `title`, `team`, `employmentType`, `workplaceType`, `applyUrl`, `compensation`, `description` (only when `includeDescription` is on), `sourceId`, `jobUrl`, `department`, `postedAt`, `updatedAt`, `scrapedAt`, `location`, `allLocations`, `isRemoteDerived`, `jobUrlNormalized`, `experienceYearsMinInferred`, `experienceBandInferred`, `experienceEvidenceInferred`. There is no job-row field named `company`. `companyNameDerived` is filled only when the ATS gives no name (Lever, Ashby): the bundled starter-list name, else the slug in title case. `companyNameSource` is `starter-list` or `slug`, or null when `companyNameDerived` is null.

**Summary rows** (free, both modes; also saved as `RUN_SUMMARY`) include `mode`, `runAt`, `boardsChecked`, `boardsSucceeded`, `boardsFailed`, `jobsScanned`, `duplicatesRemoved`, `jobsAfterFilters`, `filteredOut`, `httpRequests`, `jobsReturned`, `signalsCurrent`, `signalsNew`, `signalsChanged`, `signalsUnchangedSkipped`, `signalsNotPushedDueToLimit`, `belowMinConfidence`, `filteredBySignalType`, `companiesWithSignals`, `rollupsPushed`, `rollupsDownToZeroFree`, `repeatedSentencesSkipped`, `companiesWithRepeatedText`, `topCompanies`, `stateStore`, `runIndex`, `robots`, `durationSecs`, `signalsReposted`, and `scrapedAt`.

Example output, signal row, from the local run of the input above:

```json
{
  "recordType": "signal",
  "company": "GitLab",
  "companyName": "GitLab",
  "source": "greenhouse",
  "boardToken": "gitlab",
  "jobId": "8883003002",
  "jobTitle": "Director, Analytics Engineering",
  "jobUrl": "https://job-boards.greenhouse.io/gitlab/jobs/8883003002",
  "location": "Remote, Canada; Remote, United States",
  "isRemote": true,
  "department": "Data",
  "postedAt": "2026-10-13T14:52:13Z",
  "updatedAt": "2026-11-06T20:25:07Z",
  "tool": "snowflake",
  "toolName": "Snowflake",
  "aliasHit": "Snowflake",
  "snippet": "Build and own company-wide data applications, from Claude-built single-page apps to apps inside BI tools, that combine Snowflake data, API calls, AI-powered deep dives, and write-back to Postgres or Snowflake",
  "section": "responsibilities",
  "signalType": "implementing",
  "confidence": 0.92,
  "matchCount": 4,
  "firstSeen": "2026-11-10T03:51:07Z",
  "isNew": true,
  "changed": false,
  "reposted": false,
  "contentHash": "78092c8783440c1f",
  "scrapedAt": "2026-11-10T03:51:07Z"
}
```

Example output, company rollup row (`billed` is true because the company has current signals). On Lever and Ashby, the `company` label uses your display name or `companyNameDerived` (here "Spotify" from the starter list), not the raw slug:

```json
{
  "recordType": "companyRollup",
  "company": "Spotify",
  "source": "lever",
  "boardToken": "spotify",
  "tools": [
    "c_plus_plus",
    "go",
    "salesforce"
  ],
  "matchingPosts": 6,
  "matchingPostsLastRun": 6,
  "trend": "flat",
  "strongestSnippet": "You are comfortable using Salesforce to manage client relationships and sales pipelines, as well as Google Slides and Sheets to create presentations and reports.",
  "strongestConfidence": 1.0,
  "topSignalType": "mention",
  "jobUrls": [
    "https://jobs.lever.co/spotify/bb2c10a2-2bf8-4b21-901e-48b0e2e09cc8",
    "https://jobs.lever.co/spotify/66184cac-4d6c-46f6-868c-bac0c184fd22",
    "https://jobs.lever.co/spotify/1ee5dcb7-a5fe-465b-91da-d42ebab374c8",
    "https://jobs.lever.co/spotify/47aeb6ee-f8c9-4f1e-9197-25c4c5676e5d",
    "https://jobs.lever.co/spotify/9f66e598-8587-4a57-8552-2cdb8672014a"
  ],
  "billed": true,
  "scrapedAt": "2026-11-10T03:51:17Z"
}
```

Example output, a free rollup after a later run when a company has dropped to zero signals (`trend` is `down`, `billed` is false — this row is not charged):

```json
{
  "recordType": "companyRollup",
  "company": "Duolingo",
  "source": "greenhouse",
  "boardToken": "duolingo",
  "tools": [],
  "matchingPosts": 0,
  "matchingPostsLastRun": 1,
  "trend": "down",
  "strongestSnippet": null,
  "strongestConfidence": null,
  "topSignalType": null,
  "jobUrls": [],
  "billed": false,
  "scrapedAt": "2026-11-10T04:14:10Z"
}
```

Example output, job row, from a separate local jobs-mode run (Ramp on Ashby). `companyName` is null because Ashby does not publish one; `companyNameDerived` is filled from the starter list:

```json
{
  "recordType": "job",
  "id": "ashby:ramp:6a508863-352b-4f39-b299-5a3b6693605a",
  "platform": "ashby",
  "companySlug": "ramp",
  "companyName": null,
  "companyNameInput": null,
  "companyNameDerived": "Ramp",
  "companyNameSource": "starter-list",
  "title": "Manager, Customer Activation, Mid-Market",
  "department": "Sales",
  "team": "CSM - Mid-Market",
  "location": "New York, NY (HQ)",
  "allLocations": [
    "New York, NY (HQ)",
    "San Francisco, CA"
  ],
  "workplaceType": "Hybrid",
  "employmentType": "FullTime",
  "postedAt": "2026-10-22T22:24:53Z",
  "updatedAt": null,
  "applyUrl": "https://jobs.ashbyhq.com/ramp/6a508863-352b-4f39-b299-5a3b6693605a/application",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/6a508863-352b-4f39-b299-5a3b6693605a",
  "compensation": {
    "min": 174000,
    "max": 266000,
    "currency": "USD",
    "interval": "1 YEAR",
    "summary": "$174K - $266K"
  },
  "description": null,
  "sourceId": "6a508863-352b-4f39-b299-5a3b6693605a",
  "isRemoteDerived": false,
  "jobUrlNormalized": "https://jobs.ashbyhq.com/ramp/6a508863-352b-4f39-b299-5a3b6693605a",
  "experienceYearsMinInferred": 5,
  "experienceBandInferred": "5-7",
  "experienceEvidenceInferred": "5+ years of implementation, CSM or AM experience as an individual contributor, with a proven, consistent track record exceeding goals",
  "scrapedAt": "2026-11-10T04:09:59Z"
}
```

### How much will a run cost?

This Actor uses **pay-per-event** pricing. You pay for the events below, not for a separate platform-usage line (except in Standby mode, where Apify always adds usage). Plan-tier (Silver/Gold) discounts are not set.

| Event | When it fires | Price |
|---|---|---|
| `apify-actor-start` | Each run | $0.00005 (Apify's default) |
| `signal-found` (primary) | Each charged `signal` row | **$0.003** |
| `company-rollup` | Each `companyRollup` row for a company with at least one current signal (`billed: true`) | **$0.01** |
| `job-returned` | Each charged `job` row in jobs mode | **$0.001** |

These items are not charged: the `summary` row; jobs that were scanned but did not match; signals dropped under `minConfidence`; and rollup rows where a company dropped to 0 signals (`trend: "down"`, `billed: false`). `maxItems` is enforced before charging, so a test with `maxItems: 20` cannot bill more than 20 result events of the mode you ran.

**Formula:**

```text
run cost = (start events × $0.00005)
         + (charged signal rows × $0.003)
         + (billed rollup rows × $0.01)
         + (charged job rows × $0.001)
```

**Worked examples** (one start event per run):

- **1,000 jobs** in jobs mode (set `maxItems` to 1000 or 0; the default cap is 500): `(1 × $0.00005) + (1,000 × $0.001) = $1.00005` (about **$1.00**, or $1.00 per 1,000 jobs plus the start fee).
- **50 hiring signals and 10 billed company rollups** in signals mode (companies with at least one signal): `(1 × $0.00005) + (50 × $0.003) + (10 × $0.01) = $0.25005` (about **$0.25**). Free `billed: false` rollups do not add to this total.
- **Both in one week** (a 1,000-job run plus that signals run): `$1.00005 + $0.25005 = $1.25010` (about **$1.25**).

The two modes are separate runs, so each has its own start event. Scanned but unmatched jobs in signals mode are free.

Default memory is 512 MB. For lists of 50 or more boards, we recommend setting memory to 1024 MB.

Apify free-plan users can run the Actor; we do not earn from free-plan runs, and any free-result cap we add later will be stated here and in the input schema. Prices go live only when the Actor is published.

### Limitations

- **Sources:** Greenhouse, Lever, and Ashby public job-board APIs only. Lever EU (`lever_eu`, jobs.eu.lever.co) is supported in code but **untested** against a live EU board. Workday, Indeed, LinkedIn, and HTML career pages are out of scope.
- **You need board tokens or discovery:** the Actor does not ship a global index of every company. Discovery only tries official slug patterns and will miss unusual tokens.
- **Lever and Ashby do not provide a company name.** `companyName` is null on those boards. Jobs mode fills `companyNameDerived` from the bundled starter list, or from the slug in title case, and sets `companyNameSource` accordingly. On signal and rollup rows, the `company` label uses your display name, else the ATS company name, else `companyNameDerived`.
- **Salary filtering works only with pay that Lever and Ashby publish** (Lever `salaryRange`, Ashby `compensation`). Greenhouse job lists carry no pay, so Greenhouse jobs count as unknown salary and are dropped when a salary filter is on unless you set `includeUnknownSalary` to true. Currencies are not converted.
- **Matching is heuristic:** ambiguous names (for example "Go", "Notion", "Monday") need context words; benefits, EEO, and legal sections are ignored; negation such as "no Salesforce experience needed" is dropped. Expect some false positives and false negatives. Confidence is a rule-based score, not a guarantee.
- **Experience bands are inferred** from the post text, not a field the ATS always publishes. `includeUnknownExperience` controls what happens when nothing can be read.
- **No invented fields and no applicant data.** If a board omits salary, department, or a posting date, the field is `null`.
- **Rate limits and terms:** the Actor throttles requests and backs off on 429/5xx. Re-check each provider's API terms before you rely on a large schedule. You are responsible for how you use the output (for example outreach laws).

### FAQ

**How is this different from a large job-index Actor?** Those products search a pre-built database of many career sites. This Actor fetches the boards you specify (or discover) live from official APIs, applies your filters to the current posts, and — in hiring-signal mode, which is the default — returns a snippet and a signal type for a named tool. It does not claim broader coverage or a larger company catalog.

**Do I need API keys?** No. The official Greenhouse, Lever, and Ashby job-board endpoints used here are unauthenticated.

**Can I run it every week and only see new matches?** Yes, if you set `onlyNewOrChanged` (on by default) and a stable `stateKey`. The first run treats matching posts in the lookback window as new, up to `maxItems`.

**What does a confidence score mean?** It combines how specific the alias was, which section of the post it appeared in, the signal type, and whether more than one sentence mentions the tool. Rows under `minConfidence` are omitted and not billed.

**Is this legal?** The Actor is designed to call documented public job-board APIs, not to log in, bypass bot checks, or scrape disallowed paths. That is not legal advice. Read the provider terms and your own use case. We are not affiliated with the ATS vendors.

**Where do I get help?** Open an issue on this Actor's Issues tab in Apify. We aim to address issues within 14 days.

# Changelog

This Actor's version history is a separate document: https://apify.com/propheto/job-board-hiring-signals/changelog.md

# Actor input Schema

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

signals: flag job posts that mention the tools you list, with the matching sentence and a confidence score; weekly runs return only new or changed signals plus a per-company rollup. jobs: one normalized record per job post.

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

Boards as {"ats": "greenhouse" | "lever" | "lever_eu" | "ashby", "token": "<board slug>", "companyName": "optional label"}. The token is the last part of the public board URL, e.g. jobs.lever.co/<token>. Give boards here, in Companies, or both. Only Greenhouse, Lever and Ashby are supported. "companyName" is your display name: it is output as companyNameInput and never replaces the name the ATS returns.

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

Board URLs or slugs. Supported URL forms: boards.greenhouse.io/<slug>, job-boards.greenhouse.io/<slug>, jobs.lever.co/<slug>, jobs.eu.lever.co/<slug>, jobs.ashbyhq.com/<slug> (and their API URLs). Other sites (Workday, company career pages, etc.) are not supported and are reported in the log and the summary. Items can also be objects: {"url" or "slug", "platform", "companyName"}.

## `platform` (type: `string`):

auto tries Greenhouse, then Lever, then Ashby, and uses the first board that exists (up to 3 requests per slug).

## `discovery` (type: `object`):

{"enabled": true, "companyNames": \["Example Corp"], "maxProbesPerCompany": 6}. Builds slug guesses (examplecorp, example-corp, …) and probes only the official board APIs; 404 = skip. No search engines, no HTML crawling.

## `tools` (type: `array`):

Signals mode. Built-in ids (salesforce, hubspot, snowflake, dbt, workday, looker, looker_studio, notion, go, ruby, excel, monday, asana, zendesk, jira, servicenow, marketo, netsuite, sap, oracle_ebs, tableau, power_bi, databricks, segment, gong, outreach, intercom) or any other name, which is matched as written (case-insensitive, word-boundary safe, e.g. C++, .NET, Node.js).

## `toolAliases` (type: `object`):

{"salesforce": \["SF CRM"], "mytool": \["MyTool", "MT Cloud"], "looker": {"contextRequired": false}}. A list adds aliases; contextRequired=false turns off the context check for ambiguous names.

## `requireAllTools` (type: `boolean`):

Keep a post only if every listed tool matches it.

## `signalTypes` (type: `array`):

Empty = all. Options: replacing, migrating_from, migrating_to, implementing, experience_with, nice_to_have, mention.

## `minConfidence` (type: `number`):

0 to 1. Signals below this are dropped and not charged. Body-text mentions of ambiguous names score 0.48, so they need 0.45 or lower.

## `outputLevel` (type: `string`):

Which charged rows to output. A free summary row is always added.

## `onlyNewOrChanged` (type: `boolean`):

Compare with the previous run stored under State key and output only signals that are new, or whose post text or confidence changed (≥0.1). Turn off to output every current signal.

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

Names the persistent store hiring-signals-state-{stateKey}. Use one fixed key per weekly schedule.

## `includeSelfPostings` (type: `boolean`):

Off: drop matches where the employer is the vendor of the tool (e.g. Salesforce hiring for Salesforce).

## `removeRepeatedText` (type: `boolean`):

Signals mode. Skip sentences that appear in at least half of a company's posts (at least 3 posts; boards with 4+ posts), or in 25+ of its posts, when matching tools. Job rows and descriptions are not changed. See README.

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

Phrases. Case-insensitive, whole words, words in this order and adjacent (spaces or hyphens between them; "people systems" does not match "people, systems").

## `keywordsMode` (type: `string`):

any = at least one keyword; all = every keyword.

## `keywordsIn` (type: `string`):

Where keywords and exclude keywords are looked for.

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

Drop a job if ANY of these phrases is found (same matching rules and scope as Keywords).

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

Case-insensitive substring of any location string as the ATS gives it. "Remote" also matches jobs where isRemoteDerived is true.

## `remote` (type: `string`):

Uses isRemoteDerived: true when the ATS workplace type is remote or a location string contains "remote". Hybrid alone is not remote.

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

Case-insensitive substring match on department or team.

## `lookbackDays` (type: `integer`):

Uses updatedAt, falling back to postedAt (signals spec). Empty = 30 in signals mode, no limit in jobs mode. 0 = no limit. Jobs with no date are kept.

## `postedAfter` (type: `string`):

Keep jobs whose postedAt is on/after this date. Absolute (YYYY-MM-DD) or relative ("30 days"). Jobs without postedAt are dropped when a posted date filter is set.

## `postedBefore` (type: `string`):

Keep jobs whose postedAt is before this date, e.g. "42 days" finds roles still open after 6 weeks. Combine with Posted on or after for a date range.

## `salaryMin` (type: `integer`):

Keep jobs whose published yearly pay range reaches at least this amount (job max >= value). Uses only pay published by the ATS: Lever salaryRange and Ashby compensation. Greenhouse job lists carry no pay, so Greenhouse jobs count as unknown.

## `salaryMax` (type: `integer`):

Keep jobs whose published yearly pay range starts at or below this amount (job min <= value).

## `salaryCurrency` (type: `string`):

3-letter code. Only pay in this currency is compared; nothing is converted.

## `includeUnknownSalary` (type: `boolean`):

When a salary filter is set: keep jobs with no published yearly pay in that currency (hourly pay, other currencies, Greenhouse). Off = drop them.

## `experienceBands` (type: `array`):

Keep jobs whose inferred minimum years of experience falls in these bands: 0 (entry level / no experience), 1-2, 3-4, 5-7, 8-10, 11+. Bands do not overlap. Inferred from the post text; see README.

## `includeUnknownExperience` (type: `boolean`):

When experience bands are set: keep jobs where no requirement could be read.

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

Jobs mode: add the plain-text description to each job record (larger dataset).

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

Cap on charged rows (jobs, signals and rollups together). 0 = no limit. The free summary row is not counted.

## `maxRequestsPerMinute` (type: `integer`):

Global throttle. Each host also gets at most 1 request per second (Lever asks for Crawl-delay: 1).

## `debugLog` (type: `boolean`):

Verbose logging.

## Actor input object example

```json
{
  "mode": "signals",
  "boards": [
    {
      "ats": "greenhouse",
      "token": "duolingo",
      "companyName": "Duolingo"
    },
    {
      "ats": "lever",
      "token": "spotify",
      "companyName": "Spotify"
    },
    {
      "ats": "ashby",
      "token": "linear",
      "companyName": "Linear"
    },
    {
      "ats": "ashby",
      "token": "supabase",
      "companyName": "Supabase"
    }
  ],
  "companies": [],
  "platform": "auto",
  "discovery": {
    "enabled": false,
    "companyNames": [],
    "maxProbesPerCompany": 6
  },
  "tools": [
    "snowflake",
    "dbt",
    "salesforce",
    "workday",
    "go"
  ],
  "toolAliases": {},
  "requireAllTools": false,
  "signalTypes": [],
  "minConfidence": 0.6,
  "outputLevel": "both",
  "onlyNewOrChanged": true,
  "stateKey": "default",
  "includeSelfPostings": false,
  "removeRepeatedText": true,
  "keywords": [],
  "keywordsMode": "any",
  "keywordsIn": "title_or_description",
  "excludeKeywords": [],
  "locations": [],
  "remote": "any",
  "departments": [],
  "salaryCurrency": "USD",
  "includeUnknownSalary": false,
  "experienceBands": [],
  "includeUnknownExperience": false,
  "includeDescription": false,
  "maxItems": 20,
  "maxRequestsPerMinute": 60,
  "debugLog": false
}
```

# Actor output Schema

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

No description

## `signals` (type: `string`):

No description

## `rollups` (type: `string`):

No description

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

No description

## `runSummary` (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": [
        {
            "ats": "greenhouse",
            "token": "duolingo",
            "companyName": "Duolingo"
        },
        {
            "ats": "lever",
            "token": "spotify",
            "companyName": "Spotify"
        },
        {
            "ats": "ashby",
            "token": "linear",
            "companyName": "Linear"
        },
        {
            "ats": "ashby",
            "token": "supabase",
            "companyName": "Supabase"
        }
    ],
    "tools": [
        "snowflake",
        "dbt",
        "salesforce",
        "workday",
        "go"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("propheto/job-board-hiring-signals").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": [
        {
            "ats": "greenhouse",
            "token": "duolingo",
            "companyName": "Duolingo",
        },
        {
            "ats": "lever",
            "token": "spotify",
            "companyName": "Spotify",
        },
        {
            "ats": "ashby",
            "token": "linear",
            "companyName": "Linear",
        },
        {
            "ats": "ashby",
            "token": "supabase",
            "companyName": "Supabase",
        },
    ],
    "tools": [
        "snowflake",
        "dbt",
        "salesforce",
        "workday",
        "go",
    ],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("propheto/job-board-hiring-signals").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": [
    {
      "ats": "greenhouse",
      "token": "duolingo",
      "companyName": "Duolingo"
    },
    {
      "ats": "lever",
      "token": "spotify",
      "companyName": "Spotify"
    },
    {
      "ats": "ashby",
      "token": "linear",
      "companyName": "Linear"
    },
    {
      "ats": "ashby",
      "token": "supabase",
      "companyName": "Supabase"
    }
  ],
  "tools": [
    "snowflake",
    "dbt",
    "salesforce",
    "workday",
    "go"
  ],
  "maxItems": 20
}' |
apify call propheto/job-board-hiring-signals --silent --output-dataset

```

## MCP server setup

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

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/sfvMy2LgWoL22nvE1/builds/wsTal2vZcAZuP0M1x/openapi.json
