# Greenhouse Stale Jobs Checker & Job Board Audit (`apisolutions/stale-jobs-checker`) Actor

Compare Greenhouse jobs with your CSV/JSON catalog. Flag stale fields and duplicates, preserve protected edits, and review missing postings before making changes. Built for job boards and aggregators. $0.04 per complete board; platform usage included.

- **URL**: https://apify.com/apisolutions/stale-jobs-checker.md
- **Developed by:** [Apisolutions](https://apify.com/apisolutions) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 33.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 checked records

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

### Extract Greenhouse jobs and reconcile your catalogue

**Greenhouse Stale Jobs Checker & Job Board Audit** checks your job catalogue against the public Greenhouse boards you specify. Find stale titles, locations and job links, flag duplicate local records, and get correction previews that preserve your protected fields.

Paste a board list to get a current job inventory. Add your existing catalogue as CSV or JSON to see what needs attention. You receive a downloadable table and structured JSON with source evidence. The Actor leaves changes to your catalogue under your control.

Built for **job-board operators, job aggregators and teams maintaining authorised career-site catalogues**. Developers and automation agents can submit the same input through Apify's API and process the same action codes and patch previews.

Ready to check your own data? Choose **Run on my Greenhouse boards**. You can skip the example preview.

### Get started with live boards

1. Select **Run on my Greenhouse boards** and supply Greenhouse board tokens or supported HTTPS board-root URLs.
2. Confirm that you own or are authorised to maintain the catalogue and have permission for the intended source use. `sourceAccessConfirmed` must explicitly be `true`.
3. Optionally paste your current records into **Catalogue CSV** or **Catalogue JSON**. Use one format at a time.
4. Choose the fields to compare and whether to include new-job candidates. Set the maximum charge to cover your intended checks: **$0.04 per complete board**.
5. Run at **512 MiB and 300 seconds**. Review the dataset and `OUTPUT` summary, including unknown or skipped boards.

Example board-list input:

```json
{
  "mode": "live",
  "boards": ["figma", "https://job-boards.greenhouse.io/webflow/"],
  "sourceAccessConfirmed": true
}
```

API inputs that omit mode retain live behaviour and require explicit source consent. Empty or invalid live input never falls back to the sample.

These names demonstrate syntax; check your own source-use permissions. Live counts change. Without a supplied catalogue, current source postings become `DISCOVERED` rows.

#### If a live run asks for source permission

Live checks require both a board list and explicit permission confirmation. If the run reports `sourceAccessConfirmed`, open its **Input**, confirm you own or are authorised to maintain the catalogue and have permission for the intended source use, then tick **Confirm permission for live boards and catalogue**. In JSON, the field must be the boolean `true`, not the text `"true"`.

For demonstration data instead, choose **Preview example results (demo data)** and leave board/catalogue fields empty. The Actor never changes an invalid live request into a demo or enables your consent automatically. Rejected input produces no job rows or board charge.

### Preview example results

Choose **Preview example results (demo data)** to see seven labelled examples: matching fields, a title correction, a protected edit, duplicates, a missing-posting review and a new-job candidate. The preview is optional and has **no per-board fee**.

These are demonstration jobs, not live listings. The preview does not fetch Greenhouse jobs or check current source availability. Leave your board and catalogue fields empty; select **Run on my Greenhouse boards** before entering your own data.

For an API preview, send {"mode": "sample"}. Every example row is marked isSample: true and includes a sample notice. Platform usage is included in the configured pricing.

### Add your existing job inventory

Each record needs `localId`, `board` and `jobId`. Keep IDs as strings. The join uses the **public posting ID within its board**, not Greenhouse's internal job ID.

This authored example uses the local `examplecompany` fixture, not a live demonstration source:

```json
{
  "boards": ["examplecompany"],
  "sourceAccessConfirmed": true,
  "inventory": [{
    "localId": "refresh",
    "board": "examplecompany",
    "jobId": "2",
    "title": "Engineer",
    "jobUrl": "https://local.example.org/apply?campaign=ours",
    "protectedFields": ["jobUrl"]
  }]
}
```

In that fixture, the source title is `Senior Engineer`. The result proposes the title update while preserving the local application link. This is a field excerpt from the saved fixture result:

```json
{
  "action": "REFRESH",
  "localId": "refresh",
  "jobId": "2",
  "currentTitle": "Engineer",
  "sourceTitle": "Senior Engineer",
  "protectedChanges": ["jobUrl"],
  "patch": [
    {"op": "test", "path": "/localId", "value": "refresh"},
    {"op": "test", "path": "/board", "value": "examplecompany"},
    {"op": "test", "path": "/jobId", "value": "2"},
    {"op": "test", "path": "/title", "value": "Engineer"},
    {"op": "replace", "path": "/title", "value": "Senior Engineer"}
  ]
}
```

Patches target the original inventory object and check identity and old values before replacements. They are output data; the Actor never executes them against your system. Omitted fields are not compared or added. Explicit JSON `null` means an expected null; an optional blank CSV cell means unknown.

CSV headers are `localId,board,jobId` plus optional `title,location,jobUrl,protectedFields`. Quoted commas and newlines are supported. Multiple protected fields use `|`, such as `title|jobUrl`. Local IDs must be unique; different IDs sharing a posting key are reported as duplicates.

#### Use your existing CSV column names

You can keep your export's headers. In **Input → CSV columns (optional)**, enter the exact column name for each field that differs. For example, enter `Record ID` in **Local record ID column**, `Board` in **Greenhouse board token column**, and `Posting ID` in **Greenhouse posting ID column**. Blank controls use the standard names above. Names are case-sensitive; no identity is guessed or generated.

Map the optional title, location, application-link and protected-field columns you want to use. If your export contains other columns, explicitly enable **Ignore unmapped CSV columns** after checking they are unnecessary. Their values are not compared or copied into preview records. No rows are silently skipped. CSV supports up to 100 unique, nonempty headers of 200 characters each, within the existing 2 MiB input limit.

For an optional check before fetching live jobs:

1. In **What would you like to run?**, select **Preview my CSV import (no source check)**.
2. Supply your board list, confirm permission to use your catalogue, paste your CSV, and fill any column controls needed.
3. Run, then open **Output → CSV import preview** or **Run summary**. Review `columnMapping`, `ignoredColumns`, and the first five `previewRecords`. All rows are validated even though only five are displayed. An error identifies the affected field and CSV record's ending line without copying its values into the log.
4. When the mapping is correct, start a new run with **Run on my Greenhouse boards**, using the same CSV and column controls. A complete board costs $0.04.

Import preview has **no board fee**, makes no Greenhouse requests, and emits no charge events. Platform usage is included and still has an underlying resource cost. It validates input structure, not whether a posting exists or the mapping matches your intended meaning. It produces no reconciliation dataset rows. It does not turn invalid live input into a preview.

API inputs use `mode: "import-preview"` to preview. The seven optional controls are `csvLocalIdColumn`, `csvBoardColumn`, `csvJobIdColumn`, `csvTitleColumn`, `csvLocationColumn`, `csvJobUrlColumn` and `csvProtectedFieldsColumn`; `csvIgnoreExtraColumns` defaults to `false`. These controls apply only to CSV. JSON records keep the original schema. The following import-preview example is authored data, not a live source check:

```json
{
  "mode": "import-preview",
  "boards": [
    "examplecompany"
  ],
  "sourceAccessConfirmed": true,
  "inventoryCsv": "Record ID,Board,Posting ID,Job Title,Apply Link,Protected,Notes\nrefresh,examplecompany,2,Engineer,https://local.example.org/apply?campaign=ours,jobUrl,Not imported\nmissing,examplecompany,6,Previous posting,,,Not imported\n",
  "csvLocalIdColumn": "Record ID",
  "csvBoardColumn": "Board",
  "csvJobIdColumn": "Posting ID",
  "csvTitleColumn": "Job Title",
  "csvJobUrlColumn": "Apply Link",
  "csvProtectedFieldsColumn": "Protected",
  "csvIgnoreExtraColumns": true
}
```

Patch paths always refer to the **canonical mapped record** (`localId`, `board`, `jobId`, `title`, `location`, `jobUrl`), not your CSV's original header names. Protected-field cell values also keep the canonical names, such as `title|jobUrl`. No patch or CSV change is applied automatically.

### Understand your results

| Action | Meaning |
| --- | --- |
| `KEEP` | The selected, supplied values match |
| `REFRESH` | An unprotected field needs a proposed update |
| `PROTECTED_DIFFERENCE` | Differences occur only in protected fields |
| `DUPLICATE_LOCAL` | Several local records share the board/posting key; no patch |
| `REVIEW_UNLISTING` | A local posting is absent from a complete source board; review it |
| `DISCOVERED` | A source posting has no matching local record |
| `UNKNOWN` | Retrieval, completeness or a limit prevented reliable comparison |
| `INVALID` | A board-list entry is unsupported |

Missing postings are **review items**. They are not proof that a role was filled and never generate delete or deactivate instructions. Unavailable or incomplete boards leave your inventory untouched.

The default dataset is the flat table, available through Apify's output views and exports. Use JSON for nested arrays and patches. Rows include `rowKey`, `boardToken`, `jobId`, current/source values, action codes and `boardResultKey`.

In the run's key-value store, `BOARD-{token}` holds a board evidence packet with source URL, observation time and response hash. Hashes identify observations; they are not Greenhouse signatures. `OUTPUT` reports board outcomes, limits and separate eligible, attempted, confirmed-charged and charge-unknown totals. A complete empty board needs no artificial job row: its packet is the useful result.

A run may deliver useful data and end `PARTIAL`, `SPENDING_LIMIT` or `TIME_LIMIT`. Completed results remain available. A successful cloud process alone does not mean every requested board completed.

### Pricing: $0.04 per complete board

The example preview has no per-board fee.

One **`board-reconciled`** event costs **US$0.04**, with **platform usage included**. It covers a unique board's complete saved packet and committed rows. There is no job-row fee or start fee.

The optional CSV import preview also emits no charge events; it only validates your input and shows its mapping.

| Complete delivered boards | Board-event price |
| --- | ---: |
| 1 | $0.04 |
| 2 | $0.08 |
| 25 | $1.00 |

Valid empty boards and complete checks requiring no corrections qualify. Failed, invalid, oversized, incomplete or undelivered boards do not. Duplicate board inputs share one check and event. Each independently requested run is a fresh check.

The Actor reserves $0.04 before starting a board and respects a stricter spending limit. Below $0.04, source work stops. Ambiguous charge responses remain unknown and are not retried by the application; unknown is not a confirmed zero charge.

### Scope and limits

Up to **25 board inputs**, **500 source jobs and 500 output rows per board**, and **5,000 rows per run**. Complete packets are limited to **512 KiB**; input and source bodies to **2 MiB**. Limit violations produce explicit uncharged diagnostics, never a silently truncated complete board.

Only the [official public Greenhouse Job Board API](https://docs.greenhouse.io/job-board.html) is used. Selected fields are posting ID, title, location, public job URL and source update timestamp. Public accessibility is not an unrestricted redistribution licence.

#### Does it monitor jobs or update my database?

Your supplied catalogue is the baseline for each run. There is no cross-run monitoring database, automatic catalogue write, notification service or preconfigured schedule. Checkpoints support conservative current-run recovery; they are not job history.

#### Does it collect descriptions, applicants or other ATS data?

No. It checks specified public Greenhouse boards. It excludes descriptions, salary enrichment, applicants, private Harvest data, other ATS sources, company discovery and hosted-page scraping.

#### Why is a board unknown?

A 404 is unavailable, not verified empty. Denied access, redirects, inconsistent totals, malformed jobs and exceeded limits also prevent completeness. Transient failures receive at most one bounded retry. Check `OUTPUT.boards` and `reasonCodes` before acting.

### Local source usage

The source package includes a local CLI with dry-run billing. From its directory, using Python 3.12:

```bash
python3.12 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.lock
python -m pytest -q
python -m reconciler --input examples/catalogue.json --transport fixture --fixture-dir fixtures/boards --output-dir artifacts/my-demo --billing dry-run
```

The authored demo includes a failed board: expect eight rows, `PARTIAL`, exit **3**, and one hypothetical event. Input errors exit **2**; complete local runs exit **0**. Local mode uses no Apify credentials or real billing. Its CSV writer neutralises formula prefixes while preserving original JSON values. Detailed engineering instructions and test evidence accompany the local source package.

To preview the mapped CSV example locally without any source request:

```bash
python -m reconciler --input examples/csv-import.json --preview-import --transport fixture --output-dir artifacts/import-preview --billing dry-run
```

Read `IMPORT_PREVIEW.json` or `OUTPUT.json`; expect `IMPORT_PREVIEW_COMPLETED`, no source requests and no hypothetical charge. Use a fresh output directory for a subsequent live reconciliation.

# Actor input Schema

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

Choose a labelled demo, validate your CSV import without fetching Greenhouse or emitting charge events, or check live boards. Import preview and live mode require your permission confirmation. Empty or invalid live input never becomes a preview.

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

For Run on my Greenhouse boards: enter up to 25 public Greenhouse board names or root URLs. Each complete saved board costs $0.04, with platform usage included. Leave this empty when previewing example results.

## `sourceAccessConfirmed` (type: `boolean`):

For your own CSV preview or live boards, confirm you own or are authorised to maintain the supplied catalogue and have permission for the intended source use. You must explicitly set this field to true. The authored example preview uses demonstration data; leave it unchecked there. Import preview does not fetch source jobs.

## `inventoryCsv` (type: `string`):

Paste your CSV including its header. Standard required columns: localId,board,jobId. For different headers, expand CSV columns below and enter their exact names. You can preview the import before a live check. Use either CSV or Catalogue JSON.

## `inventory` (type: `array`):

Array of inventory records. Omit when using CSV.

## `compareFields` (type: `array`):

Compare only the selected title, location and jobUrl fields that are present in each catalogue record. Protected fields are never replaced.

## `includeNewJobs` (type: `boolean`):

Include source postings absent from the supplied catalogue as reviewable discovery candidates. No catalogue changes are applied automatically.

## `maxTotalRows` (type: `integer`):

Stop before exceeding this total output-row limit, including diagnostic rows. The hard maximum is 5,000 rows; incomplete or skipped boards are not charged.

## `csvLocalIdColumn` (type: `string`):

Exact CSV header for localId, including spaces and case. Leave empty to use localId. Values must uniquely identify your existing catalogue records.

## `csvBoardColumn` (type: `string`):

Exact CSV header for board, including spaces and case. Leave empty to use board. Values must be board tokens such as examplecompany, not company names or URLs.

## `csvJobIdColumn` (type: `string`):

Exact CSV header for jobId, including spaces and case. Leave empty to use jobId. Must contain the public numeric posting ID; never an internal requisition ID or guessed match.

## `csvTitleColumn` (type: `string`):

Exact CSV header for title, including spaces and case. Leave empty to use title. Optional when this field is not in your CSV.

## `csvLocationColumn` (type: `string`):

Exact CSV header for location, including spaces and case. Leave empty to use location. Optional when this field is not in your CSV.

## `csvJobUrlColumn` (type: `string`):

Exact CSV header for jobUrl, including spaces and case. Leave empty to use jobUrl. Optional when this field is not in your CSV.

## `csvProtectedFieldsColumn` (type: `string`):

Exact CSV header for protectedFields, including spaces and case. Leave empty to use protectedFields. Cell values use title, location or jobUrl separated by |; those field names stay canonical.

## `csvIgnoreExtraColumns` (type: `boolean`):

Explicitly allow other columns in your export. Their values are not compared, copied into preview records or changed. First map any IDs, compared fields and protected-field column you need, then inspect the import preview. No CSV rows are skipped.

## Actor input object example

```json
{
  "mode": "sample",
  "sourceAccessConfirmed": false,
  "compareFields": [
    "title",
    "location",
    "jobUrl"
  ],
  "includeNewJobs": true,
  "maxTotalRows": 5000,
  "csvIgnoreExtraColumns": false
}
```

# Actor output Schema

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

Flat job rows with actions, source evidence, protected-field differences and safe patch previews. Use JSON for arrays and patches.

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

Board completeness, row counts, limits and outcomes. Check this before treating the whole catalogue as reconciled.

## `billingLedger` (type: `string`):

Per-board event attempts and acknowledgements. Owner test events are not paid-customer revenue.

## `importPreview` (type: `string`):

Available after Preview my CSV import: column mapping, ignored columns and first five validated records. No live source check or reconciliation rows.

# 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 = {
    "mode": "sample"
};

// Run the Actor and wait for it to finish
const run = await client.actor("apisolutions/stale-jobs-checker").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 = { "mode": "sample" }

# Run the Actor and wait for it to finish
run = client.actor("apisolutions/stale-jobs-checker").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 '{
  "mode": "sample"
}' |
apify call apisolutions/stale-jobs-checker --silent --output-dataset

```

## MCP server setup

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

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/AhQ83PjRYiGLYAc4p/builds/LFj6r7LtvSB0wCpUy/openapi.json
