# Company Hiring Signals – Greenhouse, Lever & Ashby Monitor (`furkansmbl/company-hiring-signals`) Actor

Who is hiring at your target accounts? Finds each company's Greenhouse, Lever, Ashby, Workable, Recruitee, Personio or Workday job board; tracks new roles.

- **URL**: https://apify.com/furkansmbl/company-hiring-signals.md
- **Developed by:** [Furkan Sümbül](https://apify.com/furkansmbl) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

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 – Greenhouse, Lever, Ashby & more jobs monitor

**For B2B sales and RevOps teams, recruiting agencies and investors who need to know which target accounts are hiring, for which roles, and what changed since last week.** Paste a list of company domains. The Actor finds each company's public job board on Greenhouse, Lever, Ashby, Workable, Recruitee, Personio or Workday. You get **one signal row per company**: open roles, new and closed roles since the last run, a breakdown by function, seniority and country, the remote share, `sales_hiring` / `eng_hiring` flags and a `hiring_surge` flag. You can also get one row per job, with salary when the company publishes it.

Run it weekly with a `monitorId` and it becomes a hiring-intent feed for Clay, HubSpot, Slack or n8n.

***

### Why this Actor

- **You pay per company, not per job row.** Most job Actors sell raw postings. This one also compares each run with the last and sums it up per company (an illustrative row might read "Acme: 120 open roles, +6 new, 2 closed, 41 in sales"). If you only need the signal, you never pay for the hundreds of individual postings behind it.
- **Finds the job board for you.** Give it `stripe.com` and it works out that Stripe uses Greenhouse. It first guesses the board name across 6 ATS APIs, then checks the company's own `/careers` page, which is also where it finds Workday career sites. Each guessed board is validated before use: the company domain must appear in the postings, or the company name must match. Empty, abandoned and same-name boards are rejected (for example, there is a "notion" Workable account that belongs to a London PR agency).
- **Built for schedules.** With a `monitorId`, the board each company resolved to and its last snapshot are stored between runs. Repeat runs make one API call per company, and `onlyChanges` bills only new, closed and changed jobs.
- **Official public job-board APIs only.** These are the unauthenticated endpoints the ATS vendors provide so companies can list their jobs on other sites. No proxies, no logins, no LinkedIn. It respects robots.txt (including each Workday tenant's) and Lever's crawl delay.
- **Clean, normalised fields** across all 7 ATS: function, seniority, ISO country codes, remote/hybrid/on-site, employment type, salary range, posting date and apply URL.

### Supported job boards

| ATS | Board URL example | What we read |
|---|---|---|
| Greenhouse | `job-boards.greenhouse.io/stripe` | all open jobs, departments, offices, pay range from the posting text |
| Lever | `jobs.lever.co/palantir` | postings, team, commitment, workplace type, salary range (rate-limited to 1 request/second as Lever asks) |
| Ashby | `jobs.ashbyhq.com/ramp` | jobs, structured compensation, workplace type, secondary locations |
| Workable | `apply.workable.com/huggingface` | jobs, locations, remote flag |
| Recruitee | `bunq.recruitee.com` | offers, salary, remote/hybrid flags |
| Personio | `1komma5grad.jobs.personio.de` | XML feed: positions, offices, seniority, descriptions |
| Workday | `nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | the career site's own search API. It reads the newest postings, 20 per request and at most 1 request per second per tenant. The board size comes from Workday's own counts. The tenant's robots.txt is checked first. Posting dates are approximate. Descriptions cost one extra request per job. |

SmartRecruiters is **deliberately not supported**: its API host's robots.txt disallows automated access. iCIMS, Taleo, SuccessFactors and fully custom careers sites are **not** supported yet. Those companies get a row with `error: "no supported ATS found"`, and you are not charged for them.

### Input

```json
{
  "companies": [
    "stripe.com",
    "ramp.com",
    "Notion | notion.so",
    "https://jobs.lever.co/palantir",
    "Hugging Face"
  ],
  "mode": "signals",
  "monitorId": "weekly-icp-accounts",
  "onlyChanges": false,
  "functions": [],
  "countries": [],
  "titleKeywords": [],
  "remoteOnly": false,
  "includeDescriptions": false,
  "maxJobsPerCompany": 500
}
```

- **companies**: a domain, a company name, an ATS board URL, or `Name | domain`. Domains work best, because they enable validation and the careers-page fallback. Board URLs skip detection.
- **mode**:
  - `signals`: one row per company.
  - `jobs`: one row per job.
  - `both`: signal rows and job rows in the same dataset. Filter on the `type` field to separate them.
- **monitorId**: turns on run-to-run diffs. The first run is a baseline.
- **onlyChanges**: job rows only for new, closed and changed jobs. Signal rows are always emitted.
- **Filters** (`titleKeywords`, `functions`, `countries`, `remoteOnly`): apply to both the signal counts and the job rows. `open_jobs_total` always shows the unfiltered count.
- **maxJobsPerCompany**: the maximum number of job rows per company. For Workday it also sets how many of the newest postings are read. The default is 500, the minimum read is 100, and Workday's own limit is 2,000.
- **Advanced**:
  - `atsHint`: skip probing ATS you know are not used.
  - `surgeThresholdPct`: default 20.
  - `careersPageFallback`: default on.
  - `maxConcurrency`: default 5.

### Output

The dataset has two views: **Company signals** (one row per company, `type: "signal"`) and **Jobs** (one row per job, `type: "job"`). Run totals are saved as the `RUN_SUMMARY` record in the run's key-value store. The run's **Output** tab, and the `output` object of the run in the API, link to all three.

Real output from our v0.1.1 smoke run, trimmed. This is a baseline row (first run of a monitor):

```json
{
  "type": "signal",
  "input": "ramp.com",
  "company_name": "Ramp",
  "domain": "ramp.com",
  "ats": "ashby",
  "board_token": "ramp",
  "board_url": "https://jobs.ashbyhq.com/ramp",
  "open_jobs": 161,
  "open_jobs_total": 161,
  "new_since_last_run": null,
  "closed_since_last_run": null,
  "net_change_pct": null,
  "by_function": { "sales": 60, "engineering": 40, "product": 11, "finance": 10, "people": 10, "marketing": 8, "customer_success": 6, "design": 5, "operations": 5, "data": 4, "legal": 2 },
  "by_seniority": { "mid": 89, "manager": 26, "senior": 17, "lead": 10, "director": 7, "junior": 7, "intern": 5 },
  "by_country": { "US": 143, "CA": 22, "GB": 12 },
  "remote_share": 0.3,
  "sales_hiring": true,
  "eng_hiring": true,
  "hiring_surge": null,
  "newest_job_posted_at": "2026-10-14T22:22:02.947Z",
  "is_baseline": true,
  "resolved_via": "api_probe",
  "checked_at": "2026-10-14T22:53:42.680Z",
  "error": null
}
```

A company on an unsupported ATS gets an uncharged row:

```json
{
  "input": "https://careers.smartrecruiters.com/DeliveryHero",
  "ats": null,
  "open_jobs": null,
  "error": "SmartRecruiters is not supported (their robots.txt disallows automated access): smartrecruiters:deliveryhero"
}
```

On later runs of the same monitor, the diff fields are filled in. The trimmed output below comes from a test run. To simulate a week of changes, we removed a few jobs from the stored snapshot and edited one title, so these new/closed counts are synthetic:

```json
{
  "company_name": "Stripe",
  "ats": "greenhouse",
  "open_jobs": 726,
  "previous_open_jobs": 725,
  "new_since_last_run": 3,
  "closed_since_last_run": 2,
  "changed_since_last_run": 1,
  "net_change_pct": 0.1,
  "new_by_function": { "data": 3 },
  "top_new_titles": ["Data Scientist", "Data Analyst"],
  "sales_hiring": true,
  "hiring_surge": false,
  "is_baseline": false,
  "resolved_via": "cache",
  "previous_checked_at": "2026-10-14T22:31:39.064Z"
}
```

A job row (`mode: "jobs"` or `"both"`):

```json
{
  "type": "job",
  "company_name": "Ramp",
  "ats": "ashby",
  "job_id": "331dbcfd-3faf-4357-a7d8-3acd854b4b42",
  "title": "Physical Security Lead",
  "function": "operations",
  "seniority": "lead",
  "department": "Security",
  "locations": ["New York, NY (HQ)"],
  "country": "US",
  "workplace_type": "hybrid",
  "is_remote": false,
  "employment_type": "full_time",
  "salary_min": 134400,
  "salary_max": 215000,
  "salary_currency": "USD",
  "salary_period": "year",
  "posted_at": "2026-10-14T22:22:02.947Z",
  "apply_url": "https://jobs.ashbyhq.com/ramp/331dbcfd-3faf-4357-a7d8-3acd854b4b42",
  "status": "new",
  "changed_fields": [],
  "first_seen_at": "2026-10-14T22:52:52.934Z",
  "closed_at": null
}
```

A Workday company (real output, trimmed). Only the newest 100 postings were read (`maxJobsPerCompany: 100`), so `partial_read` is true: the breakdowns cover `jobs_analyzed`, while `open_jobs` is Workday's own count:

```json
{
  "company_name": "Salesforce",
  "ats": "workday",
  "board_url": "https://salesforce.wd12.myworkdayjobs.com/External_Career_Site",
  "open_jobs": 1545,
  "open_jobs_total": 1545,
  "jobs_analyzed": 100,
  "partial_read": true,
  "by_function": { "sales": 40, "engineering": 26, "customer_success": 7, "operations": 7, "data": 6, "other": 4, "finance": 3, "product": 3, "marketing": 2, "design": 1, "people": 1 },
  "by_country": { "US": 32, "IE": 14, "MX": 12, "JP": 9, "GB": 6, "CA": 5, "IN": 4 },
  "remote_share": 0.1,
  "resolved_via": "known_board"
}
```

**Signal fields:**

- `open_jobs`: open roles after filters.
- `jobs_analyzed` / `partial_read`: how many postings were read and classified. `partial_read` is true only for large Workday sites read up to the cap. In that case, without filters, `open_jobs` and `open_jobs_total` are Workday's own counts, and the breakdowns cover the newest `jobs_analyzed` postings.
- `new_since_last_run` / `closed_since_last_run` / `changed_since_last_run`: `null` on the baseline run.
- `net_change_pct`: open roles compared with the previous run.
- `by_function`: counts for engineering, sales, marketing, product, design, data, operations, finance, people, legal, customer_success and other.
- `by_seniority`: counts for intern, junior, mid, senior, lead, manager, director, vp and executive.
- `by_country`: ISO-2 codes. A job listed in two countries counts in both. `unknown` means the location could not be resolved.
- `remote_share`: share of open roles that are remote or list a remote location.
- `top_new_titles`: up to 10 titles.
- `sales_hiring` / `eng_hiring`: at least one open sales or engineering role.
- `hiring_surge`: open roles grew by at least `surgeThresholdPct` and by at least 3 roles, or went from 0 to 5 or more.
- `resolved_via`: how the board was found. One of `board_url`, `cache`, `api_probe`, `careers_page` or `known_board`.

**Job fields:** `posted_at_estimated` is true when the date comes from a Workday label such as "Posted 3 Days Ago" ("30+ Days Ago" becomes 30 days). With `includeDescriptions`, Workday rows get the exact posting date from the job detail.

**Job statuses:** `new` (first seen in this run), `existing` (with `changed_fields` when the title, locations, department, workplace type or salary changed) and `closed` (no longer on the board, with `closed_at`). `first_seen_at` is kept across runs.

The default key-value store also gets a `RUN_SUMMARY` record with counts for the run.

### Pricing (pay per event)

| Event | Price | When it is charged |
|---|---|---|
| `company-checked` | **$0.004** | Once per company whose job board was found and read. Includes detection and the signal row. |
| `job-row` | **$0.0008** | Each job row in the dataset. With `onlyChanges`, only new, closed and changed jobs. |
| `job-description` | **$0.0007** | Add-on per job row that carries `description_text` (only when `includeDescriptions` is on). |
| `apify-actor-start` | Apify default | Once per run. |

**Not charged:**

- companies where no supported ATS was found, including companies on SmartRecruiters;
- invalid entries;
- API errors;
- anything after your run's spending limit is reached. The Actor stops cleanly and the remaining companies are re-checked on the next run.

**Worked monthly example.** You monitor 500 target accounts every Monday, which is about 4.3 runs a month.

| Setup | Cost per month |
|---|---|
| Signals only | 500 × 4.3 × $0.004 = **$8.60** |
| Signals plus `mode: "both"` with `onlyChanges`, assuming ~6 changed jobs per account per week | + 500 × 6 × 4.3 × $0.0008 = $10.32, total **≈ $19** |
| One-off export of every open job at 50 companies averaging 150 roles | 50 × $0.004 + 7,500 × $0.0008 = **$6.20** |

### Run it on a schedule

1. Create a **Task** from this Actor with your company list. Set `monitorId` (for example `icp-weekly`) and `mode: "signals"`, or `"both"` with `onlyChanges: true`.
2. Run it once. This is the baseline.
3. In **Schedules**, add the task with a weekly cron such as `0 7 * * 1` (Monday 07:00).
4. Add an integration or webhook on "Run succeeded" (see below) to push the rows where `hiring_surge` or `sales_hiring` is true.

Use one `monitorId` per list. Changing filters between runs is safe, because the snapshot always covers all jobs.

### Sales recipes

**Clay table enrichment.**

1. In Clay, add an *Apify* enrichment ("Run Actor") on a table with a `Domain` column.
2. Pass `{"companies": ["{{Domain}}"], "mode": "signals"}` and map the returned `open_jobs`, `by_function.sales`, `sales_hiring`, `hiring_surge`, `top_new_titles` and `board_url` into columns.
3. Use the columns to score and route accounts. For example: "hiring 3+ AEs, or added 5+ roles this month".

For large tables, run the Actor once over the whole list via a Task and import the dataset into Clay instead of one run per row. It is faster, and you still pay $0.004 per company.

**Weekly HubSpot + Slack alert via n8n.**

1. **Schedule Trigger**, Monday 07:30.
2. **Apify node**: "Run task and get dataset items" for your monitoring task.
3. **IF node**: `{{$json.hiring_surge}} === true || {{$json.new_by_function?.sales}} >= 2`.
4. **HubSpot node**: "Company → Update", matched by `domain`. Write `open_jobs`, `sales_hiring`, `net_change_pct` and `checked_at` into custom properties, and optionally create a task for the account owner.
5. **Slack node**: post `{{company_name}}: +{{new_since_last_run}} roles ({{net_change_pct}}%), new: {{top_new_titles.join(", ")}}`.

**Make / Zapier.** Use the Apify "Watch Actor runs" / "Actor run finished" trigger, then "Get dataset items". Filter on `hiring_surge = true` and send to Slack, email or your CRM.

**Recruiting agencies.** Set `mode: "jobs"`, `onlyChanges: true`, `functions: ["engineering"]` and `countries: ["DE", "NL"]`. You get a daily feed of new engineering roles at your client list, with apply URLs.

**Investors and analysts.** Track a portfolio or watchlist weekly. Sort by `net_change_pct` and watch `by_function` shift from engineering to sales as companies move to go-to-market.

### Integrations and API

- **Apify API**: `POST https://api.apify.com/v2/acts/furkansmbl~company-hiring-signals/run-sync-get-dataset-items?token=…` with the input JSON returns the rows directly, which suits small lists.
- **Webhooks**: trigger on `ACTOR.RUN.SUCCEEDED` and fetch the dataset.
- **Clay, n8n, Make, Zapier, HubSpot** (via n8n/Make/Zapier), **Google Sheets** and **Slack**: all through Apify's official integrations.

### How detection works

1. **Board URL given.** It is used directly.
2. **Monitor cache.** The board found in an earlier run is reused (one API call). If that board disappears or empties, detection runs again in case the company switched ATS.
3. **Token guessing.**
   - Tokens tried: the domain stem, the stem without hyphens, the stem plus "hq", the stem without "hq", and the company name without spaces.
   - Greenhouse, Ashby, Recruitee and Personio are probed in parallel. Lever and Workable have strict rate limits, so they are probed only when the fast APIs found nothing conclusive.
4. **Careers-page fallback.**
   - The homepage, `/careers` and `/jobs` are read, plus same-site careers pages linked from them. Dedicated careers hosts such as `jobs.<domain>` and `careers.<domain>` are read first. Up to 6 pages per company, and robots.txt is respected.
   - These pages are searched for embed links such as `boards.greenhouse.io/<token>`, `jobs.ashbyhq.com/<token>`, `<tenant>.wd5.myworkdayjobs.com/<site>` or `wd3.myworkdaysite.com/recruiting/<tenant>/<site>`.
   - **Workday is never guessed.** Tenant and site names can't be derived from a domain, so a Workday site is used only when:
     - you pass its URL;
     - the company's careers pages link to it; or
     - the company is on a small built-in list of tenants we verified live: NVIDIA, Salesforce, Adobe, Intel, Cisco and Workday.
   - Before any request to a Workday tenant, its robots.txt is checked for both the career-site path and the `/wday/cxs/` API path. Disallowed sites (for example NVIDIA's research and talent-community sites) get an uncharged row explaining why.
5. **Validation.**
   - A guessed board is used only if:
     - the company domain appears in its postings or apply URLs; or
     - the ATS-declared company name matches; or
     - most postings mention the company name.
   - Boards with zero jobs, or whose newest posting is over 18 months old, are rejected.
   - A board linked from the company's own site is trusted.

**How well does it work?** We re-tested 44 well-known US and European tech companies with v0.2.0 in October 2026. 32 resolved automatically to the right board, including Stripe, Figma, Notion, Ramp, Datadog, Anthropic, OpenAI, Hugging Face, bunq, Palantir, Spotify, Mistral and Taxfix (found through its careers page). Three of the 12 misses (Delivery Hero, Canva, Wise) use SmartRecruiters, which we deliberately don't read. The other misses (for example Revolut, HubSpot, Zalando, Shopify and Klarna) have one of three causes: an ATS we don't support, a careers site that loads its board only with JavaScript, or a board name that cannot be guessed. For example, Trade Republic's Greenhouse board is `traderepublicbank`. In those cases, pass the board URL. Workday support was verified live on NVIDIA (2,714 open roles), Salesforce (1,545) and Intel (708). We also checked that a Workday site disallowed by robots.txt (NVIDIA's research site) is skipped without any API call.

### Limits and honest FAQ

- **Coverage.** Seven ATS: Greenhouse, Lever, Ashby, Workable, Recruitee, Personio and Workday. iCIMS, Taleo and SuccessFactors are not covered yet.
- **Workday specifics.**
  - Workday sites are found from a URL, a careers-page link or the small verified list. They are never guessed.
  - On big sites we read the newest postings up to `maxJobsPerCompany` (Workday stops at 2,000). Whenever a read stops early, `partial_read` is true, and:
    - jobs that leave the read window can't be told apart from closed jobs, so `closed_since_last_run` is `null` and no closed rows are emitted;
    - new roles and the net change in open roles are still reported.
  - Posting dates are approximate unless descriptions are on.
  - Salary is not read from Workday.
  - A Workday site whose robots.txt disallows the paths we need is skipped with an uncharged row.
- **Why not SmartRecruiters?** Its API host (`api.smartrecruiters.com`) has a robots.txt that disallows all automated clients except LinkedIn's bot. We respect that. Companies on SmartRecruiters, including SmartRecruiters board URLs you pass in, come back as `SmartRecruiters is not supported (their robots.txt disallows automated access)`. These rows are never charged and make no request to SmartRecruiters.
- **Can it pick the wrong board?** Rarely. A same-name account on another ATS can slip through validation when the real company is not on any supported ATS and the impostor's postings mention the name. Pass the board URL (or `Name | domain`) for accounts that matter.
- **Function and seniority are rule-based** from the job title (English plus common German and Dutch terms), with the department and ATS category as fallbacks. Unusual titles and non-Latin-script titles may land in `other`, or fall back to the ATS category.
- **Countries** come from ATS country codes when present, otherwise from the location text (cities, states, countries). Unresolvable places show as `unknown`.
- **Salary** appears only when the ATS publishes it (Ashby, Lever, Recruitee) or the posting contains a clear range with a currency (common on US Greenhouse boards).
- **The first run is a baseline.** Diffs start from the second run of the same `monitorId`. In `onlyChanges` mode the baseline run emits no job rows.
- **Companies with no supported ATS** are re-checked after 6 days inside a monitor, so a weekly schedule always re-checks them. If an ATS API was unreachable or rate-limited during detection, the company gets an error row (not charged) and is retried on the next run.
- **Speed.** Known boards take about one second each. A Workday site takes about 1 second per 20 postings read, so about 25 seconds at the default 500. With descriptions, add 1 second per Workday job row. Detection for unknown companies takes 1–10 seconds, longer when Lever, Workable or a careers page must be checked. Workable rate-limits bursts hard (about 10 requests a minute per IP in our tests), so Workable lookups are paced at one every 3 seconds. A run over hundreds of new companies can therefore take several minutes; later runs reuse the cached board.
- **What access does it need to my Apify account?** Only Apify's *limited permissions*. It writes to the run's own dataset and key-value store and, with a `monitorId`, to the named key-value store `company-hiring-signals-<monitorId>` that it creates in your account. It cannot read your other Actors, runs or storages.
- **Personal data.** Rows contain company and job data only. Descriptions are stripped of emails, phone numbers, "your contact: …" lines and recruiter tags. Recruiter fields from the APIs are never output.
- **Terms of use.** The Actor reads public job-board endpoints that the ATS vendors publish for syndicating job ads. Job ads are the employer's content: use them to research and contact companies, not to republish the job descriptions as your own job board.

### Changelog

Release notes are on this Actor's **Changelog** tab.

# Changelog

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

# Actor input Schema

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

One company per line: a domain (<code>stripe.com</code>), a company name (<code>Hugging Face</code>), an ATS board URL (<code>https://jobs.lever.co/palantir</code>, <code>https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite</code>; Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Workday), or <code>Name | domain</code> (<code>Notion | notion.so</code>). Domains work best: they enable validation and the careers-page fallback. Up to 5,000 per run.

## `mode` (type: `string`):

<b>signals</b>: one row per company (open roles, new/closed since last run, by function/country, sales & engineering hiring, surge flag). <b>jobs</b>: one row per job. <b>both</b>: signal rows and job rows in the same dataset (use the <code>type</code> field to tell them apart).

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

Any name, e.g. <code>weekly-icp-accounts</code>. Runs with the same Monitor ID remember the last snapshot, so you get <code>new_since_last_run</code>, <code>closed_since_last_run</code> and job status <code>new / existing / closed</code>. The first run is the baseline. Leave empty for a one-off snapshot.

## `onlyChanges` (type: `boolean`):

With a Monitor ID, emit (and pay for) only new, closed and changed jobs. Signal rows are always emitted. On the baseline run no job rows are emitted.

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

Keep only jobs whose title contains any of these (case-insensitive), e.g. <code>account executive</code>, <code>SDR</code>. Applies to signal counts and job rows.

## `functions` (type: `array`):

Keep only these functions (classified from the job title).

## `countries` (type: `array`):

Keep only jobs located in these countries, as ISO-2 codes such as <code>US</code>, <code>GB</code>, <code>DE</code>.

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

Keep only jobs that are remote or list a remote location.

## `includeDescriptions` (type: `boolean`):

Add <code>description_text</code> (plain text; emails, phone numbers and named contacts removed) to job rows. Billed as an add-on per job row.

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

Cap on job rows per company and run (new jobs first, then changed, closed, existing). Signal counts cover all open jobs, except on large Workday sites: there this is also how many of the newest postings are read (minimum 100, Workday allows at most 2,000), and the signal row says partial_read = true.

## `atsHint` (type: `string`):

If all companies use the same ATS, pick it to skip probing the others. Supported: Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Workday (Workday is found via board URLs and careers pages only, never guessed). SmartRecruiters is not supported because its robots.txt disallows automated access.

## `surgeThresholdPct` (type: `integer`):

<code>hiring_surge</code> is true when open roles grew by at least this percentage since the last run (and by at least 3 roles).

## `careersPageFallback` (type: `boolean`):

When guessing the board token fails, read the company's homepage, /careers and /jobs (respecting robots.txt) to find the ATS embed link (this is also how Workday career sites are found).

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

How many companies are checked at the same time. Per-host rate limits (e.g. Lever at 1 request/second) always apply.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "notion.so",
    "ramp.com",
    "https://jobs.lever.co/palantir"
  ],
  "mode": "signals",
  "monitorId": "my-target-accounts",
  "onlyChanges": false,
  "remoteOnly": false,
  "includeDescriptions": false,
  "maxJobsPerCompany": 500,
  "atsHint": "auto",
  "surgeThresholdPct": 20,
  "careersPageFallback": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

## `signals` (type: `string`):

Default dataset in the Company signals view: one row per company with open roles, new and closed roles since the last run, breakdowns by function, seniority and country, and the sales_hiring, eng_hiring and hiring_surge flags.

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

Default dataset in the Jobs view: one row per job posting (mode jobs or both) with function, seniority, country, salary and status new, existing or closed.

## `runSummary` (type: `string`):

RUN_SUMMARY record in the default key-value store: counts for the run.

# 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.com",
        "notion.so",
        "ramp.com",
        "https://jobs.lever.co/palantir"
    ],
    "monitorId": "my-target-accounts"
};

// Run the Actor and wait for it to finish
const run = await client.actor("furkansmbl/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.com",
        "notion.so",
        "ramp.com",
        "https://jobs.lever.co/palantir",
    ],
    "monitorId": "my-target-accounts",
}

# Run the Actor and wait for it to finish
run = client.actor("furkansmbl/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.com",
    "notion.so",
    "ramp.com",
    "https://jobs.lever.co/palantir"
  ],
  "monitorId": "my-target-accounts"
}' |
apify call furkansmbl/company-hiring-signals --silent --output-dataset

```

## MCP server setup

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