# AI/ML Jobs Scraper - Greenhouse, Lever, Ashby (`kotaro001/ai-jobs-daily`) Actor

Scrape AI, ML and data job postings from public Greenhouse, Lever and Ashby boards. Every result is validated, deduplicated and tagged AI-core vs. AI-mention, with a stable versioned schema and built-in source-change monitoring. Also callable from Claude/Cursor via the Apify MCP server.

- **URL**: https://apify.com/kotaro001/ai-jobs-daily.md
- **Developed by:** [Tuyen Tran](https://apify.com/kotaro001) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 validated results

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?

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

**Validated AI, ML and data job postings from public ATS boards — Greenhouse, Lever and Ashby — delivered as a clean, stable, versioned dataset.**

### What does AI Jobs Daily do?

AI Jobs Daily collects AI, machine-learning and data job postings straight from public applicant tracking system (ATS) boards and turns them into analysis-ready records. Every posting passes a validation gate before it reaches the dataset, is deduplicated across boards, and is tagged so you can separate true AI roles from roles that only mention AI.

It is built for recruiters, hiring-market analysts, job-board operators and AI agents that need a reliable stream of hiring data instead of raw scraped HTML.

### Why use this AI jobs scraper?

- 🎯 **AI-core vs. AI-mention classification** — `ai_core` and `mentions_ai` flags plus an `ai_skills` list (llm, rag, pytorch...), so you can buy exactly the signal you need.
- ✅ **Validated results only** — a strict validation gate runs on every row; invalid postings and partially broken boards never ship to the dataset.
- 🧩 **One schema, many boards** — Greenhouse, Lever and Ashby (plus experimental Workable) behind a single, stable, versioned schema. Field names change only with a version bump.
- 📅 **Built for daily feeds** — schedule it on Apify, call it via the Apify API, or wire it into Make, Zapier or n8n. Runs come with the full Apify platform: monitoring, scheduling, integrations and export to JSON, CSV or Excel.
- 🩺 **Self-monitoring** — every run tracks per-board field fill rates; if a source starts silently losing fields, the run log raises an alarm.
- 🔒 **Public data only** — no login walls, no personal data (no names, emails or phone numbers), no proxies required.

### Hiring Intent Signals — for Recruiters & GTM Teams

This feed is not a job board. It is a hiring-pressure monitor for recruiters, headhunters and GTM teams.

- **Stuck senior searches** — Senior or Staff roles above $200k that have been open 35–55 days (`days_open`), or were just reposted (`is_reposted: true`), are signals that the company's own pipeline is not converting. These are accounts worth a headhunting outreach (typical fee: 20–25% of annual salary).
- **Where the budget is going** — `ai_skills` and the number of open roles per company show which stacks they are scaling (vLLM, Ray, Kubernetes, ...), so you can time outreach to the hiring wave instead of guessing.
- **Daily and queryable** — schedule the Actor or pull it through the Apify API, Make, Zapier or n8n to keep a live list of accounts with rising hiring pressure.

**n8n workflow template:** [AI Hiring Signal Feed](https://gist.github.com/tuyentran4992/d57357898c335ed742d7064f10b5e1f2) — daily hiring signals from 60 AI companies to Telegram.

### How to use AI Jobs Daily

1. Create a free [Apify account](https://console.apify.com/sign-up) — new accounts include monthly usage credits.
2. Open this Actor and keep the default input (a seeded list of AI-heavy boards) or paste your own company list.
3. Press **Start** and wait — a full default run across the 60-board seed list takes about two minutes.
4. Take the results from the **Output** tab, or pull them via the [Apify API](https://docs.apify.com/api/v2).

![AI Jobs Daily input in Apify Console](https://api.apify.com/v2/key-value-stores/aSORkWBO2Rjp64MXk/records/readme-input.jpg)

*The Actor input in Apify Console — the defaults are ready to run.*

### Integrations — use from Claude Desktop, Cursor, or any MCP client

Connect this Actor to any MCP (Model Context Protocol) client through the Apify MCP server, and your AI assistant can query the job feed directly — no glue code.

**Option A — local MCP server (stdio):**

```json
{
  "mcpServers": {
    "apify-ai-jobs": {
      "command": "npx",
      "args": ["-y", "@apify/actors-mcp-server", "--tools", "kotaro001/ai-jobs-daily"],
      "env": { "APIFY_TOKEN": "YOUR_APIFY_TOKEN" }
    }
  }
}
```

**Option B — hosted MCP server (streamable HTTP, no local process):**

Point your client at `https://mcp.apify.com?tools=kotaro001/ai-jobs-daily` and send your Apify token in an `Authorization: Bearer <token>` header.

Both options expose this Actor as a tool, plus small helpers to fetch run results — and no actor discovery or generic calling tools — so the agent interface stays stable across server updates.

Then ask your agent, for example:

> "Find active AI-core ML Engineer jobs in the US with salary floor above $200k, and flag any that look like ghost postings."

Your agent will call this Actor and work with the structured results (`title`, `company`, `comp_min_usd` / `comp_max_usd`, `seniority`, `workplace_type`, `visa_sponsorship`, `ai_core`, `is_ghost_job`, `is_reposted`, `first_seen` / `last_seen`).

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `companies` | array | `[]` | Company boards to scrape; leave empty to use the built-in seed list. Each item is either `{name, ats, slug}` or a domain / careers URL string — e.g. `openai.com` or `https://anthropic.com` — auto-detected across Ashby, Greenhouse, Lever and Workable. |
| `max_companies` | integer | `0` | Cap how many companies to process — handy for cheap test runs. `0` means no cap. |
| `only_ai_core` | boolean | `true` | When true, only AI-core jobs are returned. Set to false to also include jobs that merely mention AI. |
| `runMode` | string | `daily_active` | What goes into the dataset: `daily_active` — today's validated jobs (default); `lifecycle_changes_only` — only jobs first seen today plus jobs that just disappeared; `full_history_dump` — the full lifecycle history kept in your account. See Run modes below. |

Example input:

```json
{
  "companies": [
    { "name": "OpenAI", "ats": "ashby", "slug": "openai" },
    "stripe.com"
  ],
  "max_companies": 10,
  "only_ai_core": true,
  "runMode": "daily_active"
}
```

### Output

Each dataset item is one validated job posting with 29 core fields: identity (`job_id`, `title`, `company`, plus `company_domain` and `company_linkedin_url` — resolved from a static map of the 60 seed companies; the domain is `null` when the company is unknown, and the LinkedIn URL falls back to a slug derived from the company name, `ats`, `url`), location (`location_raw`, `location_city`, `location_country`, `workplace_type` — remote / hybrid / onsite, `null` when unclear; `country_codes` — ISO-3166 alpha-2 codes such as `["US","GB"]`, empty when unclear; `remote_policy` stays as the legacy alias), timing (`posted_date`, `first_seen_at`, `last_seen_at`), details (`department`, `employment_type`, `description_text`), salary (`salary_raw`, `comp_min_usd`, `comp_max_usd` — whole-dollar annual USD; EUR/GBP converted via a static rate table shipped with the Actor, hourly rates ×2080, monthly rates ×12; other currencies left empty), `seniority` (intern / junior / mid / senior / staff / lead; empty when no rule matches), and the AI signal (`ai_core`, `mentions_ai`, `ai_skills`), plus `visa_sponsorship` — a text-scan **heuristic** (true when the posting advertises visa sponsorship / H1B transfer / relocation or immigration support; false when it cannot sponsor or requires US citizenship / Green Card / ITAR; null when the text says nothing; a hint, not a verdict). Nine lifecycle fields ride along on every row — `stable_id`, `first_seen`, `last_seen`, `lifecycle_status`, `original_first_seen`, `repost_count`, `is_reposted`, plus `days_open` (whole days from first_seen to last_seen) and `is_ghost_job` — a reference flag (heuristic). Reposts keep their original `first_seen`, so `days_open` and `is_ghost_job` always reflect the true age; see Run modes below. A dataset schema comes with the Actor, with an overview view in the Output tab.

**Daily archive (named dataset).** Every run also appends its full result set to the named dataset `ai-jobs-daily-archive`, with a `snapshot_date` field (YYYY-MM-DD) on every row. Filter on `snapshot_date` to isolate one day's snapshot. Archive writes are not billed as pay-per-event results, and an archive failure is logged as a warning without failing the run.

Example item (real output, description trimmed):

```json
{
  "schema_version": "1.6.0",
  "job_id": "a01fc9ed-de08-4326-9206-4ee3f08000dc",
  "title": "Machine Learning Engineer, Implicit Signals",
  "company": "OpenAI",
  "company_domain": "openai.com",
  "company_linkedin_url": "https://www.linkedin.com/company/openai",
  "ats": "ashby",
  "url": "https://jobs.ashbyhq.com/openai/a01fc9ed-de08-4326-9206-4ee3f08000dc",
  "location_raw": "San Francisco",
  "location_city": "",
  "location_country": "",
  "remote_policy": "hybrid",
  "workplace_type": "hybrid",
  "country_codes": ["US"],
  "visa_sponsorship": null,
  "posted_date": "2026-10-13",
  "employment_type": "FullTime",
  "department": "Applied AI",
  "description_text": "About the Team\nOur team builds classification and evaluation systems that give post-training and research teams fast, reliable feedback on model behavior. ... [trimmed]",
  "salary_raw": "$381K – $510K • Offers Equity",
  "comp_min_usd": 381000,
  "comp_max_usd": 510000,
  "seniority": null,
  "ai_core": true,
  "mentions_ai": true,
  "ai_skills": ["distillation", "fine-tuning"],
  "status": "open",
  "first_seen_at": "2026-10-14T07:45:46Z",
  "last_seen_at": "2026-10-14T07:45:46Z",
  "stable_id": "8b265edbc3ad521a",
  "first_seen": "2026-10-14",
  "last_seen": "2026-10-14",
  "lifecycle_status": "active",
  "days_open": 0,
  "is_ghost_job": false,
  "original_first_seen": "2026-10-14",
  "repost_count": 0,
  "is_reposted": false
}
```

Example item — compact output:

With `compactOutputForMcp: true` every structured field is kept, but `description_text` is omitted — here is one real row from a live run (row count and billing are unchanged):

```json
{
  "schema_version": "1.6.0",
  "job_id": "5f43b884-459e-4a5f-b57c-2c20d48dcbc0",
  "title": "Principal Machine Learning Engineer, 3D Geometric Vision",
  "company": "Wayve",
  "company_domain": "wayve.ai",
  "company_linkedin_url": "https://www.linkedin.com/company/wayve",
  "ats": "ashby",
  "url": "https://jobs.ashbyhq.com/wayve/5f43b884-459e-4a5f-b57c-2c20d48dcbc0",
  "location_raw": "Sunnyvale, California USA",
  "location_city": "",
  "location_country": "",
  "remote_policy": "hybrid",
  "workplace_type": "hybrid",
  "country_codes": ["US"],
  "visa_sponsorship": true,
  "posted_date": "2026-10-05",
  "employment_type": "FullTime",
  "department": "AV Engineering",
  "salary_raw": "$370,040 – $419,800",
  "comp_min_usd": 370040,
  "comp_max_usd": 419800,
  "seniority": "staff",
  "ai_core": true,
  "mentions_ai": true,
  "ai_skills": ["pytorch", "rag"],
  "status": "open",
  "first_seen_at": "2026-10-15T11:11:01Z",
  "last_seen_at": "2026-10-15T11:11:01Z",
  "stable_id": "d729796a56529a6e",
  "first_seen": "2026-10-15",
  "last_seen": "2026-10-15",
  "lifecycle_status": "active",
  "days_open": 0,
  "is_ghost_job": false,
  "original_first_seen": "2026-10-15",
  "repost_count": 0,
  "is_reposted": false
}
```

The **Sample output** tab on this Actor's page shows a full sample dataset — 122 jobs captured from 5 boards in a single run.

![AI Jobs Daily output in Apify Console](https://api.apify.com/v2/key-value-stores/aSORkWBO2Rjp64MXk/records/readme-output.jpg)

*A full run's dataset in the Console Output tab — one row per validated posting.*

### Run modes and job lifecycle

Every job carries a stable id and a lifecycle: the first day it was seen (`first_seen`), the last day it was seen on its board (`last_seen`) and a status — `active` or `removed`. Removed jobs are kept as tombstones, so a posting that returns later keeps its original `first_seen`. Rows also carry two derived fields: `days_open` (whole days from `first_seen` to `last_seen`) and `is_ghost_job` — a **reference flag (heuristic)**: it is true only for postings that are still `active` AND have been open for more than 60 days, which often means a stale or evergreen listing. Treat it as a hint, not a verdict. Three more fields track reposts: `original_first_seen` (the day the posting first appeared — kept across reposts), `repost_count` and `is_reposted`. A repost is a new listing whose content fingerprint — company + normalized title + location + skills + salary band — matches a removed listing of the same company from the last 30 days; the new listing inherits the original `first_seen`. Detection is deliberately conservative: a missed repost is preferred over a wrong merge.

Your first run is never blind: at the start of every run the Actor fetches the shared public lifecycle map (a key-value record readable without any account) and uses it as a baseline. A job first seen weeks before your run still carries its true `first_seen` instead of looking brand new. If that fetch fails for any reason, the run simply continues without the map — it can never break your run.

`runMode` decides what lands in the dataset:

- `daily_active` (default) — today's validated jobs, each enriched with its lifecycle fields. The classic daily snapshot.
- `lifecycle_changes_only` — only jobs first seen today plus jobs that just disappeared from their boards. Perfect for change alerts and cheap monitoring.
- `full_history_dump` — every lifecycle record kept in your account: active jobs plus removed tombstones. You pay per row returned.

Change feed only:

```json
{
  "companies": [],
  "runMode": "lifecycle_changes_only"
}
```

One-off full history export:

```json
{
  "companies": [],
  "runMode": "full_history_dump"
}
```

### Pricing

AI Jobs Daily uses pay-per-event pricing — you pay for validated results, nothing else.

- **From $2.00 to $2.50 per 1,000 validated jobs** — the exact rate depends on your Apify subscription plan (per 1,000 results: Free $2.50 / Starter $2.30 / Scale $2.10 / Business $2.00). Each validated dataset row bills one event.
- **Small Actor-start fee** — from $0.00002 per event, depending on plan and allocated memory (one event per GB; about $0.0002 per run at the default 4 GB on the Free plan, less on paid plans).
- Invalid rows and broken boards are never written to the dataset, so you never pay for them.
- You only pay the events above — there is no additional platform-usage charge.
- With `lifecycle_changes_only` a typical daily run costs a fraction of a cent (a handful of events); `full_history_dump` charges one event per history row returned.

Quick math (Free-plan rate): 100 validated jobs → $0.25; 1,000 → $2.50; 10,000 → $25.00. On paid plans the same runs cost up to 20% less. You can cap the maximum spend per run in Apify Console; the Actor respects the cap and stops cleanly when it is reached.

### Reliability and self-healing

Public job boards change their APIs and layouts without notice. AI Jobs Daily is built to notice:

- A fetch error or a validation-failure spike on a board skips that board for the run — zero partial rows — and records an incident for maintenance.
- Per-board field fill rates are computed on every run; a field that used to fill more than 50% of rows and drops below 10% triggers a warning in the run log.
- The classifier is rules-based and deterministic, so results are reproducible run after run.

### FAQ

#### Which companies can I scrape?

Any company with a public Greenhouse, Lever or Ashby board — add it through the input in seconds, no code required. Workable boards are supported experimentally.

#### How many jobs do I get per run?

It depends on your company list. The default seed list — 60 AI-heavy boards — returns around 690 AI-core jobs per run; smaller subsets scale down from there.

#### Are only AI roles included?

By default yes (`only_ai_core: true`). Set it to false to also get jobs that merely mention AI — the `ai_core` and `mentions_ai` flags let you filter either way.

#### How often should I run it?

Daily is a good fit for a hiring-market feed. Runs are deduplicated, so frequent runs only add new postings.

#### Where does the data come from?

Only public, official application boards. No logins, no personal data, no scraping of private profiles.

#### Can I trust `first_seen` from my very first run?

Yes. The Actor ships with a shared public lifecycle map built from its full tracking history, so even on your first run a job can carry a `first_seen` from days or weeks ago. The `first_seen_at`/`last_seen_at` fields stay as the raw timestamps of your own runs.

#### Why did my run stop before delivering all results?

Apify caps every run at the remaining credit of the account running it. A full ai-mention run (~10,000 rows) costs about $25 at $2.50/1,000 — on a Free plan ($10/month) the run stops when your credit runs out and you receive a partial dataset. Top up credit or use a paid plan for large runs. This is platform behavior, not an actor bug.

### Support

Questions, suggestions, or a source you'd like added? Open an issue on this Actor's page in Apify Console — the Actor is monitored and maintained continuously.

### For developers

The pipeline is deterministic by design: fetch → parse → normalize → classify → validate → dedupe → output. Sources are plug-in adapters (one file per ATS), classification and validation are pure functions covered by unit tests, and the self-healing path only ever proposes fixes — it never edits running code. Built in Python on the Apify SDK.

# Actor input Schema

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

Override the seed list. Each item is either an object with name, ats (greenhouse|lever|ashby|workable) and slug, or a company domain / careers URL string (e.g. 'openai.com' or 'https://jobs.ashbyhq.com/openai'). String entries are auto-detected across Ashby, Greenhouse, Lever and Workable; an entry that cannot be resolved is reported as a clear error and skipped, and the rest of the list still runs. Leave empty to use config/companies.yaml.

## `max_companies` (type: `integer`):

Cap the number of companies (0 = no cap). Useful for cheap test runs.

## `only_ai_core` (type: `boolean`):

When true, output only jobs classified as AI-core. When false, include jobs that merely mention AI.

## `runMode` (type: `string`):

What to write into the dataset. daily_active (default): today's validated jobs, each enriched with standard first_seen/last_seen. lifecycle_changes_only: only jobs first seen today plus jobs that just disappeared from their boards. full_history_dump: the full lifecycle history kept in your account (active + removed records).

## `aiCategoryFilter` (type: `string`):

Server-side filter on the output rows. 'all' (default) keeps every row that passed the other filters; 'ai-core' keeps only roles that are themselves AI/ML/Data; 'ai-mention' keeps only postings that mention AI without being AI-core roles (non-core rows are fetched automatically for this mode). You are billed for the rows actually written.

## `minSalaryUsd` (type: `integer`):

Keep only jobs whose parsed minimum annual salary (comp_min_usd) is at least this value, e.g. 150000. Jobs without a parsed salary are excluded while this filter is active. Leave empty or 0 for no salary filtering.

## `seniorityFilter` (type: `array`):

Keep only jobs whose detected seniority matches one of these values: intern, junior, mid, senior, staff, lead. Values outside this list are ignored; jobs with unknown (null) seniority are excluded while this filter is active. Leave empty for no seniority filtering.

## `compactOutputForMcp` (type: `boolean`):

When true, each row omits the long description_text field and keeps only the structured fields (plus ai_skills and salary fields) - handy for MCP / agent use with tight context windows. Row count and per-row billing stay unchanged.

## Actor input object example

```json
{
  "companies": [],
  "max_companies": 0,
  "only_ai_core": true,
  "runMode": "daily_active",
  "aiCategoryFilter": "all",
  "seniorityFilter": [],
  "compactOutputForMcp": false
}
```

# Actor output Schema

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

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("kotaro001/ai-jobs-daily").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 = {}

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

```

## MCP server setup

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

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/5erWE4kkAHrmRvUj1/builds/r80UWWTabvAZh914P/openapi.json
