# Tech Stack Hiring Demand - Greenhouse, Lever & Ashby Jobs (`mibedk/ats-technology-demand-index`) Actor

Reads open postings on the public Greenhouse, Ashby and Lever job boards you name, detects which of 395 technologies each requires, and returns one row per technology: companies and roles requiring it, share of postings, change since last run, plus adoption events.

- **URL**: https://apify.com/mibedk/ats-technology-demand-index.md
- **Developed by:** [Mikkel Bech-Hansen](https://apify.com/mibedk) (community)
- **Categories:** Jobs, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 technology index rows

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

## Tech Stack Hiring Demand - Greenhouse, Lever & Ashby Jobs

Give this actor a list of companies whose job boards run on Greenhouse, Ashby or Lever. It reads every open posting on those boards, runs a curated vocabulary of 395 technologies over the description text, and returns **one row per technology** rather than one row per job: how many of your companies require it, how many open roles mention it, what share of all postings that is, and how those numbers moved since your last run. It also writes typed events when a specific company starts requiring a technology, stops requiring it, or when a technology's mention share breaks out of its own trailing range.

It is not a job scraper. If you want the postings themselves, several actors on the Store already read these same endpoints. This one throws the postings away and keeps the aggregate, which is the thing you can put on a dashboard and watch week over week.

### What you need to know first

The index is a **time series**, so it needs more than one run to be interesting. The first run against a fresh history returns every level (companies, postings, share, rank) with `null` deltas and no events. Run it again — on a schedule, weekly is the natural cadence — and every row gains a run-over-run delta, a trailing median and a breakout flag. History is kept in a named key-value store (`stateStoreName`), because Apify recreates the *default* store on every run.

### Input

Every field has a working default. Running it with no configuration at all scans a twelve-company watchlist and returns a real index.

| Field | Type | Default | Notes |
|---|---|---|---|
| `greenhouseBoards` | array of strings | 7 companies | Slug is the last path segment of `job-boards.greenhouse.io/anthropic` → `anthropic`. A pasted URL is accepted and reduced to its slug. |
| `ashbyBoards` | array of strings | 4 companies | Slug is the first path segment of `jobs.ashbyhq.com/openai` → `openai`. |
| `leverBoards` | array of strings | `["palantir"]` | `jobs.lever.co/palantir` → `palantir`. |
| `categories` | array of enums | `[]` (all 14) | `language`, `frontend`, `mobile`, `backend`, `data-platform`, `database`, `cloud`, `devops`, `observability`, `ai-ml`, `security`, `testing`, `saas-and-business`, `practice`. |
| `excludeSelfMentions` | boolean | `true` | Stops a vendor's own board from counting its own product. See "How the counting works". |
| `minCompaniesPerTechnology` | integer | `1` | Raise to cut the long tail and the cost. |
| `minPostingsPerTechnology` | integer | `1` | Same, by postings. |
| `maxIndexRows` | integer | `0` (no cap) | Rows are ordered by postings, so `50` gives the 50 most-demanded technologies. Hard cap on cost. |
| `emitEvents` | boolean | `true` | Writes to the named `technology-events` dataset. |
| `maxEvents` | integer | `500` | Cost guard. `0` means no cap. Events are ranked breakouts → adoptions → abandonments, so a cap keeps the informative ones. |
| `trailingRuns` | integer | `12` (2–104) | Window for the trailing median and the window delta. At 12 on a weekly schedule this is the 12-week delta. Also how much history is stored. |
| `breakoutThresholdPercent` | integer | `150` (105–1000) | Share must reach this percentage of the trailing median to flag a breakout. |
| `minMentionsForBreakout` | integer | `3` | Stops 1 → 3 postings looking like a trend. |
| `minRunsForBreakout` | integer | `4` (capped at `trailingRuns`) | How much history a technology needs before a breakout can fire. |
| `stateStoreName` | string | `technology-demand-index-state` | Named key-value store holding the history. Use a different name to keep two watchlists as two independent series. |
| `resetState` | boolean | `false` | Wipes the history and starts a new baseline. |
| `maxPostingsPerBoard` | integer | `0` (no limit) | Caps postings read per company. Changing it changes the denominator — keep it steady across runs. |
| `maxConcurrency` | integer | `5` (1–20) | One request returns an entire board. |
| `proxyConfiguration` | object | no proxy | Not needed; these endpoints have no anti-bot protection. |

### Output

#### Index rows — the default dataset

One row per technology per run. A real record from a run over Anthropic, Cloudflare, Ramp, Vanta and Palantir (1,570 postings):

```json
{
  "runId": "local-2026-09-23T23:37:32.627Z",
  "runAt": "2026-09-23T23:37:32.627Z",
  "technology": "Kubernetes",
  "category": "devops",
  "rank": 13,
  "companiesRequiring": 3,
  "companiesScanned": 5,
  "companySharePct": 60,
  "postingsMentioning": 147,
  "postingsScanned": 1570,
  "postingSharePct": 9.36,
  "deltaBasis": "same-watchlist",
  "previousRunAt": "2026-09-23T23:37:27.752Z",
  "previousCompaniesRequiring": 3,
  "companiesRequiringDelta": 0,
  "previousPostingSharePct": 9.36,
  "postingSharePctDelta": 0,
  "windowRuns": 1,
  "windowStartAt": "2026-09-23T23:37:27.752Z",
  "postingSharePctAtWindowStart": 9.36,
  "postingSharePctWindowDelta": 0,
  "trailingMedianPostingSharePct": 9.36,
  "shareVsTrailingMedian": 1,
  "isBreakout": false,
  "newCompaniesThisRun": [],
  "droppedCompaniesThisRun": [],
  "firstSeenInIndexAt": "2026-09-23T23:37:27.752Z",
  "runsInHistory": 1,
  "exampleCompanies": [
    "anthropic"
  ],
  "exampleJobUrls": [
    "https://job-boards.greenhouse.io/anthropic/jobs/5411560008",
    "https://job-boards.greenhouse.io/anthropic/jobs/5411267008",
    "https://job-boards.greenhouse.io/anthropic/jobs/4610158008"
  ],
  "vocabularyVersion": "2026.09.1"
}
```

Field notes worth reading:

- `postingSharePct` is the headline number: the percentage of all postings in your watchlist that mention this technology. `postingSharePctDelta` is in **percentage points**, not percent.
- `deltaBasis` tells you whether the comparison is honest: `same-watchlist` means the previous run covered exactly the same companies, `watchlist-changed` means it did not (adding one company moves every share), `first-run` means there is no baseline yet.
- `windowRuns` is how many prior runs actually backed the trailing median. It is small until you have run the actor a few times.
- `newCompaniesThisRun` / `droppedCompaniesThisRun` name the companies behind `companiesRequiringDelta`.

#### Events — the named `technology-events` dataset

Three event types. This dataset is **append-only across runs** — it is the event log, and it is not purged between runs, so you can read the whole history from one place.

```json
{
  "eventType": "technology_mention_share_breakout",
  "technology": "Distributed systems",
  "category": "practice",
  "companySlug": null,
  "postingsMentioning": 246,
  "previousPostingsMentioning": 16,
  "postingSharePct": 15.67,
  "trailingMedianPostingSharePct": 8,
  "summary": "Distributed systems is mentioned in 15.67% of postings across the watchlist, against a trailing median of 8%."
}
```

- `technology_first_required` — a company that was scanned before now has at least one open posting requiring this technology, and had none last time. `companySlug`, `source`, `exampleJobTitle` and `exampleJobUrl` point at that company's own posting.
- `technology_no_longer_required` — the reverse. Only fires for companies whose board was actually read both times, so a board that 404s is never mistaken for an abandonment.
- `technology_mention_share_breakout` — index-level, so `companySlug` is `null`.

A company scanned for the first time produces a **baseline, not events** — otherwise adding one company to the watchlist would emit two hundred fake adoptions.

#### Key-value records

`SUMMARY` (run counters), `COVERAGE` (per board: reachable, postings read, technologies detected, self-mentions suppressed) and `EVENTS` (this run's events).

### How the counting works

- **One posting counts once per technology.** A posting that says "Python" nine times is one Python posting. The index measures how many roles require a technology, not how loudly.
- **Whole-token matching, longest match first.** "Java" never fires on "JavaScript"; "Apache Spark" beats "Spark"; "React Native" beats "React". Separators employers use between technologies (`/`, `-`, `,`) split, so "Python/Go/Rust" counts as three.
- **Guards on ambiguous names.** Short or English-word names — `Go`, `R`, `C`, `React`, `Swift`, `Spark`, `Chef`, `Excel`, `Linear`, `Notion` — require the exact capitalisation plus nearby supporting context, and are vetoed by phrases like "go-to-market", "R\&D", "Series C" or "head chef". This biases toward **undercounting**: a genuine mention next to an unlucky phrase is dropped rather than a false one admitted.
- **Sub-products credit their parent.** "AWS Lambda" also counts as AWS, "BigQuery" also counts as Google Cloud Platform, "T-SQL" also counts as SQL.
- **Self-mentions are dropped by default.** Stripe's postings all say "customers use Stripe to accept payments" and Databricks mentions Databricks in roughly a third of its ads. Counted naively, every vendor becomes its own biggest adopter. With `excludeSelfMentions` on, a board whose slug or company name matches a technology name (or is a prefix of one, so `openai` also drops "OpenAI API") does not contribute to that technology. `COVERAGE` lists what was suppressed per board.

### Cost

Pay-per-event. See `MONETIZATION.md` for the full plan; the shape is:

| Event | Price | When |
|---|---|---|
| `index-row` | $0.010 | Each technology row delivered |
| `adoption-event` | $0.020 | Each company-level or breakout event delivered |
| `board-scanned` | $0.005 | Each board successfully read |

A **default run** — the 12-company watchlist, measured at 4,626 postings — produces **280 index rows**: $2.80 plus $0.06 in board fees, **$2.86**, and a few dollars of events once history exists. A **300-company watchlist** produces roughly 350–400 rows: about **$3.75 + $1.50 = $5.25 per run**, so weekly monitoring of 300 accounts costs roughly **$21/month**.

To spend less, set `maxIndexRows` to the number of technologies you actually track (`maxIndexRows: 50` costs $0.50 plus board fees) or raise `minCompaniesPerTechnology` to drop the long tail.

Charges are applied **in value order** — index rows first, then events, then board fees — so a tight `ACTOR_MAX_TOTAL_CHARGE_USD` buys you the index rather than being spent on overhead. When the cap is reached the run stops cleanly, still saves its history, and reports `chargeLimitReached: true` in `SUMMARY`.

### Known limitations

Read these before you run it on a schedule.

- **The first run has no deltas.** By design. It reports levels and writes the baseline. Events need a second run; the trailing median and breakout flags need `minRunsForBreakout` (default 4) runs.
- **A job ad is not a technology audit.** This measures what companies *say they are hiring for*, which leads actual adoption and lags actual abandonment. A company that quietly runs Kafka everywhere but is not hiring for it will not show Kafka. Treat it as a demand signal, not an inventory.
- **Changing the watchlist breaks comparability.** Every share is a fraction of all postings in the watchlist, so adding or removing companies moves every row. The actor flags this in `deltaBasis` rather than hiding it, but the honest move after a big change is `resetState: true`.
- **Changing `maxPostingsPerBoard` does the same thing**, for the same reason.
- **Board size dominates the weights.** A watchlist with one 900-posting employer and nine 40-posting ones is largely an index of that one employer. `companiesRequiring` / `companySharePct` are the board-size-independent columns; use them when that matters.
- **The vocabulary is curated, not exhaustive.** 395 technologies, chosen for what shows up in job ads. Something niche to your sector may simply not be in it. `vocabularyVersion` ships on every row so you can tell when the list changed underneath a series.
- **Guarded names undercount.** See "How the counting works". `Go`, `R` and `C` in particular are deliberately conservative.
- **Lever boards 404 often.** Many companies that once used Lever have moved. A 404 is logged as a warning and skipped, never retried and never counted as an abandonment. The run only fails if *every* board fails.
- **No pagination, no deduplication across boards.** One GET returns one whole board. A company listing the same role on two boards is counted twice.
- **`companyName` is only populated for Greenhouse.** Ashby and Lever do not return it on these endpoints; use `companySlug`.
- **History lives in your account.** If you delete the named key-value store, or run the actor under a different `stateStoreName`, the next run is a fresh baseline.
- **Company-level facts only.** These feeds carry no recruiter, hiring-manager or applicant fields and this actor reads none of them.

### Legal

This actor is **not affiliated with, endorsed by, or connected to Greenhouse, Ashby, Lever**, or any of the companies whose job boards it reads.

It reads public, unauthenticated JSON endpoints that these applicant tracking systems expose to render their customers' public job boards (`boards-api.greenhouse.io`, `api.ashbyhq.com/posting-api`, `api.lever.co/v0/postings`). No login, key or token is used, and no access control is circumvented. Even so: **"public" and "permitted by the Terms of Service" are different questions.** These vendors' terms, and the terms of the sites that embed these boards, very likely restrict automated access and bulk collection regardless of whether the data requires a login. Using this actor may not comply with them.

You are responsible for your own compliance — with those terms, with the rate at which you run this actor, and with the laws that apply where you operate — and for what you do with the data it returns. If you are building a commercial product on this output, get your own legal advice rather than relying on the fact that a URL returned HTTP 200.

The endpoints can change or be withdrawn without notice; they are internal APIs, not a documented public product.

# Actor input Schema

## `greenhouseBoards` (type: `array`):

Company slugs on Greenhouse. The slug is the last path segment of the company's board URL: job-boards.greenhouse.io/anthropic -> "anthropic". Leave as-is to use the built-in 12-company watchlist. A full URL is accepted and reduced to its slug.

## `ashbyBoards` (type: `array`):

Company slugs on Ashby. The slug is the first path segment of jobs.ashbyhq.com/openai -> "openai".

## `leverBoards` (type: `array`):

Company slugs on Lever, e.g. jobs.lever.co/palantir -> "palantir". Many companies that once used Lever now return 404; those boards are skipped with a warning rather than failing the run.

## `categories` (type: `array`):

Restrict the index to these categories. Leave empty for all 14. "practice" covers things like CI/CD, MLOps and Large Language Models, which are skills rather than products - exclude it if you only want vendor adoption.

## `excludeSelfMentions` (type: `boolean`):

On by default. Stripe's postings say "customers use Stripe to accept payments" and Databricks mentions Databricks in a third of its ads; counting that would make every vendor its own biggest adopter. Turn it off if you want the raw text counts.

## `minCompaniesPerTechnology` (type: `integer`):

A technology needs this many distinct companies requiring it to appear in the index. Raise it to cut the long tail (and the cost) of one-off mentions.

## `minPostingsPerTechnology` (type: `integer`):

A technology needs this many postings mentioning it to appear in the index.

## `maxIndexRows` (type: `integer`):

Hard cap on dataset rows, and therefore on cost. Rows are ordered by postings mentioning the technology, so a cap of 50 gives you the 50 most-demanded technologies. 0 means no cap; a typical run produces 200-320 rows.

## `emitEvents` (type: `boolean`):

Writes typed events to the named "technology-events" dataset: a company started requiring a technology, stopped requiring it, or a technology broke out of its trailing mention-share range. Needs at least one prior run to produce anything.

## `maxEvents` (type: `integer`):

Cost guard on the events dataset. Events are ranked breakouts first, then adoptions, then abandonments, so a cap keeps the informative ones. 0 means no cap. A stable watchlist produces a handful per run; changing the watchlist or "maxPostingsPerBoard" between runs can produce hundreds.

## `trailingRuns` (type: `integer`):

How many past runs the trailing median and the window delta look back over. At 12 with a weekly schedule this is the 12-week delta. Stored history is trimmed to this many runs.

## `breakoutThresholdPercent` (type: `integer`):

A technology is flagged as a breakout when this run's mention share reaches this percentage of its trailing median and is above the previous run. 150 means a 50% jump.

## `minMentionsForBreakout` (type: `integer`):

Stops a jump from one posting to three being reported as a breakout.

## `minRunsForBreakout` (type: `integer`):

A technology needs this much history before its trailing median means anything. Capped at "trailingRuns".

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

Named key-value store where this actor remembers past runs. It must be a named store, because the default store is recreated on every run. Use a different name to keep two watchlists as two independent time series.

## `resetState` (type: `boolean`):

Wipes the saved run history for this store name. Use it after a big change to the watchlist, when old deltas would be misleading. This run then reports levels with null deltas.

## `maxPostingsPerBoard` (type: `integer`):

Caps how many postings are read from each company, so one very large employer cannot dominate the shares. 0 means no per-board limit. Changing it changes the denominator, so keep it steady across runs.

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

These are public JSON endpoints with generous limits, but 5 is polite and plenty fast - each request returns an entire board.

## `proxyConfiguration` (type: `object`):

Not needed. These endpoints have no anti-bot protection and the actor works fine without a proxy, which is why proxying is off by default. Enable it only if your own network blocks the requests.

## Actor input object example

```json
{
  "greenhouseBoards": [
    "anthropic",
    "databricks",
    "cloudflare",
    "stripe",
    "figma",
    "coinbase",
    "reddit"
  ],
  "ashbyBoards": [
    "openai",
    "ramp",
    "notion",
    "vanta"
  ],
  "leverBoards": [
    "palantir"
  ],
  "categories": [],
  "excludeSelfMentions": true,
  "minCompaniesPerTechnology": 1,
  "minPostingsPerTechnology": 1,
  "maxIndexRows": 0,
  "emitEvents": true,
  "maxEvents": 500,
  "trailingRuns": 12,
  "breakoutThresholdPercent": 150,
  "minMentionsForBreakout": 3,
  "minRunsForBreakout": 4,
  "stateStoreName": "technology-demand-index-state",
  "resetState": false,
  "maxPostingsPerBoard": 0,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `indexRows` (type: `string`):

One row per technology: companies requiring it, postings mentioning it, mention share, run-over-run delta, trailing median and breakout flag.

## `events` (type: `string`):

Typed company-level events for this run. Also written to the named "technology-events" dataset.

## `coverage` (type: `string`):

For each board in the watchlist: whether it was reachable, how many postings were read, how many technologies were detected and which self-mentions were suppressed.

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

Boards scanned, postings read, technologies detected, rows and events returned, and whether this run was a baseline.

# 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 = {
    "greenhouseBoards": [
        "anthropic",
        "databricks",
        "cloudflare",
        "stripe",
        "figma",
        "coinbase",
        "reddit"
    ],
    "ashbyBoards": [
        "openai",
        "ramp",
        "notion",
        "vanta"
    ],
    "leverBoards": [
        "palantir"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mibedk/ats-technology-demand-index").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 = {
    "greenhouseBoards": [
        "anthropic",
        "databricks",
        "cloudflare",
        "stripe",
        "figma",
        "coinbase",
        "reddit",
    ],
    "ashbyBoards": [
        "openai",
        "ramp",
        "notion",
        "vanta",
    ],
    "leverBoards": ["palantir"],
}

# Run the Actor and wait for it to finish
run = client.actor("mibedk/ats-technology-demand-index").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 '{
  "greenhouseBoards": [
    "anthropic",
    "databricks",
    "cloudflare",
    "stripe",
    "figma",
    "coinbase",
    "reddit"
  ],
  "ashbyBoards": [
    "openai",
    "ramp",
    "notion",
    "vanta"
  ],
  "leverBoards": [
    "palantir"
  ]
}' |
apify call mibedk/ats-technology-demand-index --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mibedk/ats-technology-demand-index"
        }
    }
}
```

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/j3hYyF6atchZYKlAr/builds/tL9SbqG7eD7vA5rB7/openapi.json
