# ATS Jobs Feed (`unrealevents22/agent-bridge`) Actor

Company names, domains or careers URLs in; every open job from Greenhouse, Lever and Ashby out, in one normalized schema. Optional monitor mode returns only new, updated and removed jobs.

- **URL**: https://apify.com/unrealevents22/agent-bridge.md
- **Developed by:** [Agent Factory](https://apify.com/unrealevents22) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 5.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 results

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

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ATS Jobs Feed: Greenhouse, Lever & Ashby jobs API

Give it a list of **company names, domains or careers-page URLs**. It finds each company's job board on
**Greenhouse, Lever or Ashby** and returns **every open job in one clean, normalized format**, no matter which ATS
the company uses. Turn on **monitor mode** and scheduled runs return **only jobs that are new, updated or removed**
since the last run. That's what you want for hiring-signal alerts, job boards and sales prospecting.

- ✅ Official public job-board APIs only. No browser, no proxies, no login, no anti-bot fights.
- ✅ Auto-detection: `acme.com` → careers page → Greenhouse/Lever/Ashby board.
- ✅ One schema across all three ATSs: title, department, team, locations, remote flag, employment type, salary (when published), dates, URLs.
- ✅ Monitor mode with per-board state, so a temporary API error never shows up as "all jobs removed".
- ✅ Companies whose board can't be found are **not charged**.

### What can I use it for?

- **Hiring signals / sales intelligence:** get notified when target accounts open (or close) roles.
- **Niche job boards and newsletters:** pull fresh roles from a curated company list every day.
- **Recruiting and market research:** see who is hiring, where and at what salary bands.
- **Job alerts:** filter by title, location, department or remote, then schedule.

### Input

| Field | Type | Description |
|---|---|---|
| `companies` (required) | array of strings | Any mix of `acme.com`, `https://acme.com/careers`, `https://jobs.lever.co/acme`, `https://job-boards.greenhouse.io/acme`, `https://jobs.ashbyhq.com/acme`, `greenhouse:acme`, `lever:acme`, `lever-eu:acme`, `ashby:acme`, or plain names like `Acme Inc` |
| `onlyNewSinceLastRun` | boolean (default `false`) | Monitor mode: emit only `new` / `updated` / `removed` jobs |
| `stateKey` | string (default `default`) | Name of the monitor. Use one key per watchlist/schedule |
| `titleIncludes` / `titleExcludes` | array | Case-insensitive substring filters on the job title |
| `locations` | array | Keep jobs where any location contains one of these strings |
| `remoteOnly` | boolean | Keep only jobs with `isRemote = true` |
| `departments` | array | Match on department or team |
| `postedWithinDays` | integer | Keep jobs first posted in the last N days |
| `includeDescription` | boolean (default `false`) | Add a plain-text `descriptionText` |
| `includeDescriptionHtml` | boolean (default `false`) | Add `descriptionHtml` |
| `maxJobsPerCompany` | integer | Newest N jobs per company |
| `maxItems` | integer | Stop after N items in total |

Example:

```json
{
  "companies": ["stripe.com", "https://jobs.lever.co/leverdemo", "ashby:ashby", "Airbnb"],
  "titleIncludes": ["engineer"],
  "onlyNewSinceLastRun": true,
  "stateKey": "engineering-watchlist"
}
```

#### How companies are matched

1. **ATS URL or `ats:slug`**: used directly (most reliable).
2. **Domain or careers URL**: the Actor fetches the given URL, `/careers`, `/jobs` and the homepage (following redirects
   and a few same-site "Careers"/"Jobs" links, and respecting `robots.txt`), then looks for Greenhouse, Lever or Ashby board links and embeds.
3. **Plain names (or when step 2 finds nothing)**: likely board names are probed (`Acme Robotics` → `acmerobotics`, `acme-robotics` …)
   on Greenhouse → Lever → Ashby. Greenhouse probe hits are checked against the board's company name.

Tip: if a company is not found or the wrong board is matched, pass the board URL or `ats:slug` explicitly.

### Output

One dataset item per job. The shape is the same for every ATS:

```json
{
  "company": "ashby",
  "ats": "ashby",
  "boardSlug": "ashby",
  "jobId": "a819e537-3a23-4615-bbf8-4ee065d7e25d",
  "title": "HR Business Partner, EMEA",
  "department": "People & Talent",
  "team": "People & Talent",
  "locations": ["United Kingdom"],
  "isRemote": true,
  "workplaceType": "remote",
  "employmentType": "full-time",
  "compensation": { "min": 100000, "max": 135000, "currency": "GBP", "interval": "year", "raw": "£100K - £135K" },
  "postedAt": "2026-10-07T18:45:13.968Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/ashby/a819e537-3a23-4615-bbf8-4ee065d7e25d",
  "applyUrl": "https://jobs.ashbyhq.com/ashby/a819e537-3a23-4615-bbf8-4ee065d7e25d/application",
  "firstSeenAt": null,
  "scrapedAt": "2026-10-08T01:35:42.777Z",
  "input": "ashby:ashby"
}
```

| Field | Notes |
|---|---|
| `company` | Greenhouse company name. For Lever/Ashby it's your input name, or the board slug for URL/domain inputs |
| `ats`, `boardSlug` | `greenhouse` / `lever` / `ashby` and the board name |
| `isRemote` | `true` fully remote, `false` on-site or hybrid, `null` unknown |
| `workplaceType` | `remote` / `hybrid` / `onsite` / `null` |
| `employmentType` | `full-time`, `part-time`, `contract`, `internship`, `temporary`, or the ATS's raw value |
| `compensation` | `{min, max, currency, interval, raw}` when the ATS publishes it (Lever, Ashby), else `null` |
| `postedAt`, `updatedAt` | ISO 8601 UTC. `updatedAt` is only provided by Greenhouse |
| `descriptionText`, `descriptionHtml` | Only when requested |
| `changeType`, `changedFields`, `firstSeenAt` | Monitor mode only (see below) |
| `input` | The input string this job came from |

The run also writes an **`OUTPUT`** record to the default key-value store, so you can see which companies were matched and which were missed:

```json
{ "mode": "full", "totals": { "companies": 3, "resolved": 2, "unresolved": 1, "jobsEmitted": 967 },
  "companies": {
    "figma.com": { "resolved": true, "ats": "greenhouse", "slug": "figma", "jobCount": 153, "resolvedVia": "careers-page" },
    "openai.com": { "resolved": true, "ats": "ashby", "slug": "openai", "jobCount": 814, "resolvedVia": "careers-page" },
    "Totally Unknown Widgets LLC": { "resolved": false, "reason": "No Greenhouse, Lever or Ashby board found" } } }
```

You can export the dataset as JSON, CSV, Excel or XML, or read it through the Apify API.

### Monitor mode (only new, updated and removed jobs)

Set `onlyNewSinceLastRun: true` and schedule the Actor (e.g. daily).

- **First run** = baseline: every current job is emitted with `changeType: "new"`.
- **Later runs** emit only:
  - `new`: job wasn't on the board last time.
  - `updated`: title, department, team, locations, remote/workplace, employment type, compensation, URLs or description text changed. `changedFields` lists what changed. (Greenhouse's `updated_at` timestamp alone does **not** count, because it changes on internal edits.)
  - `removed`: job is no longer listed. The item is the last snapshot we saw.
- `firstSeenAt` = when the job was first seen by this monitor.
- State lives in the named key-value store **`ats-jobs-feed-state`**, one record per board plus the cached company → board matches. Use a different `stateKey` per watchlist.
- If a board fails to load (API down), its state is left untouched, so you won't get false "removed" events.
- If a run is cut short by `maxItems` or your spending limit, that board's state isn't saved, and the missed changes are emitted next run.
- Filters apply to emitted changes, so you can change filters without corrupting history.

### Pricing (pay per result)

You pay **$2 per 1,000 results** ($0.002 per item in the dataset), plus a tiny Actor-start fee of $0.00005 per run.

- **Full mode:** one result = one open job.
- **Monitor mode:** one result = one new, updated or removed job. Runs with no changes cost only the start fee.
- Companies whose board can't be found produce no items, so they cost nothing.

Example: 50 companies with 2,000 open jobs = **about $4** for a full pull. A daily monitor of the same 50 companies with ~40 changes per day costs about **$0.08 per run**.
Use `maxItems` / `maxJobsPerCompany` to cap a run. The Actor also respects your run's maximum charge limit and stops cleanly when it is reached.

### FAQ

**A company wasn't found. What now?** Check `OUTPUT` for the reason. The company may use a different ATS (Workday, iCIMS, SmartRecruiters … not supported yet), host jobs on a custom site, or block automated page fetches. Pass the board URL or `greenhouse:/lever:/ashby:slug` directly if you know it.

**The wrong board was matched for a plain name.** Name probing can't always tell two companies with the same name apart. Use a domain or board URL for those companies.

**Why is `compensation` empty for Greenhouse jobs?** Greenhouse's list endpoint doesn't include pay ranges (they need one extra request per job). Lever and Ashby include salary when the company publishes it.

**Do I need proxies?** No. These are public, unauthenticated JSON APIs.

### Limitations

- Supports Greenhouse, Lever (global and EU) and Ashby. Other ATSs aren't covered.
- Greenhouse: no compensation; `team` is the second department level when present. Prospect / "General application" posts are skipped.
- Lever and Ashby don't expose an `updatedAt` timestamp.
- Ashby jobs marked `isListed: false` (direct-link only) are skipped.
- Careers-page detection reads static HTML only. Boards loaded purely by client-side JavaScript from a non-obvious URL may need the board URL as input.
- Some company websites block automated requests; the Actor then falls back to name probing.

### Legal and terms

The Actor only calls the ATS vendors' **documented public job-board endpoints**, which publish data that companies have deliberately made public:

- Greenhouse Job Board API: "Job Board data is publicly available, so authentication is not required for any GET endpoints" ([docs](https://developers.greenhouse.io/job-board.html)).
- Lever Postings API: "all job postings in the published state are publicly viewable. These jobs may be scraped by third parties" ([docs](https://github.com/lever/postings-api)).
- Ashby public Job Postings API ([docs](https://developers.ashbyhq.com/docs/public-job-posting-api)).

It identifies itself in the User-Agent, keeps at most 5 concurrent requests per host, follows Lever's 1-second crawl delay, backs off on 429/5xx, respects `robots.txt` on company websites, and only fetches the companies you list (no crawling). Job postings contain no personal data beyond what employers publish. You are responsible for how you use the data, including any rules in the destination where you republish it.

### Run locally (no Apify account needed)

```bash
npm install
mkdir -p storage/key_value_stores/default
echo '{"companies":["ashby:ashby","leverdemo.com"]}' > storage/key_value_stores/default/INPUT.json
node src/main.js            # results in storage/datasets/default, OUTPUT in storage/key_value_stores/default
npm test                    # offline tests on recorded fixtures
```

# Actor input Schema

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

Any mix of: company domains (acme.com), careers-page URLs (https://acme.com/careers), ATS board URLs (https://jobs.lever.co/acme, https://job-boards.greenhouse.io/acme, https://jobs.ashbyhq.com/acme), explicit boards (greenhouse:acme, lever:acme, lever-eu:acme, ashby:acme) or plain company names (Acme Inc). Domains/URLs/explicit boards are the most reliable; plain names are matched by probing likely board names.

## `onlyNewSinceLastRun` (type: `boolean`):

Emit only jobs that are new, updated or removed since the previous monitor run with the same state key. The first run emits every job as 'new' (baseline). State is kept in the named key-value store 'ats-jobs-feed-state'.

## `stateKey` (type: `string`):

Name of this monitor. Use a different key for each independent schedule/watchlist so their histories don't mix. Letters, digits, '-' and '\_'.

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

Keep jobs whose title contains at least one of these strings (case-insensitive).

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

Drop jobs whose title contains any of these strings (case-insensitive).

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

Keep jobs where at least one location contains one of these strings, e.g. 'New York', 'Germany', 'Remote'.

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

Keep only jobs marked fully remote (isRemote = true). Jobs with unknown remote status are dropped.

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

Keep jobs whose department or team contains one of these strings.

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

Keep only jobs first posted within the last N days. Leave empty for no limit.

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

Add descriptionText to each job.

## `includeDescriptionHtml` (type: `boolean`):

Add descriptionHtml to each job.

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

Emit at most this many jobs per company (newest first). Leave empty for all.

## `maxItems` (type: `integer`):

Stop after emitting this many items in total. Leave empty for no limit.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:airbnb",
    "lever:spotify",
    "ashby:notion"
  ],
  "onlyNewSinceLastRun": false,
  "stateKey": "default",
  "remoteOnly": false,
  "includeDescription": false,
  "includeDescriptionHtml": false
}
```

# 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": [
        "greenhouse:airbnb",
        "lever:spotify",
        "ashby:notion"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("unrealevents22/agent-bridge").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": [
        "greenhouse:airbnb",
        "lever:spotify",
        "ashby:notion",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("unrealevents22/agent-bridge").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": [
    "greenhouse:airbnb",
    "lever:spotify",
    "ashby:notion"
  ]
}' |
apify call unrealevents22/agent-bridge --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,unrealevents22/agent-bridge"
        }
    }
}
```

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/a5FUZsl7SEBdzFVLf/builds/9TSWOI8xkn3M03YJo/openapi.json
