# Hiring Signals Monitor - New Jobs & Surges (ATS) (`tiger19935/hiring-signals-monitor`) Actor

Greenhouse and Lever scraper that tracks which companies are hiring. Reads public Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Teamtailor boards, remembers every job and reports new jobs, closed jobs and hiring surges between runs.

- **URL**: https://apify.com/tiger19935/hiring-signals-monitor.md
- **Developed by:** [Andrew Boiko](https://apify.com/tiger19935) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 company hiring summaries

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 Monitor - New Jobs & Hiring Surges from Public Job Boards

Know which companies are ramping up hiring - before everyone else does. **Hiring Signals Monitor** reads the *public* job boards of the applicant tracking systems (ATS) that most growing companies use (Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Teamtailor), remembers every job it has seen, and on each run tells you:

- how many jobs a company has open right now, by department and location,
- which jobs are **new** since the last run (and which were closed),
- whether hiring is trending **up, flat or down**, and whether the company is in a **hiring surge**.

Hiring is one of the most reliable buying signals in B2B: a company opening 10 sales roles is expanding the sales team; one opening a DevOps lead is about to buy infrastructure tooling. Schedule the actor daily or weekly and plug the results into your CRM, Slack or spreadsheet.

### Who is it for?

- **Sales and SDR teams** - find accounts that are growing and the moment a relevant role opens.
- **Recruiters and staffing agencies** - spot companies that just started hiring in your niche.
- **Investors and analysts** - track headcount-growth proxies for a watch list of startups.
- **Competitive intelligence** - see where competitors are investing (engineering vs. sales vs. support).
- **Job seekers** - get alerts for new openings at the companies you care about.

### What you get

Two kinds of rows in one dataset, told apart by `rowType`:

1. `company-summary` - one per company: open jobs, top departments/locations, new and closed since last run, trend, surge flag.
2. `new-job` - one per newly detected job: title, department, location, link, posted date (when the board publishes it) and the time we first saw it.

#### Input

| Field | Type | Description |
|---|---|---|
| `companies` | list | Domains (`stripe.com`), careers/board URLs, or explicit boards as `ats:slug` such as `greenhouse:stripe`. Objects `{name, domain, ats, slug}` are accepted through the API. |
| `keywords` | list | Only count/report jobs whose title or department contains one of these words. |
| `surgeThreshold` | integer | New jobs since last run needed to flag a surge (default 5). |
| `includeBaseline` | boolean | Also output a `new-job` row for every job on the first run (default off; charged per row). |
| `monitorName` | string | Name of the saved state. Use one stable name per watch list. |
| `probeSlugGuesses` | boolean | For domains without a visible board link, try board names derived from the domain (default on). |
| `maxCompanies` | integer | Safety limit per run (default 500, max 2000). |
| `includeFailed` | boolean | Add free `error` rows for companies that could not be read. |

#### Output example

```json
{
  "rowType": "company-summary",
  "id": "greenhouse:acme:summary:2026-10-14T08:00:03.120Z",
  "companyName": "Acme",
  "domain": "acme.com",
  "ats": "greenhouse",
  "slug": "acme",
  "boardUrl": "https://boards.greenhouse.io/acme",
  "detectionMethod": "page-link",
  "detectionConfidence": "high",
  "openJobs": 42,
  "totalOpenJobs": 42,
  "previousOpenJobs": 35,
  "byDepartment": [{ "name": "Engineering", "count": 18 }, { "name": "Sales", "count": 9 }],
  "byLocation": [{ "name": "Remote - Europe", "count": 11 }, { "name": "San Francisco, CA", "count": 8 }],
  "newSinceLast": 9,
  "closedSinceLast": 2,
  "hiringTrend": "up",
  "surge": true,
  "isBaseline": false,
  "scannedAt": "2026-10-14T08:00:03.120Z",
  "source": "Greenhouse Job Board API"
}
```

```json
{
  "rowType": "new-job",
  "id": "greenhouse:acme:4101",
  "companyName": "Acme",
  "jobId": "4101",
  "title": "Staff Engineer",
  "department": "Engineering",
  "location": "Remote",
  "url": "https://boards.greenhouse.io/acme/jobs/4101",
  "postedAt": null,
  "firstSeenAt": "2026-10-14T08:00:03.120Z",
  "isBaseline": false
}
```

The run also writes an `OUTPUT` record (counts, charged events, skipped companies with reasons, warnings) and a short `RESULTS_MD` summary to the key-value store.

### Pricing

**Pay only for results**, per event:

| Event | Price | When |
|---|---|---|
| `company-summary` | $0.003 | One per company whose job board was found and read successfully |
| `new-job` | $0.002 | One per newly detected job after the baseline run |

Nothing is charged for companies that fail, boards that cannot be found, empty/suspicious responses, duplicates, or anything beyond the maximum cost you set for the run. Apify's free plan credit lets you try it at no cost, and volume discounts (Bronze/Silver/Gold) apply automatically. Example: watching 100 companies weekly with about 5 new jobs per company costs roughly 100 x $0.003 + 500 x $0.002 = $1.30 per run.

### How it works

1. **Find the board.** `greenhouse:stripe` goes straight to the board. For a domain, the actor reads the homepage and `/careers`, `/jobs`, `/company/careers` (respecting `robots.txt`) looking for links or embeds of a supported ATS. If nothing is visible it tries board names guessed from the domain and accepts a board only when it lists at least one job (marked `detectionConfidence: low`).
2. **Read the public job-board API** of that ATS (no login, no scraping of job pages).
3. **Compare with the saved state** (a named key-value store, `hiring-signals-<hash>`). First run per company = baseline, so you are not flooded with every open job. Later runs report what changed.
4. **Safety rules.** State is only updated for companies that were read successfully, so a failed request can never show up as "all jobs closed". A board that suddenly returns 0 jobs after having 5 or more is treated as a glitch and ignored. Closed jobs are remembered for 45 days so a re-posted job is not reported as new.

### Limits and honest caveats

- Only the seven ATS listed above. Companies with their own careers page, Workday, iCIMS, Taleo, BambooHR, LinkedIn-only posting and similar are **not** covered.
- A domain guess can be wrong: `probeSlugGuesses` may match a different company with the same short name. Prefer explicit `ats:slug` for important accounts; check `detectionConfidence`.
- Job counts are what the board publishes. Confidential or internal postings, and jobs listed only on aggregators, are invisible.
- Greenhouse's job list does not always include departments; the actor fetches the departments endpoint on a best-effort basis, so `department` can be `null`.
- `postedAt` is `null` where the board does not publish a posting date. `firstSeenAt` is always available and is the time of your run that first saw the job, so its precision equals your schedule interval.
- "Hiring surge" and "trend" are simple rules based on job counts between two runs; they are signals, not measured headcount growth.
- Changing the list of companies without setting `monitorName` starts a fresh baseline (the state name is derived from the list). Set a `monitorName` to keep history while editing the list.

### Legal and ethical notes

The actor reads only publicly available job-board endpoints that the ATS vendors expose for embedding job listings on company websites. It does not log in, bypass protections or collect personal data about applicants. Website pages are fetched with an honest User-Agent, low concurrency and `robots.txt` is honored. Job postings are the property of the respective companies; check the terms of the ATS and the company before republishing postings. You are responsible for lawful use of the output, including GDPR when you combine it with personal data.

### FAQ

**Does it find the job board automatically from a domain?** Usually, when the company links to its board from the homepage or careers page. Otherwise it guesses. For reliable results use `ats:slug`.

**How do I find the slug?** Open the company's job board; it is the last part of the URL: `boards.greenhouse.io/<slug>`, `jobs.lever.co/<slug>`, `jobs.ashbyhq.com/<slug>`, `apply.workable.com/<slug>`, `jobs.smartrecruiters.com/<slug>`, `<slug>.recruitee.com`.

**Why did the first run return no new jobs?** The first run per company is a baseline by design. Enable `includeBaseline` if you want all current jobs once (charged per job).

**What if a company removes a job and posts it again?** Reopened jobs are not reported as new within 45 days.

**Can I filter by role?** Yes, use `keywords` (title or department contains any keyword). Changing keywords does not cause false "new" jobs.

**What happens when I reach my maximum cost?** The run stops softly, `OUTPUT.stoppedReason` is `budget`, and companies not yet reported are not charged; rerunning continues.

**Can I run it hourly?** Yes, but most boards change a few times a day at most; daily or weekly is typical.

### Use it via API

Replace `username` with the actor owner.

```bash
curl -X POST "https://api.apify.com/v2/acts/username~hiring-signals-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies":["greenhouse:stripe","lever:palantir","notion.so"],"keywords":["engineer"],"monitorName":"my-watchlist"}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('username/hiring-signals-monitor').call({
    companies: ['greenhouse:stripe', 'lever:palantir'],
    monitorName: 'my-watchlist',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.rowType === 'new-job'));
```

```python
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
run = client.actor("username/hiring-signals-monitor").call(run_input={
    "companies": ["greenhouse:stripe", "lever:palantir"],
    "monitorName": "my-watchlist",
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["rowType"] == "company-summary" and item["surge"]:
        print(item["companyName"], item["newSinceLast"])
```

### Integrations

Use Apify's **Schedules** to run the actor daily or weekly, then connect the dataset with **Make**, **n8n** or **Zapier** (trigger: "Actor run finished", action: get dataset items) to push `new-job` rows to Slack, a CRM or Google Sheets. The built-in Google Sheets integration and webhooks work as well; filter on `rowType` and `surge`.

### More tools by the same author

- [Career Page Jobs Scraper](https://apify.com/tiger19935/career-page-jobs-scraper): Open jobs from company career pages (Greenhouse, Lever, Ashby and more)
- [Remote Tech Jobs Aggregator](https://apify.com/tiger19935/remote-tech-jobs-aggregator): Remote tech jobs from 7 boards in one feed
- [Decision-Maker Contacts](https://apify.com/tiger19935/decision-maker-contacts): Founders, CEOs and department heads from company websites
- [Website Contact Extractor](https://apify.com/tiger19935/website-contact-extractor): Emails, phones and social links from any list of websites

# Actor input Schema

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

One company per row. Accepted: a website domain (stripe.com), a careers URL (https://boards.greenhouse.io/stripe), or an explicit board as "ats:slug" with ats one of greenhouse, lever, ashby, workable, smartrecruiters, recruitee, teamtailor (example: greenhouse:stripe). Via API you may also pass objects {"name", "domain", "ats", "slug"}. For plain domains the actor looks for a job-board link on the homepage and /careers, /jobs, /company/careers, then tries slug guesses. Explicit "ats:slug" is the most reliable.

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

Only count and report jobs whose title or department contains at least one of these words (case-insensitive), for example engineer, sales, data. Leave empty to include every job. Changing keywords later does not create false 'new' jobs because all jobs are remembered.

## `surgeThreshold` (type: `integer`):

A company is flagged as a hiring surge when at least this many new (keyword-matching) jobs appeared since the previous run. Whole number from 1 to 100000.

## `includeBaseline` (type: `boolean`):

Off (default): the first run for a company only records a baseline and returns just the company summary row. On: the first run also returns one 'new-job' row per currently open job, and you are charged the new-job event for each of them, so use with care on big companies.

## `monitorName` (type: `string`):

Name of this monitor. Runs with the same name share the same saved state (known jobs per company), so use one stable name per watch list and keep it for scheduled runs. Leave empty to derive the state from the list of companies (adding or removing a company then starts a fresh baseline).

## `probeSlugGuesses` (type: `boolean`):

When a domain's website shows no job-board link, try board names derived from the domain (stripe.com -> stripe) on each supported system. A board is accepted only if it lists at least one job; such matches are marked low confidence and could belong to a different company with the same name.

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

Safety limit on the number of companies processed in one run (1 to 2000). Entries beyond the limit are skipped and listed in the run summary.

## `includeFailed` (type: `boolean`):

Add a free (never charged) 'error' row for companies whose job board could not be found or read. Failures are always listed in the run summary (OUTPUT) either way.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "lever:palantir"
  ],
  "keywords": [],
  "surgeThreshold": 5,
  "includeBaseline": false,
  "monitorName": "",
  "probeSlugGuesses": true,
  "maxCompanies": 500,
  "includeFailed": false
}
```

# Actor output Schema

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

All result rows.

## `summary` (type: `string`):

Counts, charged events and warnings.

# 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": [
        "greenhouse:stripe",
        "lever:palantir"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tiger19935/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": [
        "greenhouse:stripe",
        "lever:palantir",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tiger19935/hiring-signals-monitor").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": [
    "greenhouse:stripe",
    "lever:palantir"
  ]
}' |
apify call tiger19935/hiring-signals-monitor --silent --output-dataset

```

## MCP server setup

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

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/iIlsk9TNbWgfJgxJ2/builds/1nN138X6ZvcLIEkwC/openapi.json
