# Hiring Pulse (`roseapps/hiring-pulse`) Actor

Monitors company ATS job boards over time and emits hiring-signal deltas: net role growth, new senior hires, first sales hire, function-mix shifts.

- **URL**: https://apify.com/roseapps/hiring-pulse.md
- **Developed by:** [Elliot Rose](https://apify.com/roseapps) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.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.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

## hiring-pulse

Company hiring-signal monitor. Give it a list of company domains; it finds
each company's applicant-tracking-system (ATS) job board, snapshots the
currently open roles, and diffs that snapshot against the previous run
(stored in a named key-value store so scheduled runs stay stateful). It
sells the **change** — net growth, new senior hires, a first sales hire,
function-mix shifts — not the job list itself.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `domains` | string\[] | — (required) | Company website domains, e.g. `"acme.com"`. No protocol/path. |
| `atsOverrides` | array | `[]` | `[{domain, ats, token}]` — skip discovery for a domain by declaring its ATS and board token directly. |
| `emitJobs` | boolean | `false` | Also push one dataset row per currently open job. |
| `onlyChanged` | boolean | `false` | After a company's first snapshot, skip pushing its signal row when nothing changed (delta 0, no new/closed roles). |
| `stateStoreName` | string | `"hiring-pulse-state"` | Named key-value store used to persist ATS discovery cache (`ats:{domain}`) and job snapshots (`snap:{domain}`). |
| `maxConcurrency` | integer | `5` | Max domains processed concurrently. |

### Output

One **signal row** per domain per run in the default dataset, e.g.:

```json
{
  "domain": "acme.com",
  "ats": "greenhouse",
  "token": "acme",
  "open_roles": 24,
  "prev_open_roles": 19,
  "delta": 5,
  "new_roles": [
    { "id": "4006", "title": "Director of Sales", "department": "Sales",
      "location": "Remote", "remote": true,
      "url": "https://boards.greenhouse.io/acme/jobs/4006",
      "posted_at": "2026-08-10T15:00:00-05:00", "seniority": "lead",
      "function": "sales" }
  ],
  "closed_roles": [],
  "new_titles": ["Director of Sales", "Senior Backend Engineer"],
  "senior_hires": [ /* Job objects with seniority in lead/exec/senior */ ],
  "first_sales_hire": true,
  "function_counts": { "engineering": 12, "sales": 3, "design": 2, "other": 7 },
  "function_deltas": { "engineering": 2, "sales": 1 },
  "remote_share": 0.58,
  "score": 27,
  "first_snapshot": false,
  "snapshot_at": "2026-08-29T09:00:00+00:00",
  "prev_snapshot_at": "2026-08-15T09:00:00+00:00",
  "urls": {
    "board_url": "https://boards.greenhouse.io/acme",
    "career_page": "https://acme.com/careers"
  }
}
```

With `emitJobs: true`, one additional row per currently open job is pushed
(the `Job` object plus `domain`/`ats`).

Companies whose ATS could not be identified still get a row, with
`"ats": null`, `"open_roles": null`, and `"first_snapshot": true` (see
Limitations).

### Pricing events

Pay-per-event; wrap every charge in try/except so local runs (no PPE
config) never crash.

| Event | Price | When |
|---|---|---|
| `company-snapshot` | $0.004 | Once per pushed signal row. |
| `signal-event` | $0.02 | Per changed role (new + closed), only after the first snapshot — i.e. you're charged for detected *changes*, not for the baseline. |
| `job-row` | $0.0005 | Per pushed job row, only when `emitJobs: true`. |

### MCP tools

This Actor is designed to back two MCP tools for agent/LLM callers:

- `get_hiring_signals(domain)` — returns the latest signal row (and, if
  requested, recent history) for one company, backed by the `snap:{domain}`
  and dataset rows this Actor produces.
- `find_companies_hiring(role, stage)` — surfaces companies whose latest
  signal shows growth in a given function (`role`, e.g. "engineering",
  "sales") and/or a `first_sales_hire`/`senior_hires` pattern matching an
  early go-to-market `stage`. Implement as a query over accumulated
  `company-snapshot` rows across runs of this Actor.

### Limitations

- **Unknown/custom ATS**: companies that don't use Greenhouse, Lever,
  Ashby, SmartRecruiters, or Workable (or host a heavily customized/JS-only
  careers page we can't parse without a browser) return `"ats": null`. Use
  `atsOverrides` if you already know a company's board token, or extend
  `src/ats.py` with a new adapter.
- Discovery is a two-step heuristic (crawl known careers-page paths for a
  board link, then probe each ATS API with the domain's second-level label
  as a guessed token) — it can miss boards on non-obvious paths or under a
  token that doesn't match the domain name.
- `remote` is `null` (not `false`) whenever the source location text has no
  clear remote/on-site signal — we never guess.
- No Playwright/browser rendering: careers pages that only render job
  listings via client-side JS (no server-rendered link/iframe to the ATS)
  won't be discovered by the crawl step, only by the probe-by-token
  fallback.
- Seniority/function classification is keyword-based (see `src/classify.py`)
  and works from the title/department strings alone — it will occasionally
  miscategorize unconventional title formats.

# Actor input Schema

## `domains` (type: `array`):

Company website domains to monitor (no protocol/path), e.g. "acme.com".

## `atsOverrides` (type: `array`):

Skip discovery for specific domains by declaring their ATS and board token directly.

## `emitJobs` (type: `boolean`):

Also push one dataset row per currently open job, charged separately as job-row.

## `onlyChanged` (type: `boolean`):

After a company's first snapshot, skip pushing its signal row when nothing changed (delta 0, no new/closed roles).

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

Named key-value store used to persist the ATS discovery cache and job snapshots across runs, so scheduled runs stay stateful.

## `maxConcurrency` (type: `integer`):

Maximum number of domains processed concurrently.

## Actor input object example

```json
{
  "domains": [
    "stripe.com"
  ],
  "atsOverrides": [],
  "emitJobs": false,
  "onlyChanged": false,
  "stateStoreName": "hiring-pulse-state",
  "maxConcurrency": 5
}
```

# 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 = {
    "domains": [
        "stripe.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("roseapps/hiring-pulse").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 = { "domains": ["stripe.com"] }

# Run the Actor and wait for it to finish
run = client.actor("roseapps/hiring-pulse").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 '{
  "domains": [
    "stripe.com"
  ]
}' |
apify call roseapps/hiring-pulse --silent --output-dataset

```

## MCP server setup

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

```

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/xgf4EMj90bQd6x6Kf/builds/0kwEI0MUTTSHvETOU/openapi.json
