# New Job Postings Monitor — Hiring Signals & Job Change Alerts (`insight.solutions/job-postings-monitor`) Actor

Watch company career pages on a daily schedule and get only what changed: roles opened, closed or edited, plus per-company open counts and hiring velocity. Covers Greenhouse, Lever, Ashby, Workable, Workday and 6 more ATS. Built for sales triggers and recruiting intel.

- **URL**: https://apify.com/insight.solutions/job-postings-monitor.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 change detecteds

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?

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

## New Job Postings Monitor — Hiring Signals & Job Change Alerts

**Track competitor hiring on a schedule and get only what changed.** Point this Actor at a list of company career boards, run it daily, and every run returns the roles that **opened**, **closed** or were **edited** since the last run — plus a per-company summary with open-role counts and hiring velocity.

It reads each ATS's own public feed across 11 platforms, so a run finishes in seconds with no browser, no proxies, no API keys and no login. **You pay per change, not per job.** A quiet day costs a fraction of a cent per company.

### Try it in 30 seconds

```json
{ "boards": ["greenhouse:stripe", "ashby:ramp", "lever:leverdemo"], "emitChanged": true }
```

Run it once to lay down the baseline, then schedule it daily. The first useful output arrives the next morning — see **First run** below.

### How the job posting change detection works

A career-board scraper hands you 400 postings every morning and leaves you to work out which three are new. This Actor does that comparison for you, and keeps doing it, because it remembers. Each run:

1. Reads every board you listed, straight from the ATS's own public endpoint — 11 platforms behind one input format.
2. Loads the fingerprint snapshot the previous run stored, one record per board.
3. Diffs them on job ID, then on title, location and salary.
4. Writes a row for each `new`, `closed` and `changed` posting, plus one `summary` row per company.
5. Stores the new snapshot for tomorrow.

**Supported ATS platforms:** Greenhouse (`greenhouse:stripe`), Lever (`lever:leverdemo`), Ashby (`ashby:ramp`), Workable (`workable:lyst`), Recruitee (`recruitee:vandebron`), SmartRecruiters (`smartrecruiters:Visa`), Teamtailor (`teamtailor:oatly` or `teamtailor:career.acme.com`), Personio (`personio:acme`), Breezy HR (`breezy:new-incentives`), JazzHR **(beta)** (`jazzhr:company3`), Workday **(beta)** (`workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`). You can also paste a career-page URL, or pass a bare company domain (`stripe.com`) and let the Actor find the board.

### Use cases

- **Track competitor hiring** — watch 50 companies; `newLast30Days` per company is a hiring-velocity leaderboard, and `closedCount` spikes with a falling `openCount` are the leading edge of a freeze.
- **Sales triggers and buying signals** — a company opening its first "Head of RevOps" role, or five SDR reqs in a week, is intent that shows up here weeks before a funding announcement. `new` rows plus `titleFilter` is the whole play.
- **Get alerted to new job postings** for roles you care about — `titleFilter` and `locationFilter` narrow the watchlist, and the `new` rows are the alert.
- **Monitor career page changes without an HTML differ** — the comparison happens on structured ATS fields, so a site redesign is not a change and a re-worded paragraph is not a false alert.
- **Compensation tracking** — put `salary` in `changedFields` and every published pay-range revision lands with the old and new numbers side by side.
- **Feed a warehouse** — the dataset is stable and flat; point the Apify → Snowflake/BigQuery/webhook integration at it and append.

### How it compares

Most "hiring signal" tools re-scrape the whole board and bill you per role returned, then tell you to diff the runs yourself. This one holds state and bills for the delta.

| | [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) | This Actor |
|---|---|---|
| Answers | "What is open at these companies?" | "What changed at these companies since yesterday?" |
| State | none — every run is independent | a snapshot per board, carried between runs |
| Billed on | every posting returned | every *change*, plus a flat fee per company watched |
| A quiet day costs | the full board, again | $0.002 + $0.001 per company |
| Best used | ad-hoc, or when you want the whole board | on a daily schedule |

Two more differences worth stating plainly:

- **An ATS outage never looks like a mass closure.** If a board that held 400 roles comes back with 10% or fewer, the comparison is abandoned rather than emitting hundreds of false `closed` alerts. Nothing else in this category does this.
- **Re-posted roles are one event, not two.** A role closed and immediately re-opened under a new ATS ID is reported once, as `new` with `reposted: true` — one row, one charge.

If you need the full current state of a board rather than the deltas, use **Career Site Jobs API** — same connectors, priced per posting. They pair well: pull the board once to seed your database, then schedule this Actor to keep it current.

### First run, and setting up a schedule

**The first time this Actor sees a board there is nothing to compare against**, so it will not bill you for a backfill you did not ask for. The first run stores the snapshot, writes **one summary row** per board with `firstRun: true` and the current `openCount`, charges `actor-start` and `company-monitored` once each, and charges **no** change events. Changes start on the second run. Set `firstRunBehavior` to `emit-all` for the whole board up front — every open role is then reported as `new` and billed once. Adding a *new* company re-baselines that company only.

To schedule it, run it manually once to check the input, then use **Actions → Schedule Actor**. **Daily is right for almost everyone**, and keeps the 7-day and 30-day velocity figures meaningful; early morning in the employer's timezone catches the previous day's postings. Point a Slack, webhook, email, Zapier or Make integration at "run succeeded" and filter on `eventType`.

**One state store per watchlist.** Two runs sharing one store would each diff against the same snapshot and double-report every change — so the second one stops with a clear message instead.

#### Where the state lives

An Actor's **default** key-value store is created fresh every run, so it cannot carry yesterday's snapshot. This Actor keeps state in a **named** store — `job-postings-monitor-state` by default — which works within its **limited permissions**: such an Actor may create storage, write to it, and re-open storages it created in previous runs. Nothing else in your account is touched, and limited permissions are a requirement for x402 agentic payments.

**The one tradeoff:** a limited-permissions Actor cannot read storages *you* created by hand. If `stateStoreName` points at one you made in Console, the Actor may not be able to open it — and it then logs a warning, treats the run as a first run and emits **no** change events, so a permissions problem never becomes a false wave of "closed" alerts. Let the Actor create its own store and this never comes up.

Each board gets one record: a small fingerprint per posting plus 90 days of daily counts (a 325-role board is ~94 KB). A record cannot exceed 9 MB and a fingerprint costs ~460 bytes, so one board tops out at **about 17,000 tracked roles**; beyond 8 MB the Actor refuses to write, keeps the previous snapshot, and returns a diagnostic row telling you to lower `maxJobsPerBoard` or narrow the filters.

### Input reference

| Field | Type | Default | What it does |
|---|---|---|---|
| `boards` **(required)** | array of strings | `["greenhouse:stripe"]` | One entry per company: `ats:slug`, a career-page URL, or a bare domain for auto-detection |
| `stateStoreName` | string | `job-postings-monitor-state` | The named key-value store holding the previous snapshot. One per watchlist |
| `emitNew` | boolean | `true` | Report roles that appeared |
| `emitClosed` | boolean | `true` | Report roles that disappeared |
| `emitChanged` | boolean | `false` | Report roles that were edited. Off by default — edits are noisier than openings and closures, and each one is billable |
| `emitUnchanged` | boolean | `false` | Also write a free, description-free row per unchanged role, for reconciling against your own data |
| `changedFields` | multi-select | `["title","location","salary"]` | What has to move before a posting counts as `changed`. Options: title, location, salary, description |
| `titleFilter` | array of strings | none | Monitor only roles whose title contains one of these (case-insensitive) |
| `locationFilter` | array of strings | none | Monitor only roles whose location contains one of these |
| `firstRunBehavior` | `baseline-only` | `emit-all` | `baseline-only` | What to do on a board's first run |
| `includeDescription` | boolean | `false` | Include the posting body on new and changed rows. Forced on when `description` is a tracked field |
| `maxJobsPerBoard` | integer | `2000` | Per-board ceiling. Keep it above your boards' size — see Reliability |
| `atsPlatforms` | multi-select | all | Restricts which platforms auto-detection will consider |
| `outageGuardThreshold` | integer | `10` | *(Advanced)* See Reliability |
| `requestTimeoutSecs` | integer | `20` | *(Advanced)* Per-request timeout |
| `maxRunSecs` | integer | `240` | *(Advanced)* Wall-clock budget for the whole run. Boards not reached in time get a diagnostic row and keep their snapshots |
| `maxConcurrency` | integer | `6` | *(Advanced)* Boards fetched in parallel; never more than one request per host |

**Filters decide what is watched, not just what is reported.** A `titleFilter` of `["engineer"]` means non-engineering roles are not in the snapshot at all. Changing a filter between runs will therefore look like roles opening and closing — set them once and leave them.

### Output reference

Every row carries `eventType` and `ok`, so "just the changes" is `ok === true && eventType !== 'summary'`.

**A change row** (`eventType`: `new` | `closed` | `changed` | `unchanged`):

```jsonc
{
  "ok": true, "eventType": "changed", "changeType": "changed",
  "jobId": "8077890", "atsPlatform": "greenhouse", "boardSlug": "stripe",
  "companyName": "Stripe", "companyDomain": "stripe.com",
  "title": "Principal Product Designer", "department": "Design",
  "employmentType": "FullTime", "seniority": "principal",
  "locationRaw": "London, United Kingdom; Dublin, Ireland",
  "isRemote": false, "workplaceType": null,
  "salaryMin": 95000, "salaryMax": 145000, "salaryCurrency": "GBP", "salaryPeriod": "YEAR",
  "descriptionText": null,            // only when includeDescription is on
  "applyUrl": "https://job-boards.greenhouse.io/stripe/jobs/8077890",
  "jobUrl": "https://stripe.com/jobs/search?gh_jid=8077890",
  "postedAt": "2026-08-11T09:04:00-04:00", "updatedAt": "2026-09-09T08:30:00-04:00",
  "changedFields": ["title", "location", "salary"],
  "previousValues": {                 // what those fields held yesterday
    "title": "Staff Product Designer",
    "locationRaw": "London, United Kingdom",
    "salaryMin": 95000, "salaryMax": 130000, "salaryCurrency": "GBP", "salaryPeriod": "YEAR"
  },
  "reposted": false, "previousJobId": null,
  "firstSeenAt": "2026-09-08T06:00:00.000Z", "lastSeenAt": "2026-09-09T06:00:00.000Z",
  "closedAt": null,                   // set on closed rows only
  "companyOpenRoles": 9, "companyOpenRolesDelta": -1,
  "previousRunAt": "2026-09-08T06:00:00.000Z", "runComparedAt": "2026-09-09T06:00:00.000Z",
  "scrapedAt": "2026-09-09T06:00:00.000Z",
  "source": "boards-api.greenhouse.io",
  "sourceUrl": "https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true"
}
```

A `closed` row has the same keys, read back out of the stored snapshot — the posting is gone, so there is nothing live left to read — with `closedAt` holding the run that first found it missing. That is our observation time, not the employer's closing date, which no ATS publishes.

**A summary row** — one per company, every run — adds `firstRun`, `openCount`, `previousOpenCount`, `openCountDelta`, `newCount`, `closedCount`, `changedCount` (counted even when `emitChanged` is off), `unchangedCount`, `repostedCount`, `newLast7Days`, `closedLast7Days`, `newLast30Days`, `closedLast30Days`, `newPerDay30Days`, `historyDays` and `truncated`. **Read the velocity fields against `historyDays`:** `newLast30Days: 4` after three days of monitoring is a three-day number, not a slow month.

**A diagnostic row** — `ok: false`, `eventType: "diagnostic"`, plus `input`, `atsPlatform`, `boardSlug`, `error`, `source`, `sourceUrl`, `checkedAt` — and **no charge**. Other boards still finish and the run still succeeds **as long as at least one board was compared**; a run in which *no* board could be compared is reported as **failed**.

### Pricing

Pay per event. **You are billed for changes, not for postings.**

| Event | Free | Starter (Bronze) | Scale (Silver) | Business (Gold) |
|---|---|---|---|---|
| `actor-start` — once per run, after the run has produced at least one real row | $0.002 | $0.002 | $0.002 | $0.002 |
| `company-monitored` — once per company actually compared this run | $0.001 | $0.001 | $0.0008 | $0.0006 |
| **`change-event`** — once per `new`, `closed` or `changed` row | **$0.004** | **$0.004** | $0.003 | $0.002 |

**Worked example — 25 companies, watched daily for a month, on a Starter plan.** Assume a typical 4 changes per company per week.

```
actor-start          30 runs ×  1 × $0.002  =  $0.06
company-monitored    30 runs × 25 × $0.001  =  $0.75
change-event         25 × 4 × 4.3 weeks     =  430 events
                            430 × $0.004    =  $1.72
                                      total =  $2.53 / month
```

The same watchlist pulled as full boards, at 60 roles per company, would be 45,000 postings a month. This is the whole argument for the monitor.

Five things that are unusual, and deliberate:

1. **Diagnostic rows are free.** A dead slug, a timeout, a domain with no detectable ATS — never billed.
2. **A suspected ATS outage is free.** The board is skipped, not billed (see below).
3. **The baseline run bills nothing per role.** First contact with a board costs one `actor-start` and one `company-monitored`.
4. **`unchanged` rows are free.** They are a reconciliation manifest, not a data product — they carry no description body.
5. **`actor-start` is charged only once real output exists.** A run that produces nothing but diagnostics costs nothing at all.

### Reliability

Five failure modes could make a monitor actively harmful. All five are handled explicitly.

1. **An ATS outage must never look like a mass closure.** If a board's previous snapshot held more than `outageGuardThreshold` roles (default 10) and it returns **10% or less** of that, the comparison is abandoned: no `closed` events, previous snapshot kept, a free diagnostic row. A genuine wind-down still gets through, delayed by exactly one run — the *next* run seeing the same collapse is treated as confirmation and the closures are reported normally. This is the single most important correctness rule in the Actor.
2. **Unread is not closed.** If a board fills `maxJobsPerBoard`, postings beyond the ceiling were not read — and what we did not read did not close. Closed detection is suspended for that board, unseen fingerprints are carried forward, and the summary row is flagged `truncated: true`.
3. **Nothing is billed before delivery, nothing delivered is lost, nothing is billed twice.** Rows reach your dataset before any charge is recorded, and a snapshot is written only after its rows are pushed. If a run hits your maximum-cost limit part-way through, the snapshot advances past exactly the changes you *did* receive.
4. **A run that could not do its job says so.** Every board failing, or the deadline arriving before any board was read, ends the run as **failed** with the reason in the status message.
5. **A misbehaving state store cannot destroy your history.** An unreadable snapshot makes the board fall back to a baseline — never inventing `closed` events — and is left as it is rather than overwritten. An unwritable snapshot produces a diagnostic row, because otherwise the next run would silently re-baseline for ever.

Snapshots for boards you stop monitoring expire after 90 days. `maxRunSecs` (default 240) bounds the run in wall-clock time: on expiry the Actor stops fetching, keeps and bills for everything already delivered, and writes a diagnostic row for each board it never reached.

### Integrations and use from code

One JSON object in, one flat array out. The Actor runs with **limited permissions**, uses **pay-per-event** pricing and never enters Standby mode, so it works over the Apify MCP server and with x402 agentic payments.

```bash
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~job-postings-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"boards":["greenhouse:stripe"],"emitChanged":true}'
```

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/job-postings-monitor").call(run_input={
    "boards": ["greenhouse:stripe", "ashby:ramp", "lever:leverdemo"],
    "titleFilter": ["revops", "sales"],
    "emitChanged": True,
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row.get("ok") and row["eventType"] in ("new", "closed", "changed"):
        print(row["eventType"], row["companyName"], row["title"], row["locationRaw"], sep=" | ")
```

### FAQ

**How often should I run it?**
Daily. Weekly is fine if you only want big moves; anything faster mostly buys duplicate `changed` events.

**Why do I sometimes see a `changed` row where nothing looks different?**
You will not, from a timestamp alone: a bare `updatedAt` bump never counts as a change, because most ATS platforms touch that field for unrelated reasons. A `changed` row means one of your `changedFields` actually moved, and `previousValues` shows exactly what it was.

**Can I track description edits?**
Yes — add `description` to `changedFields`. Descriptions are fingerprinted as a hash so snapshots stay small, so `previousValues` reports `descriptionHash` rather than the old body; you get the new body in `descriptionText`. This makes every run fetch the posting bodies, which is slower.

**What is `reposted`?**
Some ATS platforms give a re-opened role a brand-new ID, which would read as one closure plus one opening. When a `new` role's title and location match one that just disappeared from the same board, it is reported once, as `new`, with `reposted: true` and the old `previousJobId` — and `firstSeenAt` is carried across, so "how long has this role been open" survives the ID change.

**What happens if I change the board list?**
Companies you add get a baseline run. Companies you remove stop being compared; their snapshots expire after 90 days. Companies you keep are unaffected.

**Can two schedules share one state store?**
No, and the Actor enforces it. The second run finds the store locked, writes a diagnostic row, and exits without touching anything. The lock is valid for that run's `maxRunSecs` plus five minutes, so a long run is never evicted while working, and a crashed run releases its board within minutes.

**Why is a company I want missing?**
It is on an ATS we do not support yet (iCIMS, Taleo, SuccessFactors and BambooHR are the usual suspects); or it uses a supported ATS under a slug that differs from its brand name — check where the "Apply" button goes; or it hosts its own careers page with no ATS behind it, so there is no public feed to read.

**Will I get rate limited?**
These are syndication endpoints — vendors publish them so postings get crawled. The Actor sends at most one request at a time to any single host and backs off exponentially on 429 and 5xx. Each attempt times out at `requestTimeoutSecs` (20 s by default), no single URL is retried beyond about 40 s, and `maxRunSecs` bounds the whole run.

### Legal and data-protection notes

- **Public endpoints only.** Every source is an unauthenticated, publicly documented job-syndication feed or a public career board page. The Actor never logs in, never accepts cookies or session tokens, and never uses an API key belonging to anyone else.
- **No personal data by design.** Recruiter *contact details* — email addresses, `mailto:` links, personal LinkedIn profile URLs — and recruiter or hiring-manager attribution lines are removed from every description before it is written to your dataset. Recruiter names exposed as structured fields by an ATS are never mapped into the output at all. A name written into free prose with no attribution marker is not chased: no regex catches that without mangling the description around it.
- **`robots.txt` is respected on every HTML page we fetch** — the career pages read during domain auto-detection, and the JazzHR public board page. The file is fetched once per domain per run and evaluated per RFC 9309; a `Disallow` means the page is skipped. The other sources are the ATS vendors' own JSON and XML syndication feeds, which are not HTML pages.
- **You are the controller of what you do next.** If you export this data, combine it with personal data, or contact people based on it, GDPR/CCPA obligations are yours, not ours.
- **Not affiliated with Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Teamtailor, Personio, Breezy HR, JazzHR, Workday, or with any employer whose postings you monitor.** All product names and trademarks belong to their respective owners and are used only to describe which public feeds this Actor reads.

### Our other Actors

HTTP-only data Actors — no browsers, no proxies, charge-on-success.

| Actor | What it does |
|---|---|
| [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) | The same 11 ATS connectors without the state: every open posting as normalized JSON, $2 per 1,000 postings |
| [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) | Business emails, phone numbers and social profiles from a list of website domains |
| [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) | SEC filings, normalized XBRL financials and full-text search, by ticker or CIK |
| [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) | Archived URLs, archived page text, and a diff of one page between two dates |

# Actor input Schema

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

One entry per company. Use `ats:slug` (for example `greenhouse:stripe`, `lever:leverdemo`, `ashby:ramp`) when you know the platform, or just a bare company domain (`stripe.com`) and we will detect the ATS for you. Workday needs the board host and site name: `workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`.

## `atsPlatforms` (type: `array`):

Only relevant for bare-domain inputs. When set, domain auto-detection will only consider these platforms. Leave empty to try all of them.

## `stateStoreName` (type: `string`):

Name of the key-value store that holds the previous run's snapshot. The Actor creates it on its first run and reads it back on every run after that — that memory is what makes this a monitor rather than a scraper. Keep one store per watchlist: two schedules sharing a store would each diff against the same snapshot, so the second one stops with a clear message instead of double-reporting. Do not point this at a store you created by hand; a limited-permissions Actor may not read those, and the run would silently restart from a baseline.

## `emitNew` (type: `boolean`):

Report roles that appeared since the previous run.

## `emitClosed` (type: `boolean`):

Report roles that were in the previous snapshot and are gone now.

## `emitChanged` (type: `boolean`):

Report roles that are still open but were edited. Off by default: edits are noisier and less actionable than openings and closures, and each one is a billable event.

## `emitUnchanged` (type: `boolean`):

Also write one row per role that did not change, so you can reconcile our view of a board against your own. These rows are free — they are never billed as change events — and they carry no job description; for full postings use our Career Site Jobs API Actor. Off by default.

## `changedFields` (type: `array`):

Which parts of a posting have to move before it is reported as `changed`. A bare `updatedAt` bump never counts on its own — most ATS platforms touch that timestamp for reasons unrelated to the posting. Selecting `description` makes every run fetch the posting bodies, which is slower.

## `titleFilter` (type: `array`):

Monitor only roles whose title contains one of these strings (case-insensitive). For example `engineer`, `sales`. Filters decide what is watched, not just what is reported — changing them between runs will look like roles opening and closing.

## `locationFilter` (type: `array`):

Monitor only roles whose location contains one of these strings (case-insensitive). For example `Berlin`, `United Kingdom`, `Remote`. Same caveat as the title filter.

## `firstRunBehavior` (type: `string`):

What to do the first time a board is seen, when there is nothing to compare against. `baseline-only` stores the snapshot and writes just the per-company summary, so you are not billed for a backfill you did not ask for. `emit-all` reports every currently open role as `new`, which is billed per role.

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

Include the posting body on new and changed rows. Off by default because a hiring signal rarely needs it and leaving it off makes every run faster. Turned on automatically when `description` is one of your changed fields.

## `maxJobsPerBoard` (type: `integer`):

Ceiling on how many postings are read from any single board. Keep this comfortably above the size of the boards you watch: if a board fills the ceiling, the postings beyond it were not read, and unread is not closed — so closed-posting detection is suspended for that board and the summary row is flagged `truncated`.

## `outageGuardThreshold` (type: `integer`):

If a board suddenly returns zero postings and its previous snapshot held more than this many, the run treats it as an ATS outage rather than a mass closure: no `closed` events, the old snapshot is kept, and a diagnostic row explains what happened. Set to 0 to disable the guard — not recommended.

## `requestTimeoutSecs` (type: `integer`):

Per-HTTP-request timeout. Raise it only if you are monitoring very large Workday boards.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run, so one unresponsive career site cannot hold a scheduled run open until the platform timeout. When it is reached the Actor stops fetching, keeps and bills for everything already delivered, and writes a diagnostic row for each board it never reached — those boards keep their snapshots and are not charged for.

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

How many career boards to fetch at the same time. We never send more than one request at a time to the same host.

## Actor input object example

```json
{
  "boards": [
    "greenhouse:stripe"
  ],
  "atsPlatforms": [],
  "stateStoreName": "job-postings-monitor-state",
  "emitNew": true,
  "emitClosed": true,
  "emitChanged": false,
  "emitUnchanged": false,
  "changedFields": [
    "title",
    "location",
    "salary"
  ],
  "titleFilter": [],
  "locationFilter": [],
  "firstRunBehavior": "baseline-only",
  "includeDescription": false,
  "maxJobsPerBoard": 2000,
  "outageGuardThreshold": 10,
  "requestTimeoutSecs": 20,
  "maxRunSecs": 240,
  "maxConcurrency": 6
}
```

# Actor output Schema

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

New, closed and changed postings since the previous run, plus one summary row per company. Delivered as JSON items in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "boards": [
        "greenhouse:stripe"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/job-postings-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 = { "boards": ["greenhouse:stripe"] }

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/job-postings-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 '{
  "boards": [
    "greenhouse:stripe"
  ]
}' |
apify call insight.solutions/job-postings-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,insight.solutions/job-postings-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/eMMFbgjIN5ejeov8C/builds/Mf2uEF9dLw6kgaEpg/openapi.json
