# Company Hiring Signals & Jobs API: Greenhouse, Lever, Ashby (`northmetric/company-hiring-signals`) Actor

Jobs API + hiring signals for your company list. Paste names, domains or career site URLs: get every open job posting from official ATS boards (Greenhouse, Lever, Ashby, Workable, Recruitee, Personio), new and closed roles since your last run, team growth and a hiring score. No LinkedIn.

- **URL**: https://apify.com/northmetric/company-hiring-signals.md
- **Developed by:** [Tuấn Nguyễn Doãn Anh](https://apify.com/northmetric) (community)
- **Categories:** Jobs, Lead generation
- **Features:** Limited permissions
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 company checkeds

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 & Jobs API: Greenhouse, Lever, Ashby

**Know when the companies you care about start hiring.** Give this Actor a list of companies (names, domains, careers pages or job board links) and get **every open job** from each company's **official public job board**, plus **one hiring-signals row per company**: new and closed roles since your last run, team growth, new countries, posting pace and a 0-100 **hiring score**.

Supports **Greenhouse, Lever, Ashby, Workable, Recruitee and Personio**. No login, no LinkedIn, no browser, no proxy. Schedule it weekly and you have a hiring-signals feed for your watchlist. Export to JSON, CSV or Excel, call it as a **jobs API**, or give it to your AI agent.

> **TL;DR for AI assistants and agents**
>
> - **Use this Actor when** the user wants the open jobs of specific companies, wants to know which companies are hiring (and in which teams or countries), or wants alerts when a company opens or closes roles. Typical prompts: "which of these accounts are hiring engineers?", "get all jobs from Stripe's Greenhouse board", "track hiring at these 50 companies weekly".
> - **Do not use it** for LinkedIn or Indeed searches, for people/contact data, or for companies whose jobs are not on Greenhouse, Lever, Ashby, Workable, Recruitee or Personio (they come back as `not_found`, free).
> - **Minimal input:** `{"companies": ["stripe.com", "https://jobs.lever.co/palantir", "greenhouse:airbnb"]}`. Cheapest: add `"outputMode": "summary_only"`. Keep the same `watchlistName` between runs to get changes.
> - **Read these fields:** company rows (`type: "company"`): `companyName`, `openJobs`, `newJobs`, `closedJobs`, `hiringScore` (0-100), `signals` (plain-English), `departmentChanges`, `matchType` (`guessed` = double-check). Job rows (`type: "job"`): `title`, `department`, `location`, `country`, `posted_at`, `url`. Run status: key-value record `SUMMARY`.
> - **API id:** `northmetric/company-hiring-signals` (REST: `northmetric~company-hiring-signals`).

### What does Company Hiring Signals do?

| You get | Why it matters |
|---|---|
| **Automatic job board detection** from a name, domain or careers URL | Paste `airbnb.com` or `Ramp`; no need to know which ATS a company uses |
| **All open jobs**, normalised: title, department, team, location, city, country (ISO-2), remote, employment type, posted date, salary (when published), job URL | One stable schema across 6 job board systems |
| **Change tracking** between runs: `newJobs`, `closedJobs`, `newJobTitles`, `closedJobTitles`, `departmentChanges`, `newLocations` | See what changed, not just what exists |
| **Hiring signals** in plain English, e.g. *"Engineering: +6 open roles"*, *"Now hiring in a new country: ES"* | Readable in a spreadsheet, CRM or Slack message |
| **Hiring score (0-100)** with a published formula | Rank a watchlist by hiring activity |
| **Posting pace**: `postedLast7d`, `postedLast30d` | Tell a hiring surge from a quiet board |
| **Breakdowns**: open roles per department, location and country | Where the company is investing |
| **Three output modes**: all jobs, summary only, new jobs only | Pay only for the rows you need |
| **No duplicates**: the same board entered twice is checked once; every job appears once | Clean data, no double billing |
| **Run `SUMMARY`** with a clear COMPLETE / PARTIAL status | Automations check one field |

### Why not just use a jobs scraper?

A scraper gives you today's list of jobs. You still have to compare it with last week's list yourself.

| | Typical job scraper | **Company Hiring Signals** |
|---|---|---|
| Input | A search query or board URL | Your list of companies, in any form (name, domain, URL, `ats:slug`) |
| Source | Often aggregators or job sites with anti-bot protection | Official public job board APIs of each company's ATS |
| What changed since last time | Not tracked | New / closed roles, team changes, new countries, per watchlist |
| Per-company view | Only job rows | One summary row per company with signals and a hiring score |
| Same company entered twice | Fetched and billed twice | Detected, checked once, the duplicate is free |
| Companies that cannot be found | Empty rows, sometimes billed | `not_found` with the reason, **free** |
| Spreadsheet use | Nested JSON | `outputFormat: "flat"` |

### Who uses it?

- **B2B sales & SDR teams**: a company hiring 10 data engineers is likely buying data tools. Track target accounts and act on new roles and growing teams.
- **Recruiters & staffing agencies**: see which client and prospect companies open roles in your speciality, the week they open.
- **Investors & analysts**: hiring pace, team mix and new markets as an early signal for portfolio and watchlist companies.
- **Job seekers & communities**: follow a shortlist of employers and get only the new openings.
- **AI agents & data pipelines**: structured hiring data over the API or MCP.

### How to use Company Hiring Signals

1. Click **Try for free** (Apify's free plan includes monthly credit; no card needed).
2. Paste your **companies**, one per line: `airbnb.com`, `Ramp`, `https://jobs.lever.co/palantir` or `greenhouse:stripe`.
3. Optionally add **filters** (title keywords, locations, departments, remote only) and choose an **output mode**.
4. Click **Start**. With 4 companies in parallel, 40 companies take about 3.5 minutes.
5. Open the **Output** tab: the *Hiring signals* view has one row per company, the *Jobs* view lists the jobs. Export to **Excel, CSV or JSON**.

To get a weekly signals feed, save the input as a **task**, give it its own **Watchlist name**, and add a **schedule** (for example, every Monday). The first run saves a baseline; every later run reports what changed.

### How much does it cost?

Pay only for results (pay per event). Prices drop on higher Apify plans:

| Event | Free plan | Bronze | Silver | Gold and above |
|---|---|---|---|---|
| **Company checked** (found and analysed, one hiring-signals row) | $0.01 | $0.008 | $0.006 | $0.005 |
| **Job returned** (one job row in your dataset) | $0.003 | $0.0025 | $0.0018 | $0.0011 |

**Free:** companies that are not found, failed, or skipped (duplicates, budget or timeout). In `summary_only` mode you pay only *Company checked*; in `new_jobs_only` mode you pay only for the new jobs returned.

| Example (Free plan prices) | Cost per run |
|---|---|
| 100 companies weekly, `summary_only` | 100 × $0.01 = **$1.00** (about $4.33 / month) |
| 50 companies, `new_jobs_only`, ~10 new jobs each | $0.50 + 500 × $0.003 = **$2.00** |
| 50 companies with ~100 open jobs each, all jobs | $0.50 + 5,000 × $0.003 = **$15.50** (Gold: $5.75) |
| One large employer with 766 open jobs (Stripe, Nov 2026) | $0.01 + 766 × $0.003 = **$2.31** |

Set **Maximum cost per run** in the run options and the Actor stops cleanly at your budget: companies it cannot afford come back as `skipped` (free). Use title, location or department filters to cut job rows you do not need.

### Input

The only required field is `companies`. Every other option has a default.

```json
{
  "companies": ["airbnb.com", "https://jobs.lever.co/palantir", "ramp", "greenhouse:stripe"],
  "outputMode": "jobs_and_summary",
  "trackChanges": true,
  "watchlistName": "fintech-accounts",
  "titleKeywords": ["engineer", "data"],
  "locations": ["London", "Remote"]
}
```

| Field | Default | What it does |
|---|---|---|
| `companies` | – | One per line: company name, domain, careers page URL, job board URL or `ats:slug` (e.g. `greenhouse:stripe`, `lever:spotify`, `ashby:ramp`) |
| `outputMode` | `jobs_and_summary` | `jobs_and_summary` = all open jobs + one summary row per company; `summary_only` = summary rows only (cheapest); `new_jobs_only` = only jobs new since the last run + summary |
| `trackChanges` | `true` | Compare with the previous run of the same watchlist |
| `watchlistName` | `default` | Changes are tracked per watchlist. Use a different name for each scheduled list (letters, digits, dashes) |
| `titleKeywords` | `[]` | Keep jobs whose title contains any of these words (case-insensitive) |
| `excludeTitleKeywords` | `[]` | Drop jobs whose title contains any of these words (e.g. `intern`) |
| `locations` | `[]` | Keep jobs whose location or country code contains any of these (e.g. `London`, `Spain`, `DE`, `Remote`) |
| `departments` | `[]` | Keep jobs whose department contains any of these |
| `remoteOnly` | `false` | Keep only jobs explicitly marked remote |
| `postedWithinDays` | `0` (no limit) | Keep jobs first published in the last N days; jobs without a publish date are kept |
| `includeDescription` | `false` | Add the full job description as plain text (bigger output) |
| `outputFormat` | `nested` | `flat` = lists and objects joined into text, for Excel / Google Sheets |
| `maxJobsPerCompany` | `2000` | Upper limit of jobs fetched per company (1-10,000) |
| `maxConcurrency` | `4` | Companies checked at the same time (1-10) |
| `proxyConfiguration` | off | Not needed: the job board APIs are public |

### Output

The dataset has two kinds of rows, told apart by `type`: `company` (one per input) and `job`.

#### Company row (hiring signals)

A real row, shortened, from a cloud run in November 2026 (first run of the watchlist, so it saves a baseline):

```json
{
  "type": "company",
  "state": "completed",
  "companyInput": "greenhouse:stripe",
  "companyName": "Stripe",
  "ats": "greenhouse",
  "slug": "stripe",
  "careersUrl": "https://job-boards.greenhouse.io/stripe",
  "openJobs": 766,
  "totalOpenJobsUnfiltered": 766,
  "newJobs": null,
  "closedJobs": null,
  "departments": {"1185 Account Executives (EMEA)": 46, "5112 General University": 29, "1195 Account Executives (APAC)": 21},
  "topLocations": {"Dublin": 61, "Singapore": 48, "London": 35, "Bengaluru": 30},
  "countries": {"US": 258, "IE": 74, "SG": 48, "CA": 47, "IN": 42, "GB": 40},
  "postedLast7d": 74,
  "postedLast30d": 232,
  "hiringScore": 36,
  "signals": [
    "Baseline saved: 766 open roles. Run again (for example weekly) to see what changes.",
    "Hiring surge: 232 roles posted in the last 30 days"
  ],
  "previousRunAt": null,
  "checkedAt": "2026-11-08T09:31:38+00:00"
}
```

From the second run on, `newJobs`, `closedJobs`, `newJobTitles`, `departmentChanges` and `newLocations` are filled, and `signals` reads like *"12 new roles since the last check"*, *"Engineering: +6 open roles"* or *"No changes since the last check (2026-11-08)"*.

| Field | Meaning |
|---|---|
| `state` | `completed` (billed), `not_found` (no supported board found, free), `failed` (free, see `error`), `skipped` (duplicate input, budget or timeout, free) |
| `companyInput`, `companyName`, `ats`, `slug`, `careersUrl` | What you typed and the job board it was matched to |
| `openJobs`, `totalOpenJobsUnfiltered` | Open jobs after your filters / before filters |
| `newJobs`, `closedJobs` | Change since the previous run of the watchlist; `null` on the first run |
| `newJobTitles`, `closedJobTitles` | Up to 10 titles each |
| `departments`, `topLocations`, `countries` | Top 10 counts of open (filtered) jobs |
| `departmentChanges` | Change in open roles per department since the last run, e.g. `{"Engineering": 6, "Sales": -2}` |
| `newLocations` | Locations that had no open role in the previous run |
| `postedLast7d`, `postedLast30d` | Jobs first published in the last 7 / 30 days; `null` when the board publishes no dates |
| `remoteJobs` | Number of jobs explicitly marked remote or hybrid-remote |
| `remoteShare` | Share of remote jobs, only when at least 80 % of the jobs state their remote status (many boards only mark remote jobs and leave the rest blank); otherwise `null` |
| `companyDomain` | Company website domain when you gave a domain or careers URL (use it to match rows to your CRM, e.g. HubSpot or Clay) |
| `matchType` | How the board was found: `input` (you gave `ats:slug`), `url` (job board URL), `careers_page` (linked from the company's own site) or `guessed` (from the name / domain). In a 145-company test about 97 % of guessed matches were the right company; filter on `guessed` to double-check |
| `matchNote` | The details of how the board was found. Pin a company with its careers page URL or `ats:slug` |
| `hiringScore` | 0-100, see *How the metrics are calculated* |
| `signals` | Plain-English list of what is notable |
| `error` | Reason for `not_found`, `failed` or `skipped` |
| `previousRunAt`, `checkedAt` | Time of the compared run and of this check |

#### Job row

A real row from the same run (Ashby board):

```json
{
  "type": "job",
  "companyInput": "ashby:ramp",
  "ats": "ashby",
  "slug": "ramp",
  "job_id": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "city": "New York City",
  "region": "NY",
  "country": "US",
  "remote": true,
  "employment_type": "full-time",
  "posted_at": "2026-04-07T17:12:35Z",
  "salary_min": 211400,
  "salary_max": 290600,
  "salary_currency": "USD",
  "salary_period": "year",
  "isNew": null,
  "firstSeenAt": "2026-11-08T09:31:55+00:00"
}
```

| Field | Meaning |
|---|---|
| `job_id` | The ATS's own id, stable across runs (used for change tracking) |
| `title`, `url` | Job title and the public job page to apply on |
| `company_name`, `department`, `team` | As published (not every board publishes all of them) |
| `location`, `city`, `region`, `country` | Raw location text plus parsed parts; `country` is ISO-3166 alpha-2 when it can be inferred |
| `remote` | `true` when the board says remote; `null` = not stated |
| `employment_type` | `full-time`, `part-time`, `contract`, `internship` or `temporary` when published |
| `posted_at`, `updated_at` | ISO 8601, when the board provides them |
| `salary_min`, `salary_max`, `salary_currency`, `salary_period` | Only when the company publishes a salary range |
| `description_text` | Plain text, only with `includeDescription` |
| `isNew` | `true` if the job was not open in the previous run; `null` on the first run |
| `firstSeenAt` | When this watchlist first saw the job |

Each run also saves a **`SUMMARY`** record in its key-value store: the overall `status` (`COMPLETE`, `PARTIAL`, `VALID_EMPTY`, `UPSTREAM_FAILED`, `INVALID_INPUT`), counts (`completed`, `notFound`, `failed`, `skipped`, `jobsReturned`, `newJobs`), warnings, and the state and reason for every company. Automations can check one field instead of parsing every row.

### Export & integrations

- **Excel / CSV / JSON / XML**: from the Output tab or the API. Use `outputFormat: "flat"` for clean spreadsheet columns, and the *Hiring signals* or *Jobs* view to get only one row type.
- **Google Sheets**: with Apify's Google Sheets integration, every scheduled run updates a sheet.
- **Make, n8n, Zapier**: run the Actor, then send new roles or signals to Slack, your CRM or Airtable.
- **Schedules & webhooks**: run weekly and get notified when the data is ready.

### Use it as a jobs API

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("northmetric/company-hiring-signals").call(run_input={
    "companies": ["airbnb.com", "greenhouse:stripe", "ashby:ramp"],
    "outputMode": "summary_only",
    "watchlistName": "my-accounts",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["companyInput"], row["state"], row.get("openJobs"), row.get("newJobs"), row.get("hiringScore"))
```

Node.js:

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });
const run = await client.actor('northmetric/company-hiring-signals').call({
    companies: ['https://jobs.lever.co/palantir', 'duolingo.com'],
    outputMode: 'new_jobs_only',
    watchlistName: 'weekly-new-roles',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.table(items.filter((r) => r.type === 'job').map(({ company_name, title, location, url }) => ({ company_name, title, location, url })));
```

cURL (starts the run and returns the rows in one request; best for short lists):

```bash
curl -X POST "https://api.apify.com/v2/acts/northmetric~company-hiring-signals/run-sync-get-dataset-items" \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"companies":["greenhouse:stripe","ashby:ramp"],"outputMode":"summary_only","outputFormat":"flat"}'
```

### Use it with AI agents (MCP)

Claude, Cursor, VS Code and other MCP clients can call this Actor as a tool through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp):

```
https://mcp.apify.com?tools=northmetric/company-hiring-signals
```

Example prompts:

- "Which of these 30 accounts are hiring data engineers in Europe right now?"
- "How many open roles does Duolingo have, and in which departments?"
- "List the sales roles Stripe has opened since last week."

The `state`, `signals`, `hiringScore` and `SUMMARY.status` fields are designed to be easy for an agent to read.

### How the metrics are calculated

- **Job board detection**, cheapest first, stopping at the first match: (1) `ats:slug` is checked directly; (2) a job board URL is read as is; (3) for a domain or careers URL, the company's own public pages (homepage, `/careers`, `/jobs`, …) are scanned for links to a supported job board; (4) otherwise likely board names are guessed from the name or domain. For a guess, every supported ATS is checked, empty boards are rejected and the biggest live board is chosen. At most 30 requests per company.
- **New / closed jobs**: the job ids of this run are compared with the previous run of the same `watchlistName`. The stored snapshot always holds **all** open jobs of the company, so changing your filters between runs never creates fake "new" or "closed" jobs.
- **Department changes**: open roles per department now minus in the previous run. A signal is written for changes of ±2 or more (top 3).
- **Hiring surge**: at least 5 roles, and at least 30 % of open roles, first published in the last 30 days.
- **Possible slowdown**: no new postings in 30 days while roles are closing.
- **Hiring score (0-100)**, rounded:

  - **Size**, up to 25: `25 × log10(1 + openJobs) / log10(501)` (500+ open roles = full 25)
  - **Posting pace**, up to 30: `30 × min(1, postedLast30d / max(5, openJobs))`
  - **Any new roles** since the last run: +15
  - **Number of new roles**: `15 × min(1, newJobs / 10)`
  - **Broad growth**, +15 when 2 or more departments grew since the last run

  Example: Stripe, first run, 766 open roles, 232 posted in 30 days: 26.7 + 9.1 = **36**. On the first run of a watchlist the last three parts are 0, so the score is at most 55; the change parts only count from the second run.

### Tips

- **Pin a company to its board** with `ats:slug` (e.g. `greenhouse:stripe`) or the job board URL. This is the fastest and most exact input.
- **Check guessed matches.** When you enter only a name, the Actor may guess the board. Look at `companyName`, `careersUrl` and `openJobs` in the first run; a short or common name can match a different company with the same name. Paste the careers URL or `ats:slug` to pin it.
- **Use one watchlist per scheduled task.** Two tasks with the same `watchlistName` overwrite each other's baseline.
- **Very large employers**: `maxJobsPerCompany` (default 2,000) caps the jobs fetched; one test company hit exactly 2,000. Raise it if you need everything.
- **Run time**: a company with a known job board takes a few seconds; finding the board from a domain or name takes longer. 40 mixed companies took about 210 seconds with 4 in parallel. For long lists, raise `maxConcurrency` or the run timeout. Companies that do not fit in the timeout come back as `skipped` (free) and the `SUMMARY` says so.

### FAQ

**Where does the data come from?**
Only from the official public job board APIs of Greenhouse, Lever, Ashby, Workable, Recruitee and Personio, which companies use to publish their openings, plus the company's own public careers pages to find the board. The Actor does not log in, does not use LinkedIn or job aggregators, and does not bypass any protection.

**Why is a company `not_found`?**
Its jobs are not on one of the 6 supported job board systems, or its job board is built with JavaScript so no board link is visible in the page. Common examples are Workday, iCIMS, SuccessFactors, Taleo and custom career sites. `not_found` companies are **free**, and `error` says what was checked. If you know the company uses a supported ATS, paste the board URL or `ats:slug`.

**How reliable is it?**
In a 40-input test on the Apify cloud (including 6 duplicate inputs and one made-up company), 27 companies were matched to live boards on all supported ATS and returned 7,180 jobs in about 205 seconds. The 6 duplicates were checked once and not billed; the other 7 inputs came back as `not_found` (free) with the reason. An output validator checked every row: no duplicate jobs, no missing required fields, and SUMMARY counts matching the dataset.

**Do I pay for duplicates?**
No. If two inputs lead to the same job board (e.g. `airbnb.com` and `greenhouse:airbnb`), the board is checked once and the second input is `skipped` and free.

**Does it work without a schedule?**
Yes. A single run gives you all open jobs and the per-company breakdown. Change tracking starts from the second run of the same watchlist.

**Is it legal?**
The Actor reads job postings that companies publish so that anyone can see them, through the public APIs meant for that. It collects no applicant data. You are responsible for using the data in line with the sources' terms and the law where you are.

### Support & feedback

Found a problem, a company that should be found, or missing an ATS? Open an issue on the **Issues** tab with the company input you used. If the Actor saves you time, a review helps other users find it.

***

*This Actor reads the public job board APIs that employers publish. It is not affiliated with, endorsed by or sponsored by Greenhouse, Lever, Ashby, Workable, Recruitee or Personio. Job postings belong to the employers; every row links to the original posting.*

# Changelog

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

# Actor input Schema

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

One company per line, in any of these forms:<ul><li>Name: <code>Ramp</code></li><li>Domain: <code>duolingo.com</code></li><li>Careers page or job board URL: <code>https://jobs.lever.co/spotify</code></li><li>ATS and slug: <code>greenhouse:stripe</code>, <code>lever:spotify</code>, <code>ashby:linear</code></li></ul>The Actor finds the official job board automatically. Duplicates (the same board entered twice) are checked once and not billed. Use <b>Bulk edit</b> to paste a list from a spreadsheet.

## `outputMode` (type: `string`):

<b>All open jobs + summary</b>: one row per job and one summary row per company (charged per company and per job).<br><b>Summary only</b>: one hiring-signals row per company, no job rows. Cheapest: 1 charge per company. Best for Clay, CRMs and account lists.<br><b>New jobs only</b>: only jobs that appeared since the last run of this watchlist, plus the summary. Best for weekly alerts. On the first run of a watchlist there is nothing to compare yet, so no job rows are returned.

## `trackChanges` (type: `boolean`):

Compare with the previous run of the same watchlist. Turn off for a one-off export that does not save or read a baseline.

## `watchlistName` (type: `string`):

Changes are tracked per watchlist. Use a different name for each company list you schedule, e.g. <code>fintech-accounts</code> or <code>client-acme</code>. Letters, digits and dashes, up to 40 characters. Upper and lower case count as the same name.

## `titleKeywords` (type: `array`):

Keep jobs whose title contains any of these words, e.g. <code>engineer</code>, <code>sales</code>, <code>data</code>.

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains any of these words, e.g. <code>intern</code>, <code>contractor</code>.

## `locations` (type: `array`):

Keep jobs whose location or country contains any of these: city (<code>London</code>), country (<code>Spain</code>), 2-letter country code (<code>DE</code>) or <code>Remote</code>.

## `departments` (type: `array`):

Keep jobs whose department contains any of these, e.g. <code>Engineering</code>, <code>Sales</code>, <code>Marketing</code>.

## `remoteOnly` (type: `boolean`):

Keep only jobs the job board marks as remote.

## `postedWithinDays` (type: `integer`):

Keep jobs first published in the last N days, e.g. <code>7</code> for a weekly schedule. 0 = no limit. Jobs without a publish date are kept.

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

Add the full job description as plain text to each job row. Same price, but a much bigger dataset (slower to download and to import into Clay or Sheets).

## `outputFormat` (type: `string`):

<b>Nested JSON</b>: lists and objects kept as they are (best for APIs, n8n, code).<br><b>Flat</b>: lists joined into text, one value per column (best for Excel, Google Sheets, Clay).

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

Safety limit for very large job boards. Jobs above the limit are not read, so they are also missing from change tracking: keep it above the company's real number of open jobs.

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

How many companies are checked at the same time. Higher is faster for long lists; lower is gentler on the job boards.

## `proxyConfiguration` (type: `object`):

Not needed: the official job board APIs are public. Leave off unless you have a specific reason.

## Actor input object example

```json
{
  "companies": [
    "duolingo.com",
    "https://jobs.lever.co/spotify",
    "ashby:linear"
  ],
  "outputMode": "jobs_and_summary",
  "trackChanges": true,
  "watchlistName": "default",
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locations": [],
  "departments": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescription": false,
  "outputFormat": "nested",
  "maxJobsPerCompany": 2000,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One row per company: open, new and closed jobs, team changes, hiring score.

## `jobs` (type: `string`):

All open jobs found (filtered).

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

Status, counts and the state of every company.

# 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": [
        "duolingo.com",
        "https://jobs.lever.co/spotify",
        "ashby:linear"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northmetric/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": [
        "duolingo.com",
        "https://jobs.lever.co/spotify",
        "ashby:linear",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("northmetric/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": [
    "duolingo.com",
    "https://jobs.lever.co/spotify",
    "ashby:linear"
  ]
}' |
apify call northmetric/company-hiring-signals --silent --output-dataset

```

## MCP server setup

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