# Hiring Signals Monitor | Greenhouse, Lever, Ashby Job Events (`kesar_chen/hiring-signals-monitor`) Actor

Job events (opened, updated, closed, reopened) with stable IDs plus a company hiring summary from public Greenhouse, Lever and Ashby boards. No login. Pay per company monitored and per event.

- **URL**: https://apify.com/kesar_chen/hiring-signals-monitor.md
- **Developed by:** [Kesar Chen](https://apify.com/kesar_chen) (community)
- **Categories:** Jobs, Lead generation, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 83.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 company monitoreds

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

Give it a list of companies. Get **job events**, not job dumps: every run compares each company's public job board with the last run and writes `opened`, `updated`, `closed` and `reopened` events with a stable job ID and a sequence number, plus one **company summary** per run with open jobs, 7-day and 30-day net hiring, breakdown by function and location, the first hire in a function in six months, and salary coverage. Built on the public job boards of Greenhouse, Lever, Ashby, Recruitee, Workable, Teamtailor, Gem and Personio, no login, no key. **Pay per company monitored and per event, no start fee**: 200 companies checked daily is about $25 a month plus $1 per 1,000 events.

### Why events instead of rows

Job-posting APIs sell you rows and leave the hard part to you: deduplicating across runs, noticing what disappeared, and working out that a company just posted its first sales role. Closed roles are as much a signal as new ones, and nobody sends them. This Actor keeps a snapshot per board in a named key-value store and emits only what changed:

| Event | Fires when | Carries |
|---|---|---|
| `opened` | a job ID appears for the first time | title, function, seniority, locations, salary with source, liveness, URL, `note` such as `first sales role in 180+ days` |
| `updated` | title, location or description changed | the new values |
| `closed` | an ID disappeared while the board itself answered | `daysOpen`, the last known title and URL |
| `reopened` | a closed ID came back, or Greenhouse / Gem re-posted the same requisition under a new ID | the date it was closed |
| `board_unreachable` | the board did not answer | the miss count; **no jobs are marked closed** on a miss |

Every event has `seq`, increasing per board. Store the last `seq` you processed and resume from it; each event is billed once.

### What you get that job APIs do not

- **A company row per run** with `openJobs`, `opened7d`, `closed7d`, `net7d`, `opened30d`, `closed30d`, `byFunction`, `byDepartment`, `byLocation`, `remoteShare`, `firstHireInFunction`, `salaryCoverage`, `staleShare90d`, and the mapping audit (`source`, `lastVerifiedAt`, `consecutiveMisses`). That is the "is this account ramping up or freezing" answer in one row.
- **Salary with provenance.** `source: "structured"` when the board publishes a range (Ashby and Greenhouse do; Lever, Recruitee and Teamtailor can), `source: "description"` when it was parsed from the text with a fixed rule and a confidence score. Never estimated.
- **Function and seniority by fixed rules**, not a model: engineering, sales, marketing, customer, people, finance, legal, product, design, data, security, infrastructure, operations; intern through executive.
- **Liveness you can explain, not a ghost-job score.** Every job and event carries `liveness`: days alive since the board's publish date, how many consecutive runs it was seen, how often it was reopened, and how many open jobs on the board share its title (multi-city duplicates). The company row adds `staleShare90d`, the share of open jobs older than 90 days. You decide the threshold; the evidence is on the row.
- **Domain in, board out.** Paste `stripe.com`; the Actor reads the careers page, finds the Greenhouse, Lever, Ashby, Recruitee, Workable, Teamtailor, Gem or Personio board, verifies it answers, and remembers the mapping with the date it was last verified. A board that merely shares the company's name is not accepted unless it provably belongs to that domain. Companies with no discoverable board are skipped and never charged.
- **Alerts built in.** Post matching events to Slack, Discord, Feishu or any webhook, filtered by event type and function, with an optional "net drop over 7 days" trigger for hiring freezes.
- **Honest degradation.** A board that stops answering produces `board_unreachable` and a `status: unreachable` company row, not a wave of false `closed` events. Three misses in a row mark it `retired` and keep it in the output so you can see it.

### Who uses it

- **Sales and SDR teams** watch target accounts: `notifyFunctions: ["sales"]` posts every new sales role; `firstHireInFunction` is the "they are building a team" trigger.
- **Recruiters** watch competitors' openings and closings per function and location.
- **Investors and analysts** track `net7d` and `net30d` across a portfolio or a sector list, with the board-level audit trail.
- **AI agents** read `/events` through the Apify MCP server; every field is described in the dataset schema.

### Run it in the Apify Console

1. Paste companies into **Companies**: domains (`stripe.com`), board URLs, or `greenhouse:stripe` / `lever:spotify` / `ashby:ramp` / `recruitee:bunq` / `workable:epignosis` / `teamtailor:career.teamtailor.com` / `gem:gem` / `personio:personio`.
2. Leave **Write current jobs on first run** on if you want the baseline rows; turn it off if you only want changes.
3. Click **Start**. The first run stores the baseline and writes company rows. Schedule the Task (daily or every 6 hours); from the second run on you get events.
4. Optional: fill **Notify: webhook URL** and pick event types and functions.

Use one **State store name** per monitoring list. The named store holds each board's snapshot and the domain-to-board mapping, and never expires.

### Pricing

| Event | Price |
|---|---|
| Company monitored (one board checked in one run, changed or not) | $0.004 |
| Job event (opened / updated / closed / reopened written) | $0.001 |
| Job row (baseline rows on a board's first run, optional) | $0.001 |
| Board resolved (a domain mapped to a board for the first time) | $0.01 |

| Scenario | Cost |
|---|---|
| 50 companies, daily, first month | about $6 plus baseline rows once (a 300-job board is $0.30) |
| 200 companies, daily | about $24 a month plus events, typically a few dollars |
| 1,000 companies, daily | about $120 a month plus events |

Unchanged boards still count as monitored (the check is the service), but cost nothing in events. Apify platform usage is included. Paid Apify plans get lower per-event prices, shown on the Actor page.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `companies` | list | required | Domains, board URLs, or `system:slug` for any of the eight systems |
| `resolveUnknown` | boolean | `true` | Resolve plain domains via the careers page; a same-name board is used only if it provably belongs to the domain |
| `emitSnapshotRows` | boolean | `true` | Write current jobs (`type: job`) on a board's first run |
| `includeDescription` | boolean | `false` | Include posting text in job rows |
| `maxJobsPerBoard` | integer | `0` | Safety cap, `0` = all |
| `stateStoreName` | text | `hiring-signals-state` | One per monitoring list. If a store with this name already exists in your account but was created by another Actor, the run logs a warning and uses `<name>-<actor id>` instead |
| `notifyWebhookUrl` | text | empty | Slack, Discord, Feishu or any HTTPS webhook |
| `notifyEvents` | list | `opened, closed, reopened` | Event types to post |
| `notifyFunctions` | list | `[]` | Only these functions; empty = all |
| `notifyMinNet7dDrop` | integer | `0` | Also post when open jobs fell by at least this many in 7 days |
| `notifyMaxItems` | integer | `15` | Events per message |

Minimal example:

```json
{ "companies": ["stripe.com", "ramp.com", "lever:spotify"] }
```

### Output

Event row:

```json
{
  "type": "event",
  "seq": 42,
  "eventType": "opened",
  "at": "2026-10-26T08:00:00.000Z",
  "domain": "ramp.com",
  "ats": "ashby",
  "slug": "ramp",
  "jobKey": "ashby:ramp:3f1c…",
  "title": "Enterprise Account Executive",
  "department": "Sales",
  "jobFunction": "sales",
  "seniority": { "value": "mid", "source": "title-rule" },
  "liveness": { "daysAlive": 0, "verifications": 1, "reopenCount": 0, "sameTitleOpen": 1 },
  "locations": ["New York"],
  "remote": false,
  "salary": { "min": 150000, "max": 200000, "currency": "USD", "period": "year", "source": "structured", "confidence": 1 },
  "url": "https://jobs.ashbyhq.com/ramp/3f1c…",
  "firstSeenAt": "2026-10-26T08:00:00.000Z",
  "closedAt": null,
  "daysOpen": 0,
  "note": "first sales role in 180+ days"
}
```

Company row:

```json
{
  "type": "company",
  "at": "2026-10-26T08:00:00.000Z",
  "domain": "ramp.com",
  "ats": "ashby",
  "slug": "ramp",
  "status": "ok",
  "openJobs": 167,
  "opened7d": 9,
  "closed7d": 4,
  "net7d": 5,
  "opened30d": 31,
  "closed30d": 22,
  "byFunction": { "engineering": 70, "sales": 41, "customer": 12 },
  "byLocation": { "New York": 120, "Remote": 30 },
  "remoteShare": 18.0,
  "firstHireInFunction": ["sales"],
  "salaryCoverage": 95.8,
  "staleShare90d": 12.0,
  "mapping": { "source": "careers-page", "firstSeenAt": "2026-10-01T08:00:00.000Z", "lastVerifiedAt": "2026-10-26T08:00:00.000Z", "consecutiveMisses": 0 },
  "runNote": "board returned 167 jobs"
}
```

### Limitations

- Covers Greenhouse, Lever, Ashby, Recruitee, Workable, Teamtailor, Gem and Personio boards. Companies on Workday, SmartRecruiters, iCIMS, SuccessFactors, LinkedIn-only or Indeed-only postings are not resolved; they are reported as "no job board found" and not charged.
- Resolution reads the public careers page. Sites that render the link only in a browser may not resolve; give the board URL or `ats:slug` directly in that case.
- Salary ranges exist only where the company publishes them; coverage per company is in `salaryCoverage`.
- The first run of a board is a baseline and produces no events. Hiring windows (`opened7d`, `net30d`) fill in as history accumulates.
- Events are computed from what the board serves. A posting removed and re-added within one interval between runs is seen as unchanged.
- Teamtailor sites on a custom domain are found when the careers page is the Teamtailor site itself; otherwise give `teamtailor:<careers host>`.
- Workable's board feed does not include salary; Personio's feed does not include it either. Those rows carry salary only when parsed from the description.

### Scheduling and integrations

Save the input as a Task, attach a Schedule, and point `notifyWebhookUrl` at Slack or Discord, or push the dataset into Clay, HubSpot, Google Sheets or n8n through Apify integrations. For an API, read the dataset with `?fields=` and filter on `seq`.

### FAQ

**Is it legal?**
The job boards involved publish these listings without login or key for exactly this kind of distribution; the Actor reads them as any browser would, stores no personal data, and omits full posting text by default. How you use the output is your responsibility.

**Why no Workday or LinkedIn?**
Their terms forbid automated access or they sit behind anti-bot checks. This Actor only uses sources whose public job boards are meant to be read.

**Can I get all companies on Greenhouse?**
No. There is no directory of boards; you bring the list of companies you care about, which is also the only list that matters for your pipeline.

**Is this affiliated with any of the job board systems?**
No. Independent tool, not endorsed by any of them.

**How do I know a source is still working?**
A daily check hits one known board per system. The result row (`type: "canary"`) carries the 7-day success rate per system; a system that fails the check fails the run, so a Schedule with a failure alert tells you the same day. Collected since 2026-10-26; once there are 30 days of history the table goes into this README.

### See also

By the same author, same schema conventions: [App Store Reviews Scraper](https://apify.com/kesar_chen/app-store-reviews-scraper), [Google Play Reviews Scraper](https://apify.com/kesar_chen/google-play-reviews-scraper), [Steam Reviews Scraper](https://apify.com/kesar_chen/steam-reviews-scraper).

### Support

Bugs, a board that does not resolve, or a job board system you need: open an issue on the Issues tab. Reports go to a human.

# Actor input Schema

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

One per line: a company domain (stripe.com), a job board URL (https://job-boards.greenhouse.io/stripe, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/ramp, https://bunq.recruitee.com, https://apply.workable.com/epignosis, https://career.teamtailor.com, https://jobs.gem.com/gem, https://personio.jobs.personio.de), or <system>:<slug> such as greenhouse:stripe, lever:spotify, ashby:ramp, recruitee:bunq, workable:epignosis, teamtailor:career.teamtailor.com, gem:gem, personio:personio. Domains are resolved to a board via the careers page.

## `resolveUnknown` (type: `boolean`):

For plain domains, look for a Greenhouse, Lever, Ashby, Recruitee, Workable, Teamtailor, Gem or Personio board on the careers page; a same-name board is used only if it provably belongs to the domain. Off = only board URLs and system:slug inputs are monitored; plain domains are skipped.

## `emitSnapshotRows` (type: `boolean`):

On a board's first run, write one row per currently open job (type = job) so you have a baseline. Later runs write events only. Charged per job row.

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

Off by default: rows carry derived fields (salary, seniority, function) and the original URL, not the full posting text.

## `maxJobsPerBoard` (type: `integer`):

0 = all. Large boards (OpenAI ~800, Stripe ~700) are fine; this is a safety cap.

## `stateStoreName` (type: `string`):

Named key-value store that keeps each board's snapshot, sequence number and the domain→board mapping. Named stores never expire. Use one store per monitoring list.

## `notifyWebhookUrl` (type: `string`):

Slack incoming webhook, Discord channel webhook, Feishu/Lark custom bot, or any HTTPS endpoint. After the run, one message per company that had matching events. No extra charge.

## `notifyEvents` (type: `array`):

Which event types to post.

## `notifyFunctions` (type: `array`):

Limit alerts to job functions: engineering, sales, marketing, customer, people, finance, legal, product, design, data, security, infrastructure, operations. Empty = all.

## `notifyMinNet7dDrop` (type: `integer`):

Also post when a company's open jobs fell by at least this many over 7 days (hiring freeze signal). 0 = off.

## `notifyMaxItems` (type: `integer`):

The rest are summarised as a count; all of them are in the dataset.

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

Leave on monitor.

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

ats:slug entries to hit in probe mode.

## `burst` (type: `integer`):

Concurrent requests fired at once per board.

## `sustainedRps` (type: `integer`):

Requests per second during the sustained phase.

## `sustainedSeconds` (type: `integer`):

How long the sustained phase runs.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "lever:spotify",
    "ashby:ramp"
  ],
  "resolveUnknown": true,
  "emitSnapshotRows": true,
  "includeDescription": false,
  "maxJobsPerBoard": 0,
  "stateStoreName": "hiring-signals-state",
  "notifyEvents": [
    "opened",
    "closed",
    "reopened"
  ],
  "notifyFunctions": [],
  "notifyMinNet7dDrop": 0,
  "notifyMaxItems": 15,
  "mode": "monitor",
  "boards": [
    "greenhouse:stripe",
    "lever:spotify",
    "ashby:ramp"
  ],
  "burst": 30,
  "sustainedRps": 5,
  "sustainedSeconds": 60
}
```

# Actor output Schema

## `results` (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 = {
    "companies": [
        "greenhouse:stripe",
        "lever:spotify",
        "ashby:ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kesar_chen/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:spotify",
        "ashby:ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("kesar_chen/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:spotify",
    "ashby:ramp"
  ]
}' |
apify call kesar_chen/hiring-signals-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kesar_chen/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/hVGAI5Qx3ub23kqyJ/builds/qVpdF8Xnbpn9q4sP2/openapi.json
