# Company Jobs Monitor: Greenhouse, Lever, Workday, Ashby & More (`dockhand/career-jobs-monitor`) Actor

Get only the jobs a company posted since your last run. Reads the public job feeds of Greenhouse, Lever, Ashby, Workable, Recruitee, Workday, Phenom and Eightfold; SmartRecruiters is closed by its host's robots.txt (measured 2026-09-07). Pay per job row; status rows and quiet days are free.

- **URL**: https://apify.com/dockhand/career-jobs-monitor.md
- **Developed by:** [Dockhand](https://apify.com/dockhand) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 job rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Company Jobs Monitor: Greenhouse, Lever, Workday, Ashby & More

[![Live on Apify Store](https://apify.com/actor-badge?actor=dockhand/career-jobs-monitor)](https://apify.com/dockhand/career-jobs-monitor)

**Watch the companies you care about and get only the jobs they posted since your last check — paid per job row, with every empty result explained for free.**

Give this company jobs monitor a list of companies — prospects, competitors, dream employers — and it reads their careers pages through the public job feeds of eight applicant-tracking systems — Greenhouse, Lever, Ashby, Workable, Recruitee, Workday, Phenom and Eightfold — and recognises a ninth, SmartRecruiters, whose feed its host currently closes. The first run returns their current openings; with monitor mode on, every run after that returns **only the new postings**. Every host it reads — the vendors' API hosts as much as an employer's own careers domain — has its `robots.txt` read and obeyed first, and one of the nine is currently closed by its own host: `api.smartrecruiters.com` publishes a `robots.txt` that disallows every client but LinkedInBot (measured 2026-09-07), so a SmartRecruiters board gets the free `not-found` row naming that rule, and no request, until the host permits it (see *Honest limits*). The quickest way to try it: press **Start** with the prefilled company list and you get a sample of real openings from six boards on six of the nine platforms (Greenhouse, Lever, Ashby, Workday, Phenom, Eightfold) in well under a minute — about 15 seconds on the platform (run vBhYaKgqE2Mwgcx0z, 2026-09-07, 150 rows in 14.8 s; each host costs a `robots.txt` read and about a second's pause before its feed).

### What you get

**Job rows, one per opening.** `company`, `platform`, `job_id`, `title`, `department`, `team`, `location`, `remote`, `employment_type`, `url` (the direct apply link), `published_at`, `updated_at`, plus `description` when you ask for it. An empty cell means the source did not publish that field; nothing is filled in with a guess. Which platform fills which column is listed under *Which job boards it reads*.

**Only-new monitor mode.** Turn on `monitorMode` and the actor remembers, per company list, which jobs it has already given you. The next run delivers only postings it has not delivered before. Memory lives in a key-value store named `career-jobs-monitor-state` on your own Apify account; change the company list and monitoring starts fresh for the new list, or delete that store to reset everything.

**Title filters.** Keep only titles containing your words (`engineer`, `junior`), drop the ones you never want (`senior`, `director`). Filtering happens before anything is charged.

**A free status row for every company with nothing to show.** You never stare at an unexplained empty table. Each of these rows carries `company`, `status` and a plain-English `note`, and none of them is ever charged:

| `status` | What it means |
| --- | --- |
| `not-found` | No supported job board answered for that name; or the URL is on a platform this actor does not cover; or the host did not answer on this run (after three attempts); or the feed answered with an error; or it answered with a bot-verification challenge instead of its job list (a fact about this run's network, named as such — no workaround is attempted); or the host's own `robots.txt` disallows the feed path, or could not be read, so the feed was not asked. The row's note says which, and RUN\_SUMMARY carries the exact answer as `detail` with a one-word `reason`. |
| `no-open-jobs` | The board you named was reached and it lists nothing open right now. |
| `filtered-out` | The board has jobs and none matched your title filters. The note says how many were on it. |
| `no-new-jobs` | Monitor mode: the board was reached and nothing on it is new since your last run. |
| `charge-limit-reached` | This run's maximum cost was reached, or billing itself failed. Jobs held back by the limit were not delivered; if billing itself failed, the rows already written are un-marked instead. Either way they are not marked as seen and arrive next run. |
| `results-truncated` | Paging stopped short of the whole board. The note says whether the SOURCE refused the rest, or this actor's own safety guard or per-board time budget stopped the read, with the numbers behind the claim. |
| `partial` | A request to the source failed twice, so that company's rows are an incomplete picture of its board — or, with descriptions on for a Phenom or Eightfold board, the per-board time budget ran out before every description was fetched, and the note says how many are missing. |

Set `includeNotFoundRows` to false if you want job rows only.

**A RUN\_SUMMARY record** in the run's key-value store, with one entry per company: the platform detected, the verdict (`ok`, `not-found`, `no-open-jobs`, `filtered-out`, `no-new-jobs` or `charge-limit-reached`), `jobs_found` — the board's own total, never the number a cap returned; only where the feed states no total and the read was cut short or lost a page does it fall back to the rows actually read, and then `results_truncated` or `partial` in the same entry says why that is not the board's size — `jobs_fetched` and `jobs_output`, and where they apply `source_pages`, `source_page_max`, `truncation_reason` (with `guard` — `postings` or `time` — when the reason is ours), `partial` and `source_errors`. A `not-found` entry carries `detail` (the exact answer, e.g. `robots.txt disallows /v1/companies/BoschGroup/postings on api.smartrecruiters.com`, or the host, attempt count and error of a host that never answered) and `reason` — one of `absent` (every platform asked answered "no such board"), `walled` (a bot-verification challenge; the entry also carries `walled: true`), `disallowed` (a `robots.txt` rule, so the feed was not asked), `unreachable` (no answer after three attempts, or a `robots.txt` that could not be read) or `error` (an HTTP error or a body that was not the feed). At the top level: `total_jobs_output`, `status_rows`, `sources_ok` (companies whose board was reached, with jobs or without), `sources_failed` (companies with no board), and of those `sources_walled`, `sources_robots_disallowed` and `sources_unreachable`, `all_sources_failed`, `monitorMode`, `charge_limit_reached`, `jobs_withheld_charge_limit`, `results_truncated`, `partial_results`, `charge_failed`, `job_rows_charged` and `pay_per_event`, present on every run.

### Which job boards it reads

Nine applicant-tracking systems, all through the public, logged-out JSON feed each one publishes for its customers' careers pages. No page scraping, no login, no proxies, no browser; every host's `robots.txt` is read and obeyed before its feed is asked, and no host — a vendor's API host or an employer's own domain — is asked more than about once a second.

| Platform | How to name a company | Careers-page URL it recognises |
| --- | --- | --- |
| Greenhouse | bare name, e.g. `stripe` | `boards.greenhouse.io/<company>` or `job-boards.greenhouse.io/<company>` |
| Lever | bare name | `jobs.lever.co/<company>` |
| Ashby | bare name | `jobs.ashbyhq.com/<company>` |
| SmartRecruiters | bare name | `careers.smartrecruiters.com/<Company>` (also `jobs.` or `www.`) — **currently refused by `api.smartrecruiters.com`'s own `robots.txt`**, so every SmartRecruiters board returns the free `not-found` row naming the rule (measured 2026-09-07; see *Honest limits*) |
| Workable | bare name | `apply.workable.com/<company>` |
| Recruitee | bare name | `<company>.recruitee.com` |
| Workday | **URL only** | `<tenant>.wd<N>.myworkdayjobs.com/<site>`, any page under it |
| Phenom | **URL only** (or `phenom:<host>/<country>/<lang>`) | the employer's own careers site, e.g. `careers.lilly.com/us/en/search-results`, `jobs.cvshealth.com/us/en/job/…` — any page under its locale path |
| Eightfold | **URL only** (or `eightfold:<host>/<tenant domain>`) | `<company>.eightfold.ai/careers`, or the employer's own site fronting it, e.g. `careers.micron.com/careers`, `jobs.ericsson.com/careers` |

A bare name is tried against the six auto-detectable platforms in the order above, and the first board with jobs on it wins; a platform whose host's `robots.txt` does not permit the feed (SmartRecruiters, as measured on 2026-09-07) is skipped without a request, and a name found on none of the six says so in its RUN\_SUMMARY entry (`company name not fully probed — 1 of 6 feeds not permitted by the host's robots.txt`) rather than claiming an answer it never got. A URL on a platform's own address (`boards.greenhouse.io`, `*.myworkdayjobs.com`, `*.eightfold.ai`, …) goes straight to its platform. A URL on any other host — an employer's own careers domain — is **asked** whether it is a Phenom or an Eightfold site: that host's `robots.txt` is read first (one request, once per host per run), then one request to each of the two feeds it permits, in the order the path suggests (`/us/en/…` is Phenom's shape, `/careers` is Eightfold's). A host on neither gets the free `not-found` row with both answers in RUN\_SUMMARY; a host whose `robots.txt` disallows a feed path, or whose `robots.txt` cannot be read, is not asked that question at all, and its RUN\_SUMMARY entry says so. If a probe misses a site you know is there, name the platform yourself with the `phenom:` or `eightfold:` prefix — for Eightfold the tenant domain is derived from the host (`careers.micron.com` → `micron.com`, `vodafone.eightfold.ai` → `vodafone.com`), and `eightfold:careers.example.com/example-corp.com` overrides that guess.

**Workday needs the URL, not the name.** Every other platform gives a company one guessable handle. A Workday board is addressed by a *tenant* **and** a *site id* — `nvidia` + `NVIDIAExternalCareerSite`, `adobe` + `external_experienced` — and the site id is arbitrary, so guessing it would only produce confident misses. Open the company's careers page and paste the address. All of these work, including a link to a single job (the board it belongs to is what gets watched):

```
https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
https://adobe.wd5.myworkdayjobs.com/en-US/external_experienced
https://salesforce.wd12.myworkdayjobs.com/en-US/External_Career_Site/job/California---San-Francisco/Some-Job_JR123
```

**What each platform's feed fills in.** A dash means that feed does not publish the field, so the column is empty for that platform.

| Column | Greenhouse | Lever | Ashby | SmartRecruiters | Workable | Recruitee | Workday | Phenom | Eightfold |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `title`, `url`, `job_id` | yes | yes | yes | yes | yes | yes | yes | yes | yes |
| `department` | yes | yes | yes | yes\* | yes | yes | — | yes | yes |
| `team` | —† | yes | yes | — | — | — | — | employer's choice‡ | — |
| `location` | yes | yes | yes | yes | yes | yes | yes\*\* | yes | yes |
| `remote` | — | yes | yes | yes | yes | yes | yes | employer's choice‡ | yes |
| `employment_type` | — | yes | yes | yes | yes | yes | with descriptions | yes | — |
| `published_at` | yes | yes | yes | yes | yes | yes | yes\*\*\* | yes | yes |
| `updated_at` | yes | — | — | — | — | yes | — | with descriptions | — |
| `description` (opt-in) | yes | yes | yes | — | yes | yes | yes | yes | yes |

\* SmartRecruiters leaves `department` to each employer and most large boards skip it; where it is empty the column carries the job **function** SmartRecruiters groups its own search by (`Engineering`, `Sales`). Both values come from the source.
\*\* Workday's list feed says `3 Locations` for a multi-office req; `includeDescription` resolves it to a real location.
\*\*\* Workday publishes relative wording (`Posted 6 Days Ago`), translated to a date and shipped alongside as `posted_relative`; the open-ended `Posted 30+ Days Ago` bucket becomes `null`. Details under *Honest limits*.
† Greenhouse carries a `team` only when an employer files a job under a second department, and none measured does: 0 of 2,181 jobs across six boards (stripe, databricks, figma, cloudflare, discord, airbnb). Expect this column to be empty for Greenhouse.
‡ Phenom leaves both to the employer. `team` is the feed's `subCategory`: filled on 73% of jobs.cvshealth.com's 19,036 postings and on none of careers.humana.com's 2,191 or careers.lilly.com's 620 (2026-09-07). `remote` is the feed's `isRemote` (`Yes`/`No`): on 100% of Humana's postings, absent from Lilly's and CVS's. Eightfold's `remote` comes from `workLocationOption` (`onsite`/`hybrid`/`remote`; only `remote` is true), set on 100% of the 464 Ericsson and 100 Micron postings measured; its list feed carries no employment type or edit time at all.

**Not covered:** any other applicant-tracking system (iCIMS, Taleo, SuccessFactors, BambooHR, Jobvite, Personio, Teamtailor and the rest), company careers pages built without a feed, and aggregators such as LinkedIn or Indeed. A URL on one of those gets a free `not-found` row (after the two Phenom/Eightfold probes described above come back empty-handed), never a guess. See *Need another job board?* below.

### How much does it cost?

Pricing is pay-per-event: **$0.0015 per job row delivered**, and nothing else — no fee per run, no charge for status rows, no charge on a monitor run where nothing changed.

| What happens | You pay |
| --- | --- |
| 50 new jobs delivered | $0.075 |
| 500 new jobs delivered | $0.75 |
| 1,000 new jobs delivered | $1.50 |
| First run on 100 companies with 5,000 openings between them | $7.50, once |
| A scheduled monitor run that finds 40 new jobs | $0.06 |
| A scheduled monitor run that finds nothing new (100 free `no-new-jobs` rows) | $0.00 |

Only rows you actually receive are billed: jobs your title filters removed, jobs you were already given on an earlier monitor run, and every status row are free. If you set a maximum cost for the run, delivery stops at that limit rather than handing you rows it cannot bill, you get a free `charge-limit-reached` row, and the held-back jobs are not marked as seen, so they arrive on your next run. On a run that is not billed per event, `job_rows_charged` in RUN\_SUMMARY reads `0` beside `pay_per_event: false`.

### How to use it

1. **List your companies.** Bare names for Greenhouse, Lever, Ashby, Workable and Recruitee (and SmartRecruiters, once its host permits the feed again — see *Honest limits*); a careers-page URL for Workday, Phenom and Eightfold (or for any company, to skip auto-detection).
2. **Press Start** for a one-off snapshot of every open job. Add `titleIncludes` / `titleExcludes` to narrow it, and `maxJobsPerCompany` to cap a large board.
3. **Turn on `monitorMode` and schedule it.** In Apify Console, open *Schedules* in the left menu, create a schedule, choose this actor, pick daily or hourly, and paste the same input. From the second run on, each run returns only new postings.
4. **Get told about new jobs.** On the actor's *Integrations* tab, connect the run's dataset to Slack, Gmail, Google Sheets, Zapier, Make or a webhook of your own; each scheduled run then pushes its new rows where you want them. The dataset is also available as JSON, CSV or Excel from the run page and through the Apify API.

Example input:

```json
{
    "companies": [
        "stripe",
        "https://jobs.lever.co/spotify",
        "openai",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "https://careers.lilly.com/us/en/search-results",
        "https://careers.micron.com/careers"
    ],
    "monitorMode": true,
    "titleIncludes": ["engineer", "developer"],
    "titleExcludes": ["senior", "staff"]
}
```

### Output examples

A job row:

```json
{
    "company": "stripe",
    "platform": "greenhouse",
    "job_id": "7532733",
    "title": "Backend Engineer, Payments",
    "department": "Engineering",
    "team": null,
    "location": "Remote, US",
    "remote": null,
    "employment_type": null,
    "url": "https://stripe.com/jobs/search?gh_jid=7532733",
    "published_at": "2026-08-12T09:11:04-04:00",
    "updated_at": "2026-08-20T11:02:55-04:00"
}
```

Free status rows:

```json
{
    "company": "bunq",
    "status": "filtered-out",
    "note": "13 open jobs found, 0 matched your title filters"
}
```

```json
{
    "company": "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "status": "results-truncated",
    "note": "the workday feed reports 2675 postings for this board but its public API served only 2000 — the rest are not available through it"
}
```

### Who uses a company jobs monitor

- **Recruiters and sourcers** — watch competitors' and clients' openings without refreshing careers pages; new roles land in Slack or a sheet each morning.
- **Job seekers** — your shortlist of companies, filtered to the titles you would actually apply to, checked on a schedule so you see a posting the day it appears.
- **Sales and lead generation** — a company opening engineering or sales roles is a company with budget. New job postings are a hiring signal you can act on the day they appear.
- **Analysts and job boards** — fresh listings straight from the source, with the board's size in every RUN\_SUMMARY and any shortfall flagged beside it.

### Honest limits

Everything below was measured against the live sources on 2026-09-03 (Phenom and Eightfold on 2026-09-07).

**Paging: every feed is read until it ends, and you are told who stopped us.** A feed ends with a short page, an empty page, or by starting to re-serve page 1 (Workday does that instead of ending, so rows are de-duplicated as they arrive and a wrap can never inflate a count).

- **Workday** — 20 postings per request; a larger page is refused outright with HTTP 400 on every tenant tried. Most tenants serve their whole board: `pwc.wd3` hands over 4,385 postings across 220 requests. **Some tenants clamp**, and that is detected rather than assumed: `nvidia.wd5` serves 2,000, then re-serves page 1, and reports `total: 2000` — while the same response's facet counts add up to 2,675 on three independent facets. Before calling that a source ceiling, this actor asks the feed for the whole board in one page; nvidia refuses, which is what makes the claim true. That run gets a free `results-truncated` row naming both numbers.
- **SmartRecruiters** (paging as measured before its host closed the feed by `robots.txt` on 2026-09-07; not read today) — states its own page size: ask for 1,000 and the payload answers `limit: 100` with 100 rows, so 100 is what it is paged at. It states `totalFound` on every page and ends cleanly (BoschGroup: 4,826 postings over 49 requests, measured 2026-09-03). Served is compared against `totalFound`, so a board that ends short of its own advertised size is reported truncated. **Since 2026-09-07 this feed is not read at all**, because `api.smartrecruiters.com/robots.txt` disallows it for this actor — see *A host's robots.txt is read before its feed is* below; the paging description stands for the day the host permits it again.
- **Lever** — honours whatever page size it is asked for (gopuff: the whole 765-posting board in a single request), so it is asked for one big enough to clear a board in one go and no page boundary is created for a row to fall through. A board larger than that still pages by `limit`+`skip` until the feed ends. Lever publishes **no total**, so a board's size is knowable only by reading the feed to its end — which is why `maxJobsPerCompany` limits the rows you are delivered there without shortening the read.
- **Greenhouse** — one response for the whole board, cross-checked against the feed's own `meta.total` (stripe 612 = 612, databricks 862 = 862). A server-side cap would show up as a truncation instead of a quiet gap.
- **Phenom** — `from`+`size` against the site's `/widgets` feed, 500 postings a request (a bigger page is clamped to 500 silently), with the board's `totalHits` on every page. Two things about it are unusual and both are handled rather than hidden. First, the feed has a **hard window: `from + size` may not pass 9,999** — a request at 9,999 answers zero hits and a zero total, whatever the board holds. A board above that (jobs.cvshealth.com, 19,036 postings) is therefore read **one category at a time**, using the facet counts the same feed publishes, and a category itself above 9,999 one US state at a time; the union is de-duplicated by job id and `jobs_found` stays the feed's own `totalHits`. Measured: CVS read whole in 10 partitions over about 46 requests (45–46 across reads, since a partition's last page depends on the day's counts), 19,013 and 19,028 of 19,036 on two reads. Second, the feed's **ordering is not stable between requests**, so a paged walk can lose a row at a page seam: two consecutive 5-page walks of careers.humana.com's 2,191 postings returned 2,190 and 2,191 distinct ids, and an unfiltered walk of the CVS window returned 9,997 distinct of 9,999. That seam loss sits inside the same small churn tolerance a live board's own comings and goings already get, and is never reported as a source ceiling — but it does mean a monitor run can, rarely, deliver a posting one run later than it appeared. Never earlier, never twice. If no facet can split a bucket below 9,999, the rows past the window are genuinely unreachable through the feed, and that run gets the free `results-truncated` row naming both numbers.
- **Eightfold** — `GET /api/pcsx/search` with `start`, newest first (`sort_by=timestamp`), and the board's `count` on every page. The page is a **server-fixed 10 rows** however it is asked, so a board costs one request per 10 postings: jobs.ericsson.com's 464 postings are 47 requests, careers.micron.com's 2,772 are 278, and starbucks.eightfold.ai's 22,985 would be 2,299. Eightfold also **rate-limits** anything faster than about one request a second (HTTP 429, or a 401 with `Please try again later`, measured after ~12 requests in 13 s), so this actor spaces its requests a second apart and, on a refusal, backs off and retries up to three times before reporting the page lost. At that pace a large Eightfold board takes minutes, which is what the per-board time budget below is for.
- **Ashby, Recruitee, Workable** — one response each, with no paging parameters to use. Largest boards measured: Ashby openai 770, Recruitee conclusion 150, Workable exoticca 43. These feeds publish no total, but one response **is** the whole board, so what they served is what `jobs_found` reports. Where a board's size genuinely cannot be established — a read cut short by the safety guard, a lost page, or a feed re-serving page 1 with no total to check against — `jobs_found` falls back to the rows actually read, and `results_truncated` or `partial` in the same RUN\_SUMMARY entry, plus the free status row, says why that number is not the board's size.

**Our own limits are labelled as ours.** Two of them, both reported as `truncation_reason: our-guard` with `guard` saying which. A runaway guard stops any paged read at 50,000 postings per board, so that a loop against a third-party feed cannot run away; it sits well above the largest board ever measured (starbucks.eightfold.ai, 22,985) and has never been reached on a live board. And a **per-board time budget of 20 minutes** stops a read that is too slow to finish inside a run: Eightfold's 10-row pages at one request a second put Starbucks at about 38 minutes, longer than a run with several companies can spend on one of them, and a run that times out delivers nothing and explains nothing. In both cases the free `results-truncated` row says *this actor* stopped the read, states the board's own size from the feed, and names the input that raises the limit — `sourceSafetyGuard` (exact: the page that trips it is trimmed back to it) or `boardTimeBudgetMinutes` (raise the run timeout with it). A capped read (`maxJobsPerCompany`) on Eightfold stops as soon as the cap is filled, because the board's size is on the first page.

**A host's `robots.txt` is read before its feed is — every host.** The vendor API hosts the seven hosted platforms answer on, and the employer domains Phenom and Eightfold live on, alike: a host's `robots.txt` is its owner's word on what may be read there, so it is fetched once per host per run, before the first feed request, and applied to each feed path the way RFC 9309 says: the group naming this actor (`DockhandCareerJobsMonitor`, or the family name `Dockhand`) outranks `User-agent: *`, the longest matching rule wins, `*` is a wildcard, a trailing `$` anchors the end, and a redirect is followed (bunq.recruitee.com sends its `robots.txt` to careers.bunq.com). On a tie the least restrictive rule wins, exactly as RFC 9309 §2.2.2 says and as Google's reference parser reads it: when an `Allow` and a `Disallow` of equal length both match a path, the path is **open**. That is the shape a CDN produces when it prepends a managed `User-agent: * / Allow: /` above the host's own `User-agent: * / Disallow: /` (measured 2026-09-07). A host whose only rule is `Disallow: /`, with nothing to tie against, is closed and never asked — api.smartrecruiters.com is exactly that. A longer `Allow` still wins over a shorter `Disallow`, which is what keeps every Eightfold site readable (below). A path a `Disallow` covers is **not requested** — that company gets the free `not-found` row and RUN\_SUMMARY says `robots.txt disallows <path> on <host>`. A `robots.txt` that answers 404 (or any 4xx) is "no rules" and the feed is asked; one that cannot be read at all — a network error, a timeout, a 5xx — is **not permission**, so nothing on that host is requested that run, and the reason says the rules could not be read rather than pretending the host is on no platform. Measured on 2026-09-07 with this actor's own identity, against the exact paths it requests: `boards-api.greenhouse.io` (`Disallow: /embed/` only), `api.lever.co` (`Allow: /`, and `Crawl-delay: 1`, which is met by the fixed one-second spacing below — the `Crawl-delay` directive itself is not parsed), `apply.workable.com` (an empty `Disallow`), `*.recruitee.com` (`Disallow: /v/` only) and `*.myworkdayjobs.com` (per-site rules that do not mention the `/wday/cxs/` feed paths) all permit their feeds; `api.ashbyhq.com` answers HTTP 401 to `/robots.txt`, which is "no rules". **`api.smartrecruiters.com` does not permit it**: its `robots.txt` is `User-agent: * / Disallow: /`, with only `LinkedInBot` allowed `/v1/companies/`, so every SmartRecruiters board — by name or by URL — gets the free `not-found` row with `robots.txt disallows /v1/companies/<Company>/postings on api.smartrecruiters.com` and no feed request is made. No other path to that data is tried; the feed comes back the day the host's rules permit it. On the employer domains: careers.lilly.com, jobs.cvshealth.com and careers.humana.com (Phenom) disallow `*/apply`, `*/px-widgets` and their tracking paths and say nothing about `/widgets`; careers.micron.com, jobs.ericsson.com, paypal.eightfold.ai, starbucks.eightfold.ai and vodafone.eightfold.ai (Eightfold) are `Disallow: /` with `Allow: /api/pcsx` — which is exactly why the longest rule has to win.

**About one request a second to any host, and no request is asked twice unless it went unanswered.** Every request to one host waits about a second after the previous one to that host — vendor API hosts included, and Workday's per-posting description pages too (they used to be fetched six at a time). Hosts do not wait on each other, so a run over six companies on six hosts is not slowed by it; one large board is: a 220-request Workday board takes about four minutes without descriptions, a 49-page SmartRecruiters board would take about 50 seconds. A request that got **no answer** — a DNS or connection failure, a reset, a timeout, which the runtime reports only as "fetch failed" — is asked again, up to three attempts with a short back-off and a 60-second budget per request, and so is an answered "not now" (HTTP 408, 425, 429, 500, 502, 503, 504) on a request that is safe to repeat; when the host still will not answer, RUN\_SUMMARY names the host, the attempt count and the underlying error. An answered "no" (a 404, a 400, a 403) is **never asked twice**: it is the source's answer and it is reported as it came. A **bot-verification challenge** (Cloudflare's managed challenge, AWS WAF's interstitial, and their kin) is told apart from both: it is a fact about the network this run was made from, not about the board — the same feeds commonly answer another network normally in the same hour — so it is named as such in the free row and in RUN\_SUMMARY (`reason: walled`, `sources_walled`), no workaround is attempted, and it never fails the run on its own. **The run fails only when every company it was given produced no board and at least one of them was a genuine outage** — a host that never answered, or an error answer; a run whose companies were all challenged, all refused by `robots.txt`, or all on no platform, exits normally with every row explained, so that a green run is never an empty one nobody could read, and a red run is never a network's mood.

**Reported counts mean what the source holds.** `maxJobsPerCompany` caps what you are delivered and charged for. It shortens the read only where that cannot change which rows you end up with, and `jobs_found` reports the board's real size — a run will never tell you a company has 5 openings because you asked for 5. The one time `jobs_found` is not the board's size is a read cut short or missing a page on a feed that states no total, and the same entry says so.

**A lost page is reported, not absorbed.** A page request that got no answer, or a "not now", is asked again as described above; if the page is still lost, that company's rows are delivered as far as they were read, with a free `partial` row and `partial_results: true` in RUN\_SUMMARY (a board whose very first request fails gets the free `not-found` row instead — see the FAQ). A shrunken count never gets to pose as a board's real size.

**Monitor mode remembers exactly two things: what it delivered to you, and what your own title filters excluded.** Both have had their outcome. Rows held back for any other reason — the per-company cap, the run's charge limit — are *not* remembered and arrive on your next run. Widening `titleIncludes` or dropping a `titleExcludes` word applies to jobs posted from then on, not to jobs already excluded under the old filter; to re-sweep a board's back catalogue, change the company list or clear the state store.

**An interrupted run never bills you twice.** State is saved after every company delivered, not once at the end, so a run cut short — platform timeout, migration, an aborted run — keeps what it already handed you and the next run carries on from there. State is saved *before* the charge, so an interruption at exactly the wrong moment leaves you with rows you were not billed for, never a bill for rows delivered twice.

**If billing itself fails, the run stops.** Delivery halts there, the companies after it get a free `charge-limit-reached` row, and the rows already written are not marked as seen, so your next run returns them.

**`published_at` precision differs by platform.** Greenhouse, Lever, Ashby, SmartRecruiters and Recruitee publish a full ISO timestamp; Workable and Workday publish a date only, and a bare date is never padded out to a fake midnight. Two feeds publish a *date* dressed as a timestamp, and both ship as they came, normalised to UTC, so you know the time part is the source's and not ours: Phenom's `postedDate` is a midnight timestamp of its own (`2026-08-10T00:00:00.000+0000`), and Eightfold's `postedTs` is epoch seconds at midnight UTC of the day the careers page shows as posted (`1788739200` → `2026-09-07T00:00:00.000Z`; 10 of 10 postings sampled on careers.micron.com, 2026-09-07) — date precision, not a time of day. Sorting works across all of them.

**Workday dates are relative at the source.** Its board feed publishes `Posted Today`, `Posted 6 Days Ago` — not timestamps. The exact buckets are translated into a `published_at` date (checked against Workday's own internal posting date: 14 of 14 exact) and the source's wording always ships alongside as `posted_relative`. Postings in the open-ended `Posted 30+ Days Ago` bucket get `published_at: null`, because that bucket is not a date. Expect roughly 50–100% of a tenant's Workday rows to carry a date, depending on how fast it posts.

**Workday locations and departments.** When a req is open in several offices the board feed literally says `3 Locations`, so that is what the `location` column says. The feed carries no department, team or employment type at all. Turn on `includeDescription` and each posting's own page is fetched, which fills in the resolved location, the employment type and the real posting date — at one extra request per posting, one a second on the tenant's host, inside the same per-board time budget as Phenom and Eightfold: a 1,000-posting Workday board is about 17 minutes of descriptions, and what does not fit the budget is counted in a free `partial` row, never left blank silently. Set `maxJobsPerCompany` or raise `boardTimeBudgetMinutes` for a large Workday board with descriptions on.

**Descriptions are plain text.** `includeDescription` adds the posting's description with HTML tags stripped; SmartRecruiters' board feed carries no description, so that column stays empty there. On Workday, Phenom and Eightfold the description is a separate request per posting (the job page, `jobDetail`, `position_details`), made one a second on the board's host, so a 620-posting board takes ten minutes of descriptions; Phenom's detail also fills `updated_at` and Eightfold's confirms the public URL. If the per-board time budget runs out before every description is fetched, the rows still ship and a free `partial` row says exactly how many lack one — a blank description is never passed off as the source having none. Set `maxJobsPerCompany` or raise `boardTimeBudgetMinutes` for a large board with descriptions on.

**Experience-level filtering is not offered**, because these feeds do not publish it reliably. Title keywords are the honest proxy.

### FAQ

**Is this legal?** It reads only the public, logged-out JSON job feeds that Greenhouse, Lever, Ashby, Workable, Recruitee, Workday, Phenom and Eightfold publish so their customers' careers pages can list openings — the same data anyone sees by visiting the page. No login, no proxies, no browser automation, no attempt to get around a block or a challenge, every request identifies itself as this actor, and no host is asked more than about once a second. Every host's `robots.txt` is read first and obeyed — the vendors' API hosts and employers' own domains alike; see *A host's robots.txt is read before its feed is* under Honest limits. Measured 2026-09-07: six of the seven vendor hosts permit their feeds and `api.smartrecruiters.com` does not, so SmartRecruiters boards are not read until it does; Phenom sites disallow their apply and tracking paths (`*/apply`, `*/px-widgets`, …), not the `/widgets` jobs feed; Eightfold sites are `Disallow: /` with an explicit `Allow: /api/pcsx`, which is the jobs feed. Job postings are company information, not personal data; no candidate or recruiter details are collected.

**How fresh is the data?** Each run reads the live feed at that moment, so freshness is your schedule: an hourly schedule catches a new posting within the hour. `published_at` is the posting date the source states. In monitor mode a job counts as new the first time it appears on a board you watch, whatever its posting date — so the first monitor run returns the whole board, and later runs return only what was not there before.

**What happens when a source blocks or fails?** A request that got no answer is asked again (three attempts, short back-off); a request the source answered "no" to is not. If a board's first request still fails — the host never answered, an HTTP error, a body that is not the feed — that company gets a free `not-found` row whose note says which of those it was, and RUN\_SUMMARY carries the exact answer (`detail`) and a one-word `reason`; nothing is marked as seen, so the next run picks that company back up. If a page fails mid-board, you get the rows that were read plus a free `partial` row. A bot-verification challenge is reported as exactly that — `reason: walled`, with the note saying it is about this run's network rather than the board — and does not fail the run; a genuine outage of every company in the run does. There is no retry through proxies or a browser: a source that refuses is reported, not fought.

**Why does Workday need a URL when the others take a name?** Because a Workday board is keyed by a tenant *and* an arbitrary site id, and guessing the site id would only produce confident misses. Paste the careers-page address; see *Which job boards it reads*.

**Why do Phenom and Eightfold need a URL too?** Because their sites live on the employer's own domain — `careers.lilly.com`, `jobs.cvshealth.com`, `careers.micron.com` — and nothing about a company name says which host that is, or whether it is a Phenom or an Eightfold site at all. Paste any page of the careers site and this actor asks the host which of the two it is; if you already know, the `phenom:` and `eightfold:` prefixes skip the question.

**How do I reset monitor mode?** Delete the key-value store named `career-jobs-monitor-state` in your Apify Storage, or change the company list (state is kept per list).

**Why pay per row instead of buying a jobs database?** Job databases are built for searching *everything*, so watching a fixed company list means re-buying the same unchanged rows every day plus a fee per run. Here you pay only for the job rows you receive, and a monitor day with nothing new costs nothing.

**Will a huge board time out?** The default run timeout is one hour. Every host is asked about once a second, so a board costs about as many seconds as it costs requests: the largest Workday board measured (4,385 postings over 220 requests) is about four minutes without descriptions; Phenom's largest (CVS, 19,036) is about 46 requests (45–46 across reads) and under a minute. Eightfold is the slow one — 10 postings a request — so its boards above roughly 12,000 postings hit the 20-minute per-board budget and get the free `results-truncated` row rather than a timed-out run; raise `boardTimeBudgetMinutes` and the run timeout together to read one whole, or use `maxJobsPerCompany`. With `includeDescription` on, budget one extra request per posting, one a second, on Workday, Phenom and Eightfold — the same per-board budget applies, and descriptions it does not reach are counted in a free `partial` row.

### Need another job board?

If your target companies hire through an applicant-tracking system not listed here, open an issue on this actor and name it. You will get an answer within a day, and if the platform publishes a public feed, coverage typically follows within days. More Dockhand tools: https://apify.com/dockhand

### Related

Watching law firms? **[Law Firm Jobs Monitor — Legal Job Alerts from BigLaw Portals](https://apify.com/dockhand/legal-jobs-monitor)** is the sibling actor: the same only-new monitor design, reading the hiring portals law firms actually use (viRecruit/viGlobal, cvMail, AllHires, WizeHire) instead of job aggregators. Both are on [our publisher page](https://apify.com/dockhand).

### About Dockhand

Dockhand builds data tools that count every row against the source and say exactly what is missing. Questions or a platform request: open an issue on this actor.

# Actor input Schema

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

Company slugs (e.g. `stripe`, `spotify`) or careers-page URLs. A bare slug is looked up across Greenhouse, Lever, Ashby, SmartRecruiters, Workable and Recruitee (every host's `robots.txt` is read and obeyed first; `api.smartrecruiters.com` currently disallows this actor, measured 2026-09-07, so a SmartRecruiters board returns the free `not-found` row naming that rule, with no request made, until the host permits it). **Workday must be given as a URL** (e.g. `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`), because a Workday board is keyed by a tenant AND an arbitrary site id that cannot be guessed from a company name — open the company's careers page and paste the address. Any `*.wdN.myworkdayjobs.com` link works, including a link to a single job. **Phenom and Eightfold sites live on the employer's own domain** (`careers.lilly.com/us/en/…`, `jobs.cvshealth.com`, `careers.micron.com/careers`, `<company>.eightfold.ai/careers`): paste any page of the careers site and the host is asked which of the two it is — its `robots.txt` is read first (once per host per run) and a feed path it disallows is not requested, then one request to each permitted feed; a host on neither gets a free `not-found` row naming both answers, and a host whose `robots.txt` refuses or cannot be read gets the same row with that reason in RUN\_SUMMARY. To skip the question, or when a probe misses, name the platform: `phenom:careers.lilly.com/us/en` or `eightfold:careers.micron.com/micron.com` (host, then the tenant domain — derived from the host when omitted: careers.micron.com → micron.com, vodafone.eightfold.ai → vodafone.com).

## `monitorMode` (type: `boolean`):

When on, the first run returns all current jobs and every later run returns only the jobs that appeared since — which is what makes this cheap to schedule. Two things are remembered: what was delivered to you, and what your own title filters excluded. Anything held back for any other reason — the per-company cap, the run's charge limit — is NOT remembered and arrives on a later run. State is stored per company list in a key-value store named `career-jobs-monitor-state` on your account; change the company list and monitoring starts fresh for the new list. A company with nothing new gets a free `no-new-jobs` row, so a quiet day never looks like a broken run.

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

Adds the description text to each row (bigger output). For Workday boards it also fills in the resolved location, the employment type and the exact posting date — but it costs one extra request per posting, so a several-thousand-job Workday board takes minutes rather than seconds. On Phenom and Eightfold the description is one extra request per posting made ONE A SECOND on the employer's host (Phenom's also fills `updated_at`); descriptions that do not fit the per-board time budget are counted in a free `partial` row, never left blank silently — use `maxJobsPerCompany` or raise `boardTimeBudgetMinutes` for a large board.

## `titleIncludes` (type: `array`):

Keep only jobs whose title contains ANY of these words (case-insensitive). Example: `engineer`, `junior`, `intern`. Empty keeps everything. Filtering happens before you are charged. In monitor mode a job your filters excluded counts as handled and is remembered, so widening this list applies to jobs posted from then on.

## `titleExcludes` (type: `array`):

Drop jobs whose title contains ANY of these words (case-insensitive). Example: `senior`, `staff`, `director`. In monitor mode a job your filters excluded counts as handled and is remembered, so removing a word here applies to jobs posted from then on.

## `includeNotFoundRows` (type: `boolean`):

Rather than leaving a company silently out of the results, add a row saying what happened. Every status this actor can emit: `not-found` (no supported job board answered for that name, a URL on a platform we don't cover, a board whose feed could not be reached on this run, or an employer host whose own `robots.txt` disallows the feed path or could not be read, so the feed was not asked — the exact reason is in RUN\_SUMMARY), `no-open-jobs` (the board you named was reached and lists nothing open), `filtered-out` (the board has jobs but none matched your title filters — the note says how many were on it), `no-new-jobs` (monitor mode: the board was reached and nothing on it is new since your last run), `charge-limit-reached` (this run's maximum charge limit ran out, or billing itself failed — jobs held back by the limit were not delivered; if billing itself failed, the rows already written are un-marked instead. Either way they are not marked as seen and arrive next run), `results-truncated` (paging stopped short of the whole board — the note says whether the SOURCE refused the rest or this actor's own `sourceSafetyGuard` or `boardTimeBudgetMinutes` stopped the run, with the numbers behind the claim) and `partial` (a request to the source failed twice, or — with descriptions on for a Phenom or Eightfold board — the per-board time budget ran out before every description was fetched, so that company's rows are an incomplete picture of its board; the note says how many). These rows are FREE — they are never charged. Turn off for job rows only.

## `sourceSafetyGuard` (type: `integer`):

This actor's own runaway protection, not a limit any job board imposes. 0 = the built-in 50,000 postings per board, which sits well above the largest board ever measured (starbucks.eightfold.ai, 22,985) and has never been reached on a live board — it exists only so a paging loop against a third-party feed cannot run away. If the guard is ever reached, the run emits a free `results-truncated` row saying THIS ACTOR stopped it and naming this input, and the count is exact. Greenhouse, Ashby, Workable and Recruitee serve a whole board in one response and ignore this.

## `boardTimeBudgetMinutes` (type: `integer`):

This actor's own limit on how long one company's board may take to read, not a limit any job board imposes. 0 = the built-in 20 minutes. It exists for the one feed shape that is slow by construction: Eightfold serves 10 postings per request and rate-limits anything faster than about one request a second, so its 22,985-posting Starbucks board would take ~38 minutes — longer than a run with several companies can spend on one of them, and a run that times out delivers nothing and explains nothing. When the budget runs out the rows read so far are delivered, a free `results-truncated` row says THIS ACTOR stopped the read and states the board's own size, and RUN\_SUMMARY carries `truncation_reason: our-guard` with `guard: time`. Raise it together with the run timeout to read a huge Eightfold board whole, or use `maxJobsPerCompany`. The same budget covers per-posting description requests on Phenom and Eightfold. Every other platform measured reads its largest board in well under a minute.

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

0 = no limit. Caps the rows you are delivered and charged for per company — a single large Workday, Phenom or Eightfold board can run to several thousand postings (CVS on Phenom: 19,036; Starbucks on Eightfold: 22,985). On Eightfold it also shortens the read, which is the cheap way to watch a huge board. In monitor mode it caps how many NEW jobs arrive per run; anything over the cap is not marked as seen, so it comes on the next run instead of being lost. The cap never changes the reported board size: RUN\_SUMMARY states the board's own total as `jobs_found` (never the cap) and what the fetch handed the run as `jobs_fetched`.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "https://jobs.lever.co/spotify",
    "openai",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "https://careers.lilly.com/us/en/search-results",
    "https://careers.micron.com/careers"
  ],
  "monitorMode": false,
  "includeDescription": false,
  "titleIncludes": [],
  "titleExcludes": [],
  "includeNotFoundRows": true,
  "sourceSafetyGuard": 0,
  "boardTimeBudgetMinutes": 0,
  "maxJobsPerCompany": 25
}
```

# Actor output Schema

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

Every job row found on this run plus the free status rows that explain any company with nothing to show, in the overview columns.

## `json` (type: `string`):

The same rows with every field, for scripts and integrations.

## `csv` (type: `string`):

The same rows as a spreadsheet file.

# 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",
        "https://jobs.lever.co/spotify",
        "openai",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "https://careers.lilly.com/us/en/search-results",
        "https://careers.micron.com/careers"
    ],
    "maxJobsPerCompany": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("dockhand/career-jobs-monitor").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",
        "https://jobs.lever.co/spotify",
        "openai",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "https://careers.lilly.com/us/en/search-results",
        "https://careers.micron.com/careers",
    ],
    "maxJobsPerCompany": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("dockhand/career-jobs-monitor").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",
    "https://jobs.lever.co/spotify",
    "openai",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "https://careers.lilly.com/us/en/search-results",
    "https://careers.micron.com/careers"
  ],
  "maxJobsPerCompany": 25
}' |
apify call dockhand/career-jobs-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dockhand/career-jobs-monitor"
        }
    }
}

```

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/cd5p2ujokhSOrSJad/builds/tbvpjHZ1PynPfRF1T/openapi.json
