# Competitor Hiring Tracker: Open Roles by Company (`steady-scrape/company-hiring-signals`) Actor

Hiring signals per company from its own career site: open roles, new and closed roles, posting velocity, function, seniority, location and remote mix, salary. Look up by name, domain or careers URL, or discover who's hiring. Daily history for trends.

- **URL**: https://apify.com/steady-scrape/company-hiring-signals.md
- **Developed by:** [SteadyScrape](https://apify.com/steady-scrape) (community)
- **Categories:** Jobs, Lead generation, Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 company reports

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

## Company Hiring Signals: Open Roles & Hiring Trends by Company

Get a **hiring report for any company**, built from the company's own career site rather than job-board reposts. Give it company names, domains or careers URLs and you get back **one row per company**: how many roles are open, how many were posted in the last 7, 30 and 90 days, which functions, seniority levels, locations and countries the company is hiring for, how many roles are remote, disclosed salary ranges, and **day-over-day changes (new and closed roles, growth trend)** from our daily history.

You can also **discover companies** without a list, for example "companies with the most open engineering roles in Germany posted in the last 30 days".

Data comes from a daily index of **1.2 million+ open jobs on 31,000+ company career sites** (Greenhouse, Lever, Ashby, Workday, Workable, BambooHR, Recruitee, Personio and Rippling). Boards that aren't indexed yet are read live, and that includes SmartRecruiters.

- 🏢 **Company in, signals out:** names, domains, careers pages or ATS board URLs. Companies are matched through a curated directory, then the index, then the company's own website
- 📈 **Hiring velocity:** roles posted in the last 7/30/90 days, postings per week, plus new and closed roles and open-role change over 1, 7, 30 and 90 days as daily history builds up
- 🧭 **Hiring mix:** functions (Engineering, Sales, Data & AI…), raw departments, seniority, top locations and countries, remote/hybrid/on-site, employment types
- 💰 **Salary:** share of roles with a posted range, plus the median annual midpoint
- ✨ **Highlights:** short readable lines such as "21 Engineering roles in Seattle, US"
- 🔎 **Discovery mode:** rank companies by matching roles, recent postings, total openings, or growth once enough history exists
- 🔔 **Weekly alerts:** `changesOnly` mode outputs only the companies that changed since the last run, with their new and closed roles, and you're not charged for unchanged companies
- 💸 **Pay only for companies found.** Inputs that can't be matched are returned for free so you can see what was missed

### Who uses this

- **Sales and GTM teams:** find accounts that are hiring for the team you sell to, for example companies adding data engineers or growing their sales team in a region
- **Investors and analysts:** follow headcount momentum across a portfolio or watchlist
- **Recruiters and agencies:** see who is hiring fastest in your niche and location
- **Job seekers:** check whether a company is growing or shrinking before you apply

### Quick start

```json
{
  "companies": ["Stripe", "datadoghq.com", "https://jobs.lever.co/palantir", "Anthropic", "nvidia.com"]
}
```

Discovery, which needs no company list:

```json
{
  "discoverFunctions": ["Engineering"],
  "discoverCountries": ["DE"],
  "discoverPostedWithinDays": 30,
  "maxCompanies": 25
}
```

More examples:

- Companies hiring remote data roles in the US: `{"discoverKeywords": ["data engineer", "analytics engineer"], "discoverRemoteOnly": true, "discoverCountries": ["US"]}`
- Fastest-growing sales teams, once 7 days of history exist: `{"discoverFunctions": ["Sales"], "discoverSortBy": "growth7d"}`

### Output

You get one item per company. The Console's overview view shows the headline fields. Key fields:

| Field | Example |
|---|---|
| `company`, `atsPlatform`, `boardUrl` | "Stripe", `greenhouse`, https://job-boards.greenhouse.io/stripe |
| `openRoles`, `openRolesIsLowerBound` | 715, false |
| `postedLast7Days`, `postedLast30Days`, `postedLast90Days`, `postingsPerWeekLast30Days` | 50, 203, 532, 47.4 |
| `trend` | `growing` / `shrinking` / `stable` / `not_enough_history` |
| `openRolesChange` | `{"7d": {"since": "2026-11-02", "from": 690, "to": 715, "change": 25, "changePct": 3.6}, ...}` |
| `newRolesSinceLastSnapshot`, `closedRolesSinceLastSnapshot` | 12, 7 |
| `newRolesLast7Days`, `closedRolesLast7Days`, `newRolesLast30Days`, `closedRolesLast30Days` | filled once the window is covered |
| `hiringHighlights` | `["23 Sales roles in Dublin, IE", ...]` |
| `functions`, `departments` | `[{"function": "Engineering", "openRoles": 178, "share": 0.249, "change": null}, ...]` |
| `topLocations`, `countries`, `countryCount` | `[{"location": "San Francisco, US", "openRoles": 65}]` |
| `seniorityMix`, `seniorityShare` | `{"SENIOR": 120, "MID": 300, ...}` |
| `workplaceMix`, `remoteShare` | `{"remote": 110, "hybrid": 40, "onsite": 300, "unspecified": 265}`, 0.154 |
| `salaryDisclosedShare`, `salary` | 0.62, `{"currency": "USD", "medianAnnualMidpoint": 182000, "rolesWithSalary": 410}` |
| `recentRoles`, `recentClosedRoles` | newest roles with title, department, location, posted date and URL |
| `history` | `{"trackedSince": "2026-10-26", "snapshotDays": 1, "windowsAvailable": [], "openRolesSeries": [...]}` |
| `dataNotes` | caveats for this row, for example Workday limits |
| `status`, `statusDetail`, `resolvedVia`, `matchConfidence` | `ok` (charged), or free: `not_found`, `no_open_roles`, `uncertain_match`, `error`; `directory`, `board_url`, `index_name_match`, `careers_page`, `discovery`; `high` / `medium` |

The run summary is saved as `OUTPUT` in the key-value store. It has counts, how inputs were matched, index freshness, and which history windows exist.

### History and trends: what exists today

Daily company snapshots **started on October 26, 2026**. Each night, after the index refresh, we record every company's open-role count, its function, location and seniority mix, and the IDs of new and closed roles. The trend fields fill in as that history builds up:

| Field | Available from |
|---|---|
| `openRolesChange.1d`, `newRolesSinceLastSnapshot`, `closedRolesSinceLastSnapshot` | the second daily snapshot (about Oct 27, 2026) |
| `openRolesChange.7d`, `newRolesLast7Days`, `trend`, growth7d sort | about Nov 2, 2026 |
| `openRolesChange.30d`, `newRolesLast30Days`, growth30d sort | about Nov 25, 2026 |
| `openRolesChange.90d` | about Jan 24, 2027 |

Until a window exists its field is `null`, and `history.windowsAvailable` lists the windows you can use. You don't need history for `postedLast7Days`, `postedLast30Days` and `postedLast90Days`, because they come from the posting dates on roles that are open now. Roles that were posted and then filled before today aren't counted.

### Weekly alerts / changes only

Want a weekly "what changed" report for a watchlist instead of the full report every time? Turn on **`changesOnly`** and give the list a **`monitorId`**:

```json
{
  "companies": ["Stripe", "datadoghq.com", "https://jobs.lever.co/palantir", "Figma", "Okta"],
  "changesOnly": true,
  "monitorId": "weekly-saas-accounts"
}
```

How it works:

- **First run = baseline.** Each company gets its normal full report, marked `changeType: "baseline"`, and its current open roles are saved. If you set `emitBaseline: false`, the baseline is saved silently instead: nothing is output and nothing is charged.
- **Later runs output only companies that changed** since the last run with the same `monitorId`. Unchanged companies are left out and **aren't charged**.
- A change row is the company's full current report plus:
  - `changeType`: `changed`, `status_changed`, `baseline` or `check_failed`
  - `changesSince`: when the previous state was saved
  - `changeSummary`: lines you can read at a glance, e.g. `"Open roles up 12 (+15.0%): 80 -> 92"`, `"7 new role(s), 2 closed"`, `"New department: Robotics (2 roles)"`, `"Now hiring in France (1 role)"`
  - `newRolesCount`, `closedRolesCount`, `openRolesDelta`
  - `changes`: `openRoles` {from, to, change, changePct}, `velocityAlert` (`up` / `down` / null), `newRoles` and `closedRoles` (title, function, department, location, posted date, plus a URL for new roles), `functionChanges`, `newDepartments` / `closedDepartments`, `newLocations` / `closedLocations`, `newCountries` / `droppedCountries`, and `postedLast30Days` from/to
- The **Changes** view in the Console shows the headline fields.

When is a company reported?
| Option | Default | Meaning |
|---|---|---|
| `alertMinChangedRoles` | 1 | Report when at least this many roles were added plus closed. Smaller changes keep adding up and are reported once they reach the threshold |
| `alertMinChangePct` + `alertMinChangeRoles` | 10 % and 3 | `velocityAlert` fires when open roles move by at least 10 % **and** at least 3 roles. Set the percent to 0 to use the role count only. The same rule marks a `functionChanges` entry as `notable` |
| `changedRolesLimit` | 25 | How many new and closed roles are listed per company. The counts are always complete |
| `resetBaseline` | false | Start this monitor again from a fresh baseline |

A status change (for example a company whose roles all close) and a new or dropped department or country are always reported. A company that can't be checked on a run (`check_failed`) is reported free of charge, and its saved state is kept so the next run still compares against the last good check. If a run reaches your maximum charge, the changes that weren't delivered are reported on the next run.

**State storage:** the saved state lives in your own Apify account, in the key-value store `company-hiring-signals-monitors` (one record per company per monitor). Different `monitorId`s don't affect each other. If you run without a `monitorId`, the ID defaults to the saved task's ID, or to `default`. Giving each watchlist its own ID is clearer. Companies you add to the list later get a baseline row the first time they appear. Changes only works with a Companies list, not with discovery.

#### Example schedule (weekly, Monday 8:00)

1. In the Console, open this Actor, enter the input above and click **Save as a new task** (for example "Weekly hiring alerts: SaaS accounts").
2. Go to **Schedules > Create new**, set the cron expression to `0 8 * * 1` and pick your timezone. Under **Add**, choose the task you just saved.
3. Optional: add an integration on the task (email, Slack, webhook, Zapier or Make) that runs when a run succeeds. It sends you the `changes` view link, for example `https://api.apify.com/v2/datasets/{defaultDatasetId}/items?view=changes`.

Because the job index refreshes once a day, running a monitor more than once a day won't show anything new. Weekly or daily schedules work best.

### Good to know

- **Workday:** a career site lists at most 2,000 roles in the index, or 1,000 when read live. Very large employers are marked `openRolesIsLowerBound: true`. Workday shows posting dates only up to 30 days, so `postedLast90Days` is a lower bound there.
- **Several boards per company:** some companies run more than one career site. The main one is reported, and the others are listed in `otherBoards`.
- **Functions and seniority** are worked out from job titles and departments. Roles that don't fit a function are counted as `Other`.
- **Matching by name** uses the company names in the index. For ambiguous names, give a domain or the careers URL instead.
- Only public job postings are read. No personal data is collected.

### Pricing

Pay per event: you're charged per company reported with `status: ok`. These are free: companies that aren't found, matched career sites with no open roles, uncertain name matches and errors. A run stops cleanly at your maximum charge.

Price per company report: **$0.015** on the Apify Free plan, **$0.013** on Starter (Bronze), **$0.012** on Scale (Silver) and **$0.010** on Business (Gold). Example: 50 companies cost $0.75 on the Free plan, or $0.50 on Business.

In **changes only** mode you pay the same per-company price, but only for companies that are output: baseline reports on the first run, then companies that changed. Unchanged companies cost nothing.

### Related Actors

- [Career Site Jobs Search](https://apify.com/steady-scrape/career-jobs-search): search the jobs themselves, with full filters and descriptions
- [Remote Jobs API](https://apify.com/steady-scrape/remote-jobs-api): remote jobs with eligibility and time-zone detection
- [ATS Job Feed](https://apify.com/steady-scrape/ats-job-feed): every job from a list of company career sites

Questions or a missing company? Open an issue on the Actor page. SteadyScrape (steady-scrape)

# Actor input Schema

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

Company names ("Stripe"), domains ("datadoghq.com"), careers page URLs or ATS board URLs ("https://jobs.lever.co/palantir"). Up to 5,000. Leave empty to use discovery instead.

## `recentRolesLimit` (type: `integer`):

How many of the newest roles (title, department, location, URL) to list on each company row.

## `sniffCareerPages` (type: `boolean`):

For a domain that is not in the directory or index, open its homepage and careers page to find the ATS board. Slower, but finds more companies.

## `liveFetchUnindexed` (type: `boolean`):

If a board URL or careers page points to a supported ATS board that the nightly index does not cover yet, read it live. Live rows have no history yet.

## `changesOnly` (type: `boolean`):

Output only what changed since the previous run of this monitor: new and closed roles, open-role moves, new or dropped departments, locations and countries. The first run for a company outputs its full report as the baseline. Needs a Companies list (not discovery).

## `monitorId` (type: `string`):

A stable name for this watchlist, e.g. "weekly-saas-accounts". Runs with the same ID compare against each other; use different IDs for different lists. The state is kept in the key-value store company-hiring-signals-monitors in your account. Default: the saved task's ID, or "default".

## `emitBaseline` (type: `boolean`):

On: the first run (and any company added later) outputs its full report, charged as a normal company report. Off: the baseline is saved silently and nothing is output or charged until something changes.

## `alertMinChangedRoles` (type: `integer`):

Report a company when at least this many roles were added or closed. Smaller changes keep adding up and are reported once they reach it. Velocity, department and country changes are always reported.

## `alertMinChangePct` (type: `integer`):

Flag velocityAlert up/down when open roles change by at least this percent AND at least the role count below. 0 = use the role count only.

## `alertMinChangeRoles` (type: `integer`):

Minimum absolute open-role change for a velocity alert (also used for notable function changes).

## `changedRolesLimit` (type: `integer`):

How many new and closed roles to list in each change row (counts are always complete).

## `resetBaseline` (type: `boolean`):

Ignore the saved state for this monitor and start again with a fresh baseline.

## `discoverFunctions` (type: `array`):

Count only roles in these functions.

## `discoverKeywords` (type: `array`):

Count only roles whose title contains one of these words or phrases, e.g. "data engineer", "SRE". Put a phrase in quotes for an exact match.

## `discoverCountries` (type: `array`):

ISO 2-letter country codes, e.g. US, DE, GB.

## `discoverLocations` (type: `array`):

City or region words in the job location, e.g. Berlin, "New York", Bay Area.

## `discoverRemoteOnly` (type: `boolean`):

Count only roles marked remote.

## `discoverPostedWithinDays` (type: `integer`):

Count only roles with a posting date in this window. Roles without a posting date are excluded when this is set.

## `discoverPlatforms` (type: `array`):

Limit discovery to these career-site platforms (fewer platforms = faster run).

## `discoverMinMatchingRoles` (type: `integer`):

Only report companies with at least this many matching roles.

## `discoverSortBy` (type: `string`):

Growth sorts use the daily history; until that window exists the run falls back to matching roles and says so in the log and summary.

## `discoverExcludeJobAggregators` (type: `boolean`):

Skip known career sites that re-post other employers' jobs.

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

How many companies discovery reports (each reported company is one charged result).

## Actor input object example

```json
{
  "companies": [
    "Stripe",
    "datadoghq.com",
    "https://jobs.lever.co/palantir",
    "Anthropic",
    "nvidia.com"
  ],
  "recentRolesLimit": 10,
  "sniffCareerPages": true,
  "liveFetchUnindexed": true,
  "changesOnly": false,
  "emitBaseline": true,
  "alertMinChangedRoles": 1,
  "alertMinChangePct": 10,
  "alertMinChangeRoles": 3,
  "changedRolesLimit": 25,
  "resetBaseline": false,
  "discoverRemoteOnly": false,
  "discoverMinMatchingRoles": 3,
  "discoverSortBy": "matchingRoles",
  "discoverExcludeJobAggregators": true,
  "maxCompanies": 25
}
```

# Actor output Schema

## `companies` (type: `string`):

One item per company. Use /items without ?view for every field.

## `changes` (type: `string`):

In changesOnly mode: one item per changed company, with what changed since the last run.

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

Counts, how inputs were resolved, index freshness and which history windows exist.

# 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": [
        "Stripe",
        "datadoghq.com",
        "https://jobs.lever.co/palantir",
        "Anthropic",
        "nvidia.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("steady-scrape/company-hiring-signals").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": [
        "Stripe",
        "datadoghq.com",
        "https://jobs.lever.co/palantir",
        "Anthropic",
        "nvidia.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("steady-scrape/company-hiring-signals").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": [
    "Stripe",
    "datadoghq.com",
    "https://jobs.lever.co/palantir",
    "Anthropic",
    "nvidia.com"
  ]
}' |
apify call steady-scrape/company-hiring-signals --silent --output-dataset

```

## MCP server setup

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

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/bWgkhl4jaNhyxtl9V/builds/jIWasw0bbwo2h9X40/openapi.json
