# Company Jobs Finder & New-Jobs Monitor (11 ATS) (`southern-data/company-jobs-finder`) Actor

Give company websites, get every open job from their job board: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio, Teamtailor, BambooHR, Workday, Breezy. Working filters, no duplicates, and a monitor mode that returns only new jobs.

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

## Pricing

from $1.50 / 1,000 job returneds

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

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

## What's an Apify Actor?

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

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

## How to integrate an Actor?

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

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

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

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

# README

## Company Jobs Finder & New-Jobs Monitor (11 ATS)

Give it a list of **company websites**. For each one it finds the company's job board (Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio, Teamtailor, BambooHR, Workday, Breezy) and returns **every open job as one clean row**. Or switch to **monitor mode** and schedule it: each run returns **only the jobs that are new since the last run**.

- **Domain in, jobs out.** `linear.app`, `https://notion.com`, a careers page, or a board link such as `https://jobs.lever.co/acme`. One company list, all providers, one output format.
- **Only official public job-board feeds.** The same feeds the companies use to publish their jobs on their own careers pages. No logins, no bypassing protection, no LinkedIn or Indeed scraping.
- **Filters that work on every board.** Title, excluded words, location, department, workplace type (remote/hybrid/onsite) and posting date are applied by this Actor to every job, whatever the provider supports. The run summary tells you how many jobs each filter removed, so you can see it working.
- **No duplicates.** Repeat postings of the same job (one per hiring slot) are merged into one row by default (`duplicatesMerged` says how many), and a board reached from two inputs is listed once.
- **New-jobs monitor.** Remembers which jobs it has already seen in a named key-value store in your account, so a daily schedule plus a webhook or Slack/email integration gives you "new jobs at these companies" alerts.

### Pricing

| Event | Price | When |
|---|---|---|
| `company-checked` | **$0.001** | A company's job board was found and read |
| `job` | **$0.0015** | Each job row returned (all-jobs mode) |
| `new-job` | **$0.003** | Each new job returned in monitor mode |

**1,000 jobs from 50 companies cost $1.55.** A daily monitor of 100 companies costs $0.10 per run plus $0.003 per new job. **Free:** companies where no board is found, invalid input, duplicate inputs, jobs removed by your filters, and the jobs a monitor has already shown you. The log shows the cost before the run starts. Set **Maximum cost per run** and the Actor stops cleanly there (the rest of the list goes to the `REMAINING` record, free).

### Quick start

1. Click **Try for free**.
2. Paste company websites, one per line (or comma-separated), **or** paste a link to a CSV / TXT file or a Google Sheet (shared as "Anyone with the link can view") in **companiesFileUrl**. A column named `website`, `domain`, `url`, `company website`, `careers url` (or similar) is used automatically.
3. Optional: add filters, e.g. title `engineer`, location `berlin, germany, remote`.
4. Click **Start**. Open the **Output** tab: *Jobs*, *Job details*, *Companies (not found / no match)* or *All fields*. Download as CSV, Excel or JSON.

**Monitor:** set **What to return** to `new_jobs`, give it a **Monitor name** (e.g. `my-target-accounts`), and add a daily schedule. The first run saves the current jobs as the baseline (only `company-checked` is charged; turn on *Return current jobs on the first run* if you want them too). Every later run returns only jobs that weren't there before.

Via API:

```bash
curl -X POST "https://api.apify.com/v2/acts/southern-data~company-jobs-finder/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"companies":["linear.app","notion.com","https://jobs.lever.co/palantir"],"titleKeywords":["engineer"],"locationKeywords":["remote","europe"]}'
```

Shorthand works too when you already know the board: `greenhouse:figma`, `lever:palantir`, `ashby:linear`, `workday:tenant/Site@wd5`.

### Example row

A real row from a monitor run (`new_jobs` mode, input `ashby:ramp`), trimmed:

```json
{
  "rowType": "job",
  "input": "ashby:ramp",
  "company": "Ramp",
  "ats": "ashby",
  "boardUrl": "https://jobs.ashbyhq.com/ramp",
  "detectionMethod": "ats_url_input",
  "confidence": "high",
  "status": "ok",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "jobKey": "2eh88o1bsuoru",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "country": "United States",
  "workplaceType": "hybrid",
  "employmentType": "Full-Time",
  "postedAt": "2026-04-07T17:12:35.753Z",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salaryText": "$211.4K – $290.6K • Offers Equity",
  "duplicatesMerged": 0,
  "isNew": true,
  "firstSeenAt": "2026-11-01T03:11:26.006Z"
}
```

Every row has the same 41 fields (missing values are `null`), so CSV exports line up. `jobKey` is stable across runs, so you can deduplicate in your own database. Companies with nothing to return get one `rowType: "company"` row explaining why (`board_not_found`, `no_open_jobs`, `no_matching_jobs`, `invalid_input`, `duplicate_board`, `not_processed`, `error`), all free.

### How it finds the job board

1. **Board links you give** are used as they are.
2. **The company website**: the homepage and up to 6 careers pages are read (robots.txt respected) for links or embeds of a supported provider. This is the most reliable path (`detectionMethod: link_on_site`, `confidence: high`).
3. **Name lookup**: if the site links no board, the company name is tried on each provider. A guessed board is **only used when it really belongs to the company**: its postings link to the company's domain, or its name matches and the website mentions that provider (`slug_match`). A board that merely shares the name is rejected.

### How accurate is it?

Tested in November 2026 on real companies, with default settings:

| Test | Result |
|---|---|
| Tuning set: 40 companies with a public board on a supported provider | **35 of 40 found** (20 via a link on the website, 15 via name lookup), **0 wrong boards** |
| Held-out set: 28 companies with open jobs, not used while building | **21 of 28 found, 0 wrong boards**. Misses: 3 use their own or an unsupported system (e.g. Rippling, Revolut's site blocks bots), 2 use a board name that can't be guessed (e.g. Trade Republic → `traderepublicbank`), 1 empty board, 1 unclear |
| Companies with no supported board (should return nothing) | **9 of 9 correctly returned `board_not_found`** — no false matches |

So expect roughly **3 in 4 company websites to resolve** on tech/startup lists, and **no wrong company's jobs**. When the website doesn't link its board and the name can't be guessed, paste the board link (or shorthand) instead: those always work.

### What's in each field, per provider

Fields are only as complete as the provider's public feed:

| | Dept. | Country | Workplace type | Posted date | Salary | Description |
|---|---|---|---|---|---|---|
| Greenhouse | ✅ | — | from location text only | ✅ | — | ✅ |
| Lever | ✅ + team | ✅ | ~60% | ✅ | when published | ✅ |
| Ashby | ✅ | ✅ | ✅ | ✅ | when published | ✅ |
| Workable | ✅ | ✅ | ✅ | ✅ | — | ✅ |
| SmartRecruiters | ✅ | ✅ | ✅ | ✅ | — | ✅ (1 request per job) |
| Recruitee | ✅ | ✅ | ✅ | ✅ | when published | ✅ |
| Personio | ✅ | — | partly | ✅ | — | ✅ |
| Teamtailor | partly | ✅ | ✅ | ✅ | — | ✅ |
| BambooHR | ✅ | ✅ | ✅ | read per job | when published | ✅ (1 request per job) |
| Workday | — | per job | rarely | "Posted N days ago" → date; exact date per job | — | ✅ (1 request per job) |
| Breezy | ✅ | ✅ | — | ✅ | ✅ | — |

### What it does NOT do (please read)

- **It needs a website or board link, not a company name.** `Stripe` alone is rejected (free) with a message; `stripe.com` works.
- **Workday is best-effort.** Workday boards are found only when the website links a `myworkdayjobs.com` board. Big companies with a custom career site on top of Workday (e.g. Salesforce, Adobe) are missed; paste the board link (`https://acme.wd5.myworkdayjobs.com/External`) if you have it. Only the "Posted N days ago" text is in the list; exact dates and all locations are read per job when your filters need them (up to 300 per company).
- **Board names that can't be guessed are missed** unless the website links them (see the accuracy table).
- **One board per company.** The first supported board found is read; a second board (e.g. a separate one for a subsidiary or country) is not. Add it as its own input.
- **Unsupported systems** (company-built career sites, Rippling, iCIMS, Taleo, SuccessFactors, Join, etc.) return `board_not_found`, free.
- **Descriptions** are capped at 20,000 characters. On boards that need one request per job (Workday, BambooHR, SmartRecruiters) they are read for up to 300 matching jobs per company; the COMPANIES record notes when that limit was hit.
- **Monitor mode notes:**
  - A new job that your filters remove is remembered as seen; it won't appear later if you change the filters. Change the **Monitor name** to start a fresh baseline.
  - Don't run the same monitor twice at the same time (the second run may repeat jobs).
  - Jobs not seen for 90 days are forgotten, so a job reposted after 90+ days counts as new.
  - Without a Monitor name, the memory is tied to the exact company list; editing the list starts a new memory. Set a name to avoid that.
- **`postedWithinDays` drops undated jobs** (rare; counted in `RUN_SUMMARY.jobsRemovedByFilter`).
- **Keyword matching is by word start**: `engineer` matches *Engineering Manager*; `sales` also matches *Salesforce Admin* — add `salesforce` to *Job title does NOT contain* if you don't want those.

### Records in the key-value store

- `RUN_SUMMARY`: counts per status, jobs removed by each filter, duplicates merged, events charged, the monitor store used.
- `COMPANIES`: one entry per input: status, board, detection method, jobs on board / matched / returned, notes.
- `REMAINING`: inputs not processed (max cost reached or `maxCompanies`), ready to re-run.

### Speed

Fast: most boards are read in one or a few requests. Measured in the Apify cloud: **25 companies in 13 seconds**; 3,000 jobs from Bosch and Workday in 34 seconds. Detail requests (Workday/BambooHR dates, per-job descriptions) are slower: ~45 seconds for a filtered run over 6 large boards with descriptions. 512 MB memory is plenty (a 4,400-job board used 130 MB).

Bad input never fails the run: if nothing usable is given, the run ends with a clear message and **$0 charged**.

### Good to combine with

- **[Website Contact Finder](https://apify.com/southern-data/website-contact-finder)**: emails and phones for the companies that are hiring.
- **[Domain Checker](https://apify.com/southern-data/domain-intel)**: tech and email setup for the same domains.

### Responsible use

Job postings are published by the companies to be shown widely, and this Actor uses the providers' official public job-board feeds. Please link back to the original `jobUrl` when you republish a job, and respect each provider's terms if you build a job board on top.

### Support

Found a company whose board we missed or misread? Open an issue on the **Issues** tab with the website. Bug reports are usually fixed within a few days.

# Actor input Schema

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

Company websites (stripe.com), careers pages, or job-board links (https://jobs.lever.co/acme, https://boards.greenhouse.io/acme). Shorthand also works: greenhouse:acme, lever:acme, ashby:acme, workday:tenant/site@wd5. We find the job board linked from the website; if none is linked, we look for one under the company's name on each provider and only use it when it is verified to belong to this company.

## `companiesFileUrl` (type: `string`):

Link to a CSV, TXT or Google Sheet (shared as "anyone with the link") with one company website or job-board link per row. A column named website, domain, url, careers or company url is used when present.

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

all_jobs: every open job (that passes your filters). new_jobs: only jobs that appeared since the previous run of this monitor — schedule it daily or weekly. On the first run of new_jobs, current jobs are saved as the baseline and nothing is returned (unless you switch on "Return current jobs on the first run").

## `monitorName` (type: `string`):

Name for the memory of already-seen jobs (kept in a named key-value store in your account: jobs-monitor-<name>). Set it so you can edit the company list or filters without losing history. If empty: a saved task uses its own memory; otherwise the memory is tied to the exact company list.

## `includeExistingOnFirstRun` (type: `boolean`):

On a monitor's first run for a company, also return its current jobs (charged at the normal per-job price, isNew = false). Off: the first run only saves the baseline.

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

Keep only jobs whose title contains one of these words (any case, matches word starts: "engineer" also finds "Engineering Manager"). Empty = all titles.

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains any of these words, e.g. intern, werkstudent.

## `locationKeywords` (type: `array`):

Keep only jobs whose location (or any of its locations, or country) contains one of these words, e.g. berlin, germany, remote. "remote" also matches jobs the board marks as remote.

## `departmentKeywords` (type: `array`):

Keep only jobs in a matching department or team, e.g. engineering, sales.

## `workplaceTypes` (type: `array`):

Keep only these workplace types. "unknown" = the board does not say. Empty = all.

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

Keep only jobs published in the last N days. Jobs whose board gives no date are dropped (and counted in RUN_SUMMARY). 0 = no date filter.

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

Add the job description as plain text. Most boards include it in one request; Workday, BambooHR and SmartRecruiters need one request per job, so there descriptions are read for the jobs you get (after filters), up to 300 per company. Breezy boards give no description. Long descriptions are cut at 20,000 characters.

## `mergeDuplicatePostings` (type: `boolean`):

Merge repeat postings of the same job on one board (same title, location and department — e.g. one requisition per hiring slot). The row shows how many were merged. Boards reached from two of your inputs are always listed once.

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

Stop reading a board after this many jobs (0 = no limit). Filters apply after this limit, so keep it high when you filter.

## `maxCompanies` (type: `integer`):

Check only the first N companies of your list (0 = all). The rest go to the REMAINING record.

## `detectFromWebsite` (type: `boolean`):

Read the homepage and careers pages (respecting robots.txt) to find the linked job board.

## `guessBoards` (type: `boolean`):

When the website links no board, try the company name on each provider. A board is only used when its postings link to the company's domain, or its name matches and the website or board size confirms it.

## `maxPagesPerSite` (type: `integer`):

Pages read on each company website to find the job board.

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

How many companies are checked at the same time.

## `selfTest` (type: `boolean`):

Run the built-in health check instead of a normal run: no rows, nothing charged; the run fails if any check fails.

## Actor input object example

```json
{
  "companies": [
    "linear.app",
    "notion.com",
    "https://jobs.lever.co/palantir"
  ],
  "mode": "all_jobs",
  "includeExistingOnFirstRun": false,
  "postedWithinDays": 0,
  "includeDescription": false,
  "mergeDuplicatePostings": true,
  "maxJobsPerCompany": 1000,
  "maxCompanies": 0,
  "detectFromWebsite": true,
  "guessBoards": true,
  "maxPagesPerSite": 6,
  "maxConcurrency": 8,
  "selfTest": false
}
```

# Actor output Schema

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

One row per job. Views: Jobs, Job details, Companies & boards, All fields.

## `companies` (type: `string`):

COMPANIES record: every input with its board, detection method, job counts and status.

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

RUN_SUMMARY record: counts per status, jobs removed per filter, duplicates merged, charged events, monitor store name, any stop reason.

## `remaining` (type: `string`):

REMAINING record: inputs not checked in this run (spend cap or maxCompanies). Only present when some were left.

## `selfTest` (type: `string`):

SELF_TEST record: only present when the Actor runs with selfTest: true.

# 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": [
        "linear.app",
        "notion.com",
        "https://jobs.lever.co/palantir"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("southern-data/company-jobs-finder").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": [
        "linear.app",
        "notion.com",
        "https://jobs.lever.co/palantir",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("southern-data/company-jobs-finder").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": [
    "linear.app",
    "notion.com",
    "https://jobs.lever.co/palantir"
  ]
}' |
apify call southern-data/company-jobs-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,southern-data/company-jobs-finder"
        }
    }
}
```

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/yu9i8x7hSIGBQg6H7/builds/LUyfpZAUGCk1pqqs3/openapi.json
