# ATS Hiring Signals Monitor — Greenhouse, Lever & Ashby (`johnatan029/ats-hiring-signals-monitor`) Actor

Hiring signals for B2B sales and recruiting: new job postings per company from the official public Greenhouse, Lever and Ashby job-board APIs. Batch of company slugs, diff mode (new within N days), filters. No login. Not affiliated with the ATS vendors.

- **URL**: https://apify.com/johnatan029/ats-hiring-signals-monitor.md
- **Developed by:** [Johnn Mottin](https://apify.com/johnatan029) (community)
- **Categories:** Lead generation, Business, Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 job results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## ATS Hiring Signals Monitor — Greenhouse, Lever & Ashby

**"Company X just opened a role for Y" is a buying signal** — a company hiring devs is about to buy dev tools; a company hiring SDRs is about to buy sales tools. This Actor turns the official public job-board APIs of **Greenhouse, Lever and Ashby** into that signal: give it a list of companies, get back **only the jobs published in the last N days**, normalized across the three ATSs. No login, no browser, no LinkedIn.

**Not affiliated with, sponsored by, or endorsed by Greenhouse, Lever, Ashby, or any company whose job board is queried.** All data comes from the official public job-board APIs those companies expose and remains subject to their terms.

Who it's for:

- **B2B sales / outbound teams (SDRs):** wake up to "these 4 of my 80 target accounts posted new engineering roles this week" — in your CRM, Slack or spreadsheet via Apify integrations.
- **Recruiting agencies:** monitor client and prospect boards for new mandates before anyone calls.
- **Analysts / founders:** track hiring velocity of competitors as a growth proxy.

### How it works

You provide company **board slugs** (e.g. `stripe` — the identifier in the company's careers URL). For each company, the Actor queries the official API of its ATS — or **auto-detects** the ATS by trying Greenhouse → Lever → Ashby (cheap 404s). Default mode is the **signal**: only jobs first published within `newWithinDays` (computed at run time, so a saved schedule never goes stale). One unknown slug never kills the batch — it becomes a controlled `NOT_FOUND` in `STATS.companySummary`.

**You are only charged for jobs actually written to the dataset** — jobs discarded by the date window, filters or caps cost you nothing.

### Input

Copy-paste ready (weekly signal over 3 boards):

```json
{
  "companies": [
    { "slug": "stripe" },
    { "slug": "gitlab", "ats": "greenhouse" },
    { "slug": "spotify", "ats": "lever" }
  ],
  "newWithinDays": 7,
  "titleKeywords": ["engineer", "developer", "data"],
  "remoteOnly": false
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | array | required | 1–100 `{slug, ats?}` objects. `ats`: `greenhouse` | `lever` | `ashby` | `auto` (default). Unknown slug → controlled `NOT_FOUND`; duplicates skipped |
| `newWithinDays` | int 1–365 | `7` | The signal window: only jobs first published in the last N days (Greenhouse `first_published`, Lever `createdAt`, Ashby `publishedAt`). Ignored when `fullDump` is on |
| `fullDump` | boolean | `false` | Return every job currently on each board (ignores the date window) — a one-off full export instead of the signal |
| `titleKeywords` | string\[] | `[]` | Any keyword may match the title (case-insensitive). Discarded = never billed |
| `departments` | string\[] | `[]` | Substring on department/team. On Greenhouse triggers the heavier content fetch automatically |
| `locations` | string\[] | `[]` | Substring on location(s) — e.g. `"london"`, `"remote"`, `"brazil"` |
| `remoteOnly` | boolean | `false` | Only jobs explicitly remote (Ashby `isRemote`, Lever `workplaceType`; Greenhouse via location text — best-effort) |
| `includeDescription` | boolean | `false` | Adds the job description (bigger records) |
| `maxJobsPerCompany` | int 1–2000 | `500` | Newest first |
| `maxResults` | int 1–5000 | `1000` | Run-wide hard cap |

### Output (missing = `null` / `[]`, never invented)

One record per job, same key set across the three ATSs. Real record (GitLab, official public data):

```json
{
  "atsSource": "greenhouse",
  "companyIndex": 1,
  "companySlug": "gitlab",
  "jobId": "8115023002",
  "title": "Account Executive - Italy",
  "department": null,
  "team": null,
  "employmentType": null,
  "location": "Remote, Italy",
  "secondaryLocations": [],
  "isRemote": true,
  "workplaceType": null,
  "url": "https://job-boards.greenhouse.io/gitlab/jobs/8115023002",
  "publishedAt": "2026-06-11T20:23:44.000Z",
  "publishedAtRaw": "2026-06-11T16:23:44-04:00",
  "updatedAt": "2026-07-17T12:48:22.000Z",
  "compensation": null,
  "description": null,
  "scrapedAt": "2026-07-30T12:00:00.000Z"
}
```

| Field group | Fields | Notes |
|---|---|---|
| Correlation | `atsSource`, `companyIndex`, `companySlug` | ties every job to your input list |
| Job | `jobId`, `title`, `department`, `team`, `employmentType`, `url` | core fields¹: `title`, `url`, `publishedAt`, `atsSource`, `companySlug` |
| Location | `location`, `secondaryLocations[]`, `isRemote`, `workplaceType` | `isRemote` is native on Ashby/Lever; text heuristic on Greenhouse (best-effort) |
| Dates | `publishedAt` (+`Raw`), `updatedAt` (Greenhouse only) | ISO, parsed from each ATS's native format (ISO-with-offset / epoch-ms / ISO) |
| Extras | `compensation` (Ashby, when the company exposes it), `description` (opt-in) | |

¹ Watched by the built-in health check: if more than 50% come back null the run **fails loudly naming the dead field** (`DEAD_FIELDS`) — never a silent broken dataset.

Availability by ATS (measured on real boards, 2026-07): `department`/`team` — always on Lever/Ashby, Greenhouse only with the content fetch; `compensation` — Ashby only, when the company enables it; `updatedAt` — Greenhouse only.

### Run health

`STATS` (key-value store) records per-company outcomes (`companySummary`: `OK`, `NO_JOBS`, `NOT_FOUND`, `FAILED`, `INVALID_COMPANY`, `DUPLICATE_SKIPPED`, `SKIPPED_MAX_RESULTS`, plus `detectedAts` for auto mode), HTTP counters, retries and discard counters (`discardedByDate` / `discardedByFilters` / `discardedByCap` / `discardedNoDate`). `ERRORS` records failures with stable codes (`INVALID_INPUT`, `NOT_FOUND`, `HTTP_UNAVAILABLE`, `HTTP_TIMEOUT`, `HTTP_RATE_LIMITED`, `API_CONTRACT_CHANGED`, `DEAD_FIELDS`, `ALL_COMPANIES_FAILED`).

- A week with no new postings on a quiet board is a **legitimate** result (`STATS.legitimateNoNewJobs`) — that's what a monitor is for.
- A batch where **no** company completes fails loudly with `ALL_COMPANIES_FAILED`.

### API usage examples

Run and get items in one call:

```bash
curl -s "https://api.apify.com/v2/acts/<YOUR_USERNAME>~ats-hiring-signals-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST -H "Content-Type: application/json" \
  -d '{"companies":[{"slug":"stripe"}],"newWithinDays":7}'
```

Weekly account-list watch (Console → Schedules): save your 50–100 target accounts as input with `newWithinDays: 7` and schedule for Monday morning — the relative window recomputes every run.

Recruiting mandate radar:

```json
{
  "companies": [{ "slug": "ramp" }, { "slug": "linear" }],
  "newWithinDays": 14,
  "departments": ["sales", "marketing"]
}
```

### Schedule it (recommended — cloud, not your desktop)

New postings are a weekly signal — `newWithinDays: 7` plus a weekly cron. **Use Apify's own Schedules, not a local scheduler** — configure it once and it runs in the cloud whether or not your machine is on.

1. Save your input as a **Task** (Console → this Actor → *Create task*), e.g. your 50–100 target accounts with `newWithinDays: 7`.
2. Console → **Schedules → Create schedule**, add the Task, set the cron (e.g. Mondays 6am → `0 6 * * 1`).
3. Route the dataset to Slack, Sheets, your CRM or webhook via Apify integrations.

The date window is computed at run time, so a saved schedule never goes stale — Monday's run always asks for last week's postings.

### Pricing

Billed per job written to the dataset (Pay Per Event) — the Pricing tab on this page is always the authoritative source for current rates and for any per-run fee. Jobs discarded by the window, filters or caps are never billed. `newWithinDays` is the main cost lever: the signal mode typically writes a handful of jobs per company per week.

### Honest limits

- **You supply the company list.** No ATS publishes a public directory of its boards; the slugs come from your target accounts (careers-page URLs).
- **Coverage = Greenhouse + Lever + Ashby.** Companies on Workday, SmartRecruiters, iCims etc. return `NOT_FOUND` in auto mode. More ATSs are a natural expansion.
- **`publishedAt` semantics differ slightly per ATS** (Greenhouse first publication, Lever creation, Ashby publication) — all fresh enough for weekly signals; raw values are preserved.
- **Greenhouse `department` needs the heavier fetch** — triggered automatically only when you filter by department or ask for descriptions.
- **Remote detection on Greenhouse is text-based** (best-effort) — native flags exist only on Lever/Ashby.
- The Actor is deliberately polite with the APIs: 2 concurrent requests, pacing, exponential backoff on 429/5xx, batches capped at 100 companies per run.

### FAQ

**Do I need an account or API key for Greenhouse, Lever or Ashby?** No. The Actor reads the official public job-board APIs those companies expose, without logging in.

**Where do I find a company's slug?** In its careers page URL: `boards.greenhouse.io/<slug>`, `jobs.lever.co/<slug>`, `jobs.ashbyhq.com/<slug>`. There is no public directory of all companies per ATS — your target-account list is the input, which is exactly how outbound works.

**What exactly am I charged for?** Per job written to the dataset (Pay Per Event) — jobs discarded by the date window, filters or caps cost you nothing. The Pricing tab on this page is always the authoritative source for current rates and for any per-run fee.

**Can I schedule it?** Yes — that is the intended use. See "Schedule it" above.

**Is this affiliated with the ATS vendors?** No. This is an unofficial community Actor, not affiliated with Greenhouse, Lever, Ashby or any company whose job board is queried; all data comes from their official public APIs and remains subject to their terms.

# Actor input Schema

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

1–100 entries as { "slug", "ats" } objects. slug = the company's job-board identifier (e.g. "stripe" in boards-api.greenhouse.io/v1/boards/stripe/jobs). ats: greenhouse | lever | ashby | auto (default — tries the three in order; 404s are cheap). An unknown slug becomes a controlled NOT\_FOUND in STATS.companySummary without stopping the batch.

## `newWithinDays` (type: `integer`):

Keep only jobs first published in the last N days (Greenhouse first\_published, Lever createdAt, Ashby publishedAt). Computed at run time — a saved task never goes stale. Ignored when 'Full dump' is on.

## `fullDump` (type: `boolean`):

When on, returns every job currently on each board instead of only recent ones. Turns the monitor into a one-off full export.

## `titleKeywords` (type: `array`):

Any keyword may match the job title (case-insensitive). Applied after fetching — you are only charged for jobs that pass.

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

Substring match on department or team (case-insensitive). Note: on Greenhouse this requires the heavier content payload — fetched automatically when used.

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

Substring match on the job's location(s), case-insensitive (e.g. "london", "brazil", "remote").

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

Keep only jobs explicitly marked remote (Ashby isRemote, Lever workplaceType; Greenhouse by location text — best-effort).

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

Adds the job description to each record (bigger payloads; on Greenhouse triggers the heavier content fetch).

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

Newest first. Discarded jobs are never billed.

## `maxResults` (type: `integer`):

Hard cap of records written to the dataset across the whole batch.

## `debug` (type: `boolean`):

Verbose logging.

## Actor input object example

```json
{
  "companies": [
    {
      "slug": "stripe"
    },
    {
      "slug": "gitlab"
    },
    {
      "slug": "spotify",
      "ats": "lever"
    }
  ],
  "newWithinDays": 30,
  "fullDump": false,
  "titleKeywords": [],
  "departments": [],
  "locations": [],
  "remoteOnly": false,
  "includeDescription": false,
  "maxJobsPerCompany": 500,
  "maxResults": 1000,
  "debug": false
}
```

# 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": [
        {
            "slug": "stripe"
        },
        {
            "slug": "gitlab"
        },
        {
            "slug": "spotify",
            "ats": "lever"
        }
    ],
    "newWithinDays": 30,
    "maxJobsPerCompany": 500,
    "maxResults": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("johnatan029/ats-hiring-signals-monitor").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": [
        { "slug": "stripe" },
        { "slug": "gitlab" },
        {
            "slug": "spotify",
            "ats": "lever",
        },
    ],
    "newWithinDays": 30,
    "maxJobsPerCompany": 500,
    "maxResults": 1000,
}

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

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    {
      "slug": "stripe"
    },
    {
      "slug": "gitlab"
    },
    {
      "slug": "spotify",
      "ats": "lever"
    }
  ],
  "newWithinDays": 30,
  "maxJobsPerCompany": 500,
  "maxResults": 1000
}' |
apify call johnatan029/ats-hiring-signals-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=johnatan029/ats-hiring-signals-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/BpKd0idviWIfegebs/builds/7Co1Bv71SUHZVnqat/openapi.json
