# Hiring Signals Monitor: Roles Opened & Closed (`saiberops/hiring-signals-monitor`) Actor

Weekly hiring snapshot per company from Greenhouse, Lever, Ashby, Recruitee and Personio boards: roles opened and closed since last run, velocity, team and location mix, pay ranges and rule-based signals such as surges and new teams.

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

## Pricing

from $16.00 / 1,000 company snapshots

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: Roles Opened & Closed per Company (Greenhouse, Lever, Ashby)

Watch a list of companies and get **one hiring snapshot per company per run**: how many roles are open, which roles opened and closed since your last run, hiring velocity, the team / location / remote mix, published pay ranges, and plain-language signal tags such as `surge`, `new_department` or `leadership_role_opened`. It reads each employer's own public job board on Greenhouse, Lever, Ashby, Recruitee or Personio, so it's live, not a stale database.

**Built for:**

- **B2B sales and RevOps.** Hiring is the cheapest early buying signal. Three new SDR roles means a sales-tooling purchase is coming; a new Head of Security means security software. Run it weekly over your target accounts and act on the deltas.
- **Agencies.** Spot prospects that just opened marketing, RevOps or engineering roles.
- **VC, PE and analysts.** Headcount momentum for portfolio companies and competitors, by team and location.
- **Recruiting firms.** See which companies are scaling which teams before the roles reach job sites.
- **AI agents.** One structured row per company, no API key.

### Sample output

A company snapshot (role lists shortened):

```json
{
  "recordType": "snapshot",
  "stateKey": "default",
  "company": "coinbase",
  "companyName": "Coinbase",
  "ats": "greenhouse",
  "boardUrl": "https://job-boards.greenhouse.io/coinbase",
  "status": "ok",
  "runAt": "2026-10-31T12:00:00Z",
  "previousRunAt": "2026-10-24T12:00:00Z",
  "daysSincePrevious": 7.0,
  "isBaseline": false,
  "openRoles": 214,
  "openedCount": 12,
  "closedCount": 4,
  "changedCount": 2,
  "reopenedCount": 0,
  "netChange": 8,
  "opened": [{ "globalId": "greenhouse:coinbase:9000001", "title": "VP, Sales", "department": "Sales", "primaryLocation": "Remote - US", "workplaceType": "remote", "jobUrl": "https://www.coinbase.com/careers/positions/9000001", "postedAt": "2026-10-29T00:00:00Z" }],
  "velocity": { "windowDays": 30, "openedLastNDays": 31, "closedLastNDays": 9, "openedPerWeek": 7.23, "closedPerWeek": 2.1, "medianDaysToClose": 41.5, "medianPostingAgeDays": 38.0, "oldestOpenPostingDays": 212.4 },
  "byDepartment": { "Engineering": { "open": 88, "opened": 5, "closed": 1 }, "Sales": { "open": 21, "opened": 4, "closed": 0 } },
  "workplace": { "remote": 120, "hybrid": 61, "onsite": 3, "unknown": 30, "remoteShare": 0.561 },
  "pay": { "jobsWithRange": 198, "share": 0.925, "annualByCurrency": { "USD": { "min": 90000, "max": 410000, "count": 160, "medianMidpoint": 187000 } } },
  "signals": [
    { "tag": "surge", "detail": "opened 12 roles in 7 days vs 3.0/week before" },
    { "tag": "department_expansion:Sales", "detail": "4 roles opened in Sales" },
    { "tag": "leadership_role_opened", "detail": "VP, Sales" }
  ],
  "topSignal": "surge"
}
```

(Illustrative example.) With **change events** on, each opened, closed or changed role is also its own row (`recordType: "change"`, `eventType: "opened" | "closed" | "changed"`, title, departments, location, workplace, employment type, pay, URL, first and last seen). Dataset tabs: **Snapshots**, **Signals**, **Changes**. All rows go to one dataset, so the **Changes** tab also lists snapshot rows (with empty change columns): when you export or integrate change events, filter on `recordType = "change"` (and on `recordType = "snapshot"` for snapshots).

#### How monitoring works

1. The first run for a company records a **baseline**: a full snapshot with `isBaseline: true` and zero deltas.
2. Every later run compares the board with what was saved and reports what opened, closed or changed in between.
3. What each run saw is kept in your Apify account's key-value store named `saiberops-hiring-signals`, per **watchlist name**, so it works from a schedule, a saved task, the API or a manual run, as long as it's the same account and watchlist name. Use different watchlist names for different lists. If you leave the name at `default`, a run started from a saved Apify task uses `task-<task ID>` instead, so each saved task automatically keeps its own memory; runs outside a task share `default`.

### Inputs

| Input | What it does |
|---|---|
| Companies | Job-board URLs, shorthands (`greenhouse:stripe`, `lever:spotify`, `ashby:ramp`, `recruitee:acme`, `personio:acme`) or careers-page URLs. Up to 1,000 per run. |
| Watchlist name | Separate memory per list. Left at `default`, each saved task gets its own (`task-<task ID>`); other runs share `default`. |
| Also output one row per opened, closed or changed role | Change-event rows, charged per row. Off by default. |
| List roles inside each snapshot | Up to 100 opened / closed / changed roles per snapshot, free. On by default. |
| Velocity window (days) | 7–90, default 30. |
| Detect edited roles | Title, location, department, employment type, workplace type or pay changes. On by default. |
| Start over for this watchlist | Records a new baseline. |
| Max jobs read per company | Safety cap (default 5,000). A capped board reports no closures that run; roles it didn't reach are kept, and a role unseen on 4 capped runs in a row is dropped from memory without being reported as closed. |
| Detect the ATS from careers pages / Guess the board name / Respect robots.txt | Same discovery as the ATS Job Feed. |

Schedule it weekly (or daily) in Apify. Paste careers-page URLs rather than plain company names.

### Pricing

Pay per event.

| Event | Free plan | Bronze | Silver | Gold |
|---|---|---|---|---|
| Company snapshot | $0.025 | $0.02 | $0.018 | $0.016 |
| Role change event (optional) | $0.0025 | $0.002 | $0.0018 | $0.0016 |
| Careers page detected | $0.0025 | $0.002 | $0.0018 | $0.0016 |

A 500-company watchlist checked weekly costs about $10 a week at Bronze, plus Apify's standard Actor start fee. **Not charged:** snapshots for companies whose board couldn't be read (`status: "stale"` or `"error"`), suspicious snapshots (see Limits), board URLs and shorthands (no detection needed), cached detections, detections for a company whose snapshot wasn't charged in that run (the detection is cached and later runs reuse it free), and anything after your **Max cost per run**, where the Actor stops cleanly.

### Integrations and API

Send snapshots to Google Sheets, a CRM, Slack, Make, Zapier or n8n through Apify's integrations, or call it from the Apify API. A common setup: weekly schedule, then a Slack alert for every snapshot whose `signals` list isn't empty. AI agents can use it through Apify's MCP server.

### Limits

- **Supported ATSs:** Greenhouse, Lever, Ashby, Recruitee and Personio. Workable, SmartRecruiters, BambooHR, Teamtailor, Rippling, Workday and iCIMS boards are recognised but not read yet.
- **Signal tags are simple, documented rules** (no AI): `surge` (5+ roles opened and at least 3× the recent weekly rate; needs two earlier runs), `new_department` (first role ever seen in a department), `department_expansion:<name>` (3+ roles opened in one department), `first_remote_role`, `leadership_role_opened` (Head of, Director, VP, Chief, C-level titles), `hiring_freeze_signal` (5+ closed, none opened, open roles down 30% or more), `pay_range_published` (first structured pay range on the board), `roles_reopened` (re-posted within 60 days of closing).
- **Safety guards:** if a board can't be read, the saved state is left untouched, so an outage never looks like mass closures. If only Greenhouse's department list fails, roles keep the departments seen before and no role is reported as changed for it. If more than 80% of a company's known roles (at least 5) vanish at once, the snapshot is marked `suspicious`, the known roles are kept, and it isn't charged. If the next run sees exactly the same reduced set of roles, the closures are real: that run's snapshot reports them (with `hiring_freeze_signal` where it applies, and closed-role events if they're on), is charged as usual and carries a `MASS_CLOSURE_CONFIRMED` note. A board that comes back in full in between reports no closures.
- **Pay:** only annual ranges are summarised per currency; no currency conversion. Greenhouse ranges don't state an interval, so it's inferred (see the ATS Job Feed's notes).
- **No job descriptions and no people.** Snapshots and events contain company, role, location, pay, dates and URLs only. Recruiter and hiring-manager names or emails are never output.
- Unlisted Ashby jobs (direct-link-only) are not counted.
- If a run hits the max cost while writing change events, the remaining events for that company are skipped (the snapshot still lists the roles).
- A host that answers 403 or 429 (or a bot challenge) is not called again during that run. No proxies are used.

### FAQ

**Why are all my snapshots "baseline"?** It's the first run for those companies under that watchlist name (or **Start over** was on). The next run shows deltas.

**Can I monitor the same companies for two clients?** Yes: use two watchlist names, or two saved tasks with the name left at `default` (each task then keeps its own memory). To make two tasks share one memory, give them the same watchlist name.

**A company shows `status: "error"`.** Open the `DETECTION` and `RUN_SUMMARY` records: the board name may be wrong, or the careers page doesn't link a supported board.

**How is "days to close" measured?** From the ATS's posted date (or the first run that saw the role) to the run that found it gone.

### Responsible use and data sources

The Actor reads only what each employer already publishes for its own careers page, through the ATS vendors' public, no-login job-board endpoints (Greenhouse Job Board API, Lever Postings API, Ashby public job posting API, Recruitee Careers Site API, Personio's public job XML feed). It identifies itself as `SaiberOpsBot` with a contact address, follows robots.txt (RFC 9309) on every host it calls, paces itself to about two requests per second per host, never logs in, never uses proxies, and stops calling a host that answers 403 or 429. Output is business data about job openings; free-text fields that can contain personal data are never read. Not affiliated with or endorsed by any ATS vendor or employer.

### Support

Found a bug or want another signal rule? Open an issue on this Actor's **Issues** tab and we'll respond there.

*Maintained by Saiber Operations LLC.*

# Changelog

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

# Actor input Schema

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

One per line: job-board URLs (boards.greenhouse.io/stripe, jobs.lever.co/spotify, jobs.ashbyhq.com/ramp, acme.recruitee.com, acme.jobs.personio.de), shorthands such as greenhouse:stripe, or a careers-page URL (the ATS is detected from the page). Up to 1,000 per run.

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

Each watchlist keeps its own memory of previous runs, so one account can track several lists (for example clients-a, competitors). Letters, numbers, - and \_. Left at "default", a run from a saved task uses its own memory (task-<task ID>).

## `emitChangeEvents` (type: `boolean`):

Adds change-event rows (charged per row) next to the company snapshots. Not produced on a company's first (baseline) run.

## `includeRoleLists` (type: `boolean`):

Up to 100 roles per list, with title, department, location, workplace type, URL and posted date. Free.

## `velocityWindowDays` (type: `integer`):

Window for 'opened/closed in the last N days' and the per-week rates.

## `trackContentChanges` (type: `boolean`):

Reports roles whose title, location, department, employment type, workplace type or pay changed.

## `resetState` (type: `boolean`):

Ignore what earlier runs saved for these companies and record a new baseline.

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

Safety cap. If a board has more open roles than this, closures are not reported for it that run (the snapshot says truncated).

## `autoDetect` (type: `boolean`):

For URLs that aren't job-board URLs, read the page once (respecting robots.txt) and look for an embedded or linked supported board. A successful detection is charged once; later runs use a 30-day cache for free.

## `probeGuessedTokens` (type: `boolean`):

Try the domain name (acme for acme.com) as a board name on Greenhouse, Lever and Ashby. Can match an unrelated company with the same name.

## `respectRobots` (type: `boolean`):

On by default. Turn off only for careers pages you own. Job-board API hosts always follow robots.txt.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/coinbase",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "stateKey": "default",
  "emitChangeEvents": false,
  "includeRoleLists": true,
  "velocityWindowDays": 30,
  "trackContentChanges": true,
  "resetState": false,
  "maxJobsPerCompany": 5000,
  "autoDetect": true,
  "probeGuessedTokens": false,
  "respectRobots": true
}
```

# Actor output Schema

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

No description

## `detection` (type: `string`):

No description

## `summary` (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": [
        "https://boards.greenhouse.io/coinbase",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("saiberops/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": [
        "https://boards.greenhouse.io/coinbase",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("saiberops/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": [
    "https://boards.greenhouse.io/coinbase",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp"
  ]
}' |
apify call saiberops/hiring-signals-monitor --silent --output-dataset

```

## MCP server setup

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