# Hiring Signals Tracker: Job Openings by Company (`edgef-4/hiring-signals`) Actor

Hiring signals tracker for company job boards on Greenhouse, Lever and Ashby: per company, the count of job openings, open roles by department and location, new job postings, roles opened and closed since the last run, and your role rules. A job board monitor: schedule it to track company hiring.

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

## Pricing

from $4.00 / 1,000 company report delivereds

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?

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 tracker for company job boards on Greenhouse, Lever and Ashby: per company, the count of job openings, open roles by department and location, new job postings, roles opened and closed since the last run, and your role rules. A job board monitor: schedule it to track company hiring. Try it: run the prefilled input (GitLab on Greenhouse, Palantir on Lever, Linear on Ashby) and open the Hiring signals by company table.

### What it does

Give it the job boards of the companies you follow; it returns one report per company.
Each report counts open roles by department and location, applies your own role rules, and says how many roles opened and closed since the last run.
It reads the public job board endpoints of Greenhouse, Lever and Ashby.
It honours robots.txt and its Crawl-delay on every host it reads: when a host states a Crawl-delay, the Actor waits at least that long between two requests to that host (a value over 30 seconds is honoured as 30 seconds and noted in the run summary).

### Who uses it

- Sales teams who treat new openings in a department as a buying signal ("three new data engineering roles at this account").
- Investors and analysts who watch hiring at a list of companies week by week.
- Recruiters who track where competitors are hiring.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `boards` | list | required | Board addresses such as `https://boards.greenhouse.io/<board>`, `https://jobs.lever.co/<board>`, `https://jobs.ashbyhq.com/<board>`, or objects `{"platform": "lever", "slug": "<board>"}` |
| `rules` | list of objects | empty | `{"name": "sales", "titleKeywords": ["account executive"], "departments": ["Sales"]}`: counts the open roles whose title contains a keyword as a whole word (letters and digits of any alphabet belong to a word, so `rations` does not match `Opérations`) and, when departments are given, whose department or team is one of them |
| `deltaMode` | true or false | true | Compares with the last run that read the same board |

### Output

One record per company board. A board that cannot be read gives one record with `status` set to `not_found`, `blocked` or `error`.
Before it reads a board, the Actor reads the robots.txt of the board's host, once per host per run. When that robots.txt does not allow this Actor, the board gives one `blocked` record with `error` set to `robots_disallow`. When that robots.txt cannot be read (a server error, a "too many requests" answer, a timeout, a network failure, a redirect to another site or more than five redirects), the board gives one `error` record with `error` set to `robots_unreadable`. A redirect of robots.txt inside the same site is followed. When a host name does not resolve, the board gives one `error` record with `error` set to `name_not_resolved`. In both cases nothing is read from the board, nothing is charged and nothing is remembered. A robots.txt that answers with another 4xx code, such as 404, counts as no rules.
When the departments list of a Greenhouse board cannot be read (a request that still fails after three retries, or a robots.txt that disallows it), the board gives one `error` record with `error` set to `departments_unreadable` instead of a report with every role under unknown: not charged and not remembered.
Board names are read in lower case, so `boards.greenhouse.io/GitLab` and `boards.greenhouse.io/gitlab` are one board, read and charged once. Every record keeps the board as you gave it in `board_input`.
Sample record (SYNTHETIC: example.com data, not a real company):

```json
{
  "id": "greenhouse:examplecorp",
  "platform": "greenhouse",
  "company_slug": "examplecorp",
  "open_roles": 42,
  "new_roles_since_last_run": 3,
  "closed_roles_since_last_run": 1,
  "first_run": false,
  "by_department": {"Engineering": 20, "Sales": 12, "Support": 10},
  "by_location": [{"location": "Remote, Europe", "count": 18}, {"location": "Berlin", "count": 14}, {"location": "London", "count": 10}],
  "rule_matches": [{"name": "data", "count": 4, "examples": [{"title": "Senior Data Engineer", "job_url": "https://jobs.example.com/examplecorp/101"}]}],
  "new_role_examples": [{"title": "Senior Data Engineer", "job_url": "https://jobs.example.com/examplecorp/101"}],
  "closed_role_examples": [{"title": "Office Manager", "job_url": null}],
  "source_url": "https://api.example.com/v1/boards/examplecorp/jobs",
  "fetched_at": "2026-10-07T18:00:00Z",
  "status": "ok",
  "error": null,
  "error_detail": null
}
```

Sample record (REAL: the board `ashby:linear` of the prefilled input, read 2026-10-08T01:55:11Z on a first run with no memory, so the new and closed fields are null):

```json
{
  "id": "ashby:linear",
  "platform": "ashby",
  "company_slug": "linear",
  "open_roles": 31,
  "new_roles_since_last_run": null,
  "closed_roles_since_last_run": null,
  "first_run": true,
  "by_department": {"GTM": 16, "Operations": 2, "Product": 13},
  "by_location": [{"count": 16, "location": "North America"}, {"count": 8, "location": "Europe"}, {"count": 3, "location": "North America; Europe"}, {"count": 2, "location": "London"}, {"count": 2, "location": "Singapore"}],
  "rule_matches": [{"count": 12, "examples": [{"job_url": "https://jobs.ashbyhq.com/linear/d3bc1ced-3ce4-4086-a050-555055dbb1ff", "title": "Senior / Staff Fullstack Engineer"}, {"job_url": "https://jobs.ashbyhq.com/linear/cd5ae036-0223-427a-b038-ba16ef9dcb32", "title": "Senior / Staff Fullstack Engineer"}, {"job_url": "https://jobs.ashbyhq.com/linear/069c4628-88d7-4e4d-b393-c996fc7f3076", "title": "Senior / Staff Product Engineer"}], "name": "engineering"}, {"count": 3, "examples": [{"job_url": "https://jobs.ashbyhq.com/linear/c21af93e-210f-4969-8eaa-90fb16a5b720", "title": "Account Executive, Startups"}, {"job_url": "https://jobs.ashbyhq.com/linear/cb8e77a6-4316-45c6-ac8d-7ade980cd65e", "title": "Account Executive, Growth"}, {"job_url": "https://jobs.ashbyhq.com/linear/6dd5500a-4bd4-4f57-ba09-7b07acefecf4", "title": "Account Executive, Enterprise"}], "name": "sales"}],
  "new_role_examples": null,
  "closed_role_examples": null,
  "source_url": "https://api.ashbyhq.com/posting-api/job-board/linear?includeCompensation=true",
  "fetched_at": "2026-10-08T01:55:11Z",
  "status": "ok",
  "error": null,
  "error_detail": null
}
```

Field rules: on the first run for a board, `first_run` is true and `new_roles_since_last_run` and `closed_roles_since_last_run` are empty (null), not zero, because there is nothing to compare with. `by_department` uses the department a board gives, or the team when the board has no departments, or `unknown`. `by_location` lists the five most frequent location texts. Rule examples are real open roles of that board, up to three. Closed roles keep only the title the Actor remembered.

### What a run costs

The price is $4.00 per 1,000 company reports, plus the platform's start fee of $0.00005 per run (charged once, because the Actor runs with at most 1 GB of memory). A run over 100 boards costs $0.40005. Boards that fail are not charged.

### Limits and what it will not do

- Three platforms in this version: Greenhouse, Lever, Ashby. Other addresses give an `unsupported_board` record.
- SmartRecruiters boards are not read. The robots.txt of its API host (`api.smartrecruiters.com`, read 2026-10-08) allows one other agent and disallows all others, so a SmartRecruiters address gives one `blocked` record with `error` set to `robots_disallow`, not charged.
- The comparison is per board: adding a board to the list does not reset the others, and a board seen for the first time starts with empty new and closed counts. Two schedules that read the same board share one comparison point, so each sees the changes since whichever of them ran last. Turning `deltaMode` off neither reads nor writes the memory.
- The memory keeps, per open role, its id, title, department and the day it was first seen, in the named key-value store `hiring-signals-state` of your account. No posting text is kept.
- Up to 100 pages per board; at most 2 requests per second per platform; GET requests only.
- Each board has a time budget of 300 seconds, its robots.txt included. A board that is not read in full within it gives one `error` record with `error` set to `time_budget_exceeded`: what was read is dropped, not charged and not remembered, and the next board starts with its own budget.
- It reads public job board endpoints only: no login, no cookies, no proxies, no browser. A block is reported as `blocked`, never worked around.
- It identifies itself as `edge-hiring-signals/0.2` in every request. A site owner can block it in robots.txt with `User-agent: edge-hiring-signals` and `Disallow: /`. It never pretends to be a browser.
- It reports on roles, never on people.

### Source and terms

- Greenhouse Job Board API: https://docs.greenhouse.io/job-board.html ("authentication is not required for any GET endpoints").
- Lever Postings API: https://github.com/lever/postings-api
- Ashby public job posting API: https://developers.ashbyhq.com/docs/public-job-posting-api
- This Actor is not affiliated with Greenhouse, Lever, Ashby or SmartRecruiters. The postings belong to the companies that publish them.

### Questions and answers

**Why are new and closed empty on my first run?** There is no earlier run to compare with. From the second run on they are numbers, zero included.

**What counts as a new role?** A role id the board did not list at the last run that read it. A role that is taken down and posted again under a new id counts as closed and new.

**Can I count roles in one department only?** Yes: add a rule with `departments` and no keywords, or both to narrow it.

**How do I reset the comparison?** Delete the key-value store `hiring-signals-state` in your account, or the record of one board in it.

**Does it read the job descriptions?** No. Titles, departments, teams, locations and links are enough for the counts.

# Changelog

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

# Actor input Schema

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

Company job boards to track, one item each: a Greenhouse, Lever or Ashby board address such as https://boards.greenhouse.io/<board>, https://jobs.lever.co/<board> or https://jobs.ashbyhq.com/<board>, or an object {"platform": "lever", "slug": "<board>"}. Up to 500 boards per run.

## `rules` (type: `array`):

Each rule counts the open roles it matches and shows up to three of them: {"name": "sales", "titleKeywords": \["account executive"], "departments": \["Sales"]}. A role matches when its title contains one of the keywords as a whole word (any case) and, when departments are given, its department or team is one of them.

## `deltaMode` (type: `boolean`):

Counts the roles opened and closed since the last run that read the same board, with up to five examples of new job postings and closed roles. The open role ids, titles, departments and first-seen dates are kept per board in the named key-value store hiring-signals-state of your account; no posting text is kept.

## Actor input object example

```json
{
  "boards": [
    "https://boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/linear"
  ],
  "rules": [
    {
      "name": "engineering",
      "titleKeywords": [
        "engineer",
        "developer"
      ]
    },
    {
      "name": "sales",
      "titleKeywords": [
        "account executive",
        "sales"
      ]
    }
  ],
  "deltaMode": true
}
```

# Actor output Schema

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

The dataset: one report per company job board with the count of open roles, open roles by department and location, roles opened and closed since the last run, and the role rule matches. Only rows with status ok are charged.

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

The RUN_SUMMARY record of the default key-value store: rows by status, errors by type, charged events, requests per host and seconds per board.

# 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 = {
    "boards": [
        "https://boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/linear"
    ],
    "rules": [
        {
            "name": "engineering",
            "titleKeywords": [
                "engineer",
                "developer"
            ]
        },
        {
            "name": "sales",
            "titleKeywords": [
                "account executive",
                "sales"
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("edgef-4/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 = {
    "boards": [
        "https://boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/linear",
    ],
    "rules": [
        {
            "name": "engineering",
            "titleKeywords": [
                "engineer",
                "developer",
            ],
        },
        {
            "name": "sales",
            "titleKeywords": [
                "account executive",
                "sales",
            ],
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("edgef-4/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 '{
  "boards": [
    "https://boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/linear"
  ],
  "rules": [
    {
      "name": "engineering",
      "titleKeywords": [
        "engineer",
        "developer"
      ]
    },
    {
      "name": "sales",
      "titleKeywords": [
        "account executive",
        "sales"
      ]
    }
  ]
}' |
apify call edgef-4/hiring-signals --silent --output-dataset

```

## MCP server setup

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