# Jobs & Companies by Tech Stack — Greenhouse/Lever/Ashby (`yasaslive/jobs-by-stack`) Actor

Fetches postings from public Greenhouse, Lever, and Ashby job-board APIs, extracts the technologies each posting mentions, filters jobs by stack, and aggregates companies by technology for B2B lead generation.

- **URL**: https://apify.com/yasaslive/jobs-by-stack.md
- **Developed by:** [Eonix Pvt Ltd](https://apify.com/yasaslive) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.00005 / actor start

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/platform/actors/running/actors-in-store#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

## Jobs & Companies by Tech Stack — Greenhouse/Lever/Ashby

Crawls the **public job-board APIs** of Greenhouse, Lever, and Ashby for a list of companies, extracts the **technologies every posting mentions** (455-entry curated dictionary + optional LLM fallback), and emits two datasets in one run:

1. **Job postings filtered by stack** — e.g. "remote jobs that mention Rust AND Kafka".
2. **Companies aggregated by technology** — e.g. "which of these 50 companies are hiring people who use Snowflake, and how many open roles mention it" — ready for B2B prospecting and market research.

No browser, no scraping of HTML pages, no proxies needed: these are official public JSON endpoints, fetched politely (per-host concurrency limit + request spacing + exponential backoff).

### Who it's for

- **Job seekers** hunting roles by the stack they love, not just the title.
- **B2B sales / lead-gen teams** building account lists of companies that demonstrably use (and are investing in) a technology.
- **Analysts & founders** tracking stack adoption trends across a market.

### How it works

1. For each company you provide, the Actor resolves an ATS board:
   - If you pass a board token (`greenhouse`, `lever`, or `ashby`), it is used directly.
   - If you pass only a `careersUrl`, token candidates are derived from the URL (host and path) and probed **Greenhouse → Lever → Ashby**; the first endpoint that answers with a valid board wins. The detected ATS is recorded on the output (`atsSource: "auto-detected"`).
2. All postings are fetched in a single request per board and normalized to one shape (`company, ats, jobId, title, department, location, remote, url, publishedAt, descriptionHtml`).
3. Technologies are extracted with word-boundary matching against a vendored dictionary of **455 canonical technologies** (languages, frameworks, databases, cloud, devops, data, frontend, mobile, AI) with alias handling (`golang`→Go, `k8s`→Kubernetes, `postgres`→PostgreSQL) and longest-match-first logic (`React Native` never double-counts as `React`). Ambiguous names are guarded — `Go` never fires inside "Google" or "go-to-market", `Swift` ignores "SWIFT payments", `Phoenix`/`Kong` never fire on city names.
4. Optionally, postings where the dictionary found fewer than 2 technologies are sent to OpenAI in batched structured calls (only if you provide `openaiApiKey`); merged hits are flagged `source: "llm"`.
5. Postings matching your filters are pushed as `kind: "job"` records; per-company aggregates are pushed as `kind: "company"` records and also written to the key-value store as `companies-by-stack.json`. A final `kind: "summary"` record reports totals, failures, and charges.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | array (required) | – | Companies to crawl, see shape below. Max 100 per run. |
| `requiredTech` | string\[] | `[]` | Keep only postings mentioning **ALL** of these (aliases understood). |
| `anyTech` | string\[] | `[]` | Keep only postings mentioning **AT LEAST ONE** of these. |
| `titleIncludes` | string\[] | `[]` | Keep only postings whose title contains one of these substrings (case-insensitive). |
| `remoteOnly` | boolean | `false` | Keep only remote postings (ATS remote flag where available, else heuristic). |
| `maxPostingsPerCompany` | integer | `500` | Cap on normalized postings per company (also caps billing). |
| `openaiApiKey` | secret string | – | Enables the LLM fallback for low-hit postings. Without it, **no external LLM calls are ever made**. |
| `openaiModel` | string | `gpt-4o-mini` | Model for the LLM fallback. |
| `proxyConfiguration` | object | none | Optional Apify Proxy settings; not needed for these public APIs. |

Each item of `companies`:

```json
{
  "name": "Ramp",                                  // optional display name
  "greenhouse": "ramp",                            // Greenhouse board token, OR
  "lever": "ramp",                                 // Lever board token, OR
  "ashby": "ramp",                                 // Ashby job-board name, OR
  "careersUrl": "https://jobs.ashbyhq.com/ramp"    // any careers URL — ATS auto-detected
}
```

At least one of `greenhouse` / `lever` / `ashby` / `careersUrl` is required per company.

### Output

#### `kind: "job"` — one record per posting that passed your filters

Real record from a run with the default sample input (`descriptionHtml` shortened here — the dataset contains the full posting HTML):

```json
{
  "kind": "job",
  "company": "GitLab",
  "ats": "greenhouse",
  "jobId": "8695515002",
  "title": "Backend Engineer, Geo Team",
  "department": "Platforms Engineering",
  "location": "Bangalore, India",
  "remote": false,
  "url": "https://job-boards.greenhouse.io/gitlab/jobs/8695515002",
  "publishedAt": "2026-08-12T03:33:39.000Z",
  "technologies": [
    { "name": "GitLab", "category": "devops", "source": "dictionary" },
    { "name": "PostgreSQL", "category": "database", "source": "dictionary" },
    { "name": "Ruby on Rails", "category": "framework", "source": "dictionary" },
    { "name": "Sidekiq", "category": "framework", "source": "dictionary" }
  ],
  "technologiesSummary": "GitLab, PostgreSQL, Ruby on Rails, Sidekiq",
  "descriptionHtml": "<div class=\"content-intro\"><p>GitLab is the intelligent orchestration platform for DevSecOps. …</p>…"
}
```

#### `kind: "company"` — one aggregate per company (always covers ALL fetched postings, not just filtered ones)

Real record from the same run (66 technologies total; the top 5 shown here):

```json
{
  "kind": "company",
  "company": "Ramp",
  "ats": "ashby",
  "atsSource": "input",
  "careersUrl": "https://jobs.ashbyhq.com/ramp",
  "totalPostings": 136,
  "matchedPostings": 33,
  "technologies": [
    {
      "name": "Python",
      "category": "language",
      "postingCount": 27,
      "sampleJobUrls": [
        "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
        "https://jobs.ashbyhq.com/ramp/f564dcf9-9390-4a3f-896f-8047a5086040",
        "https://jobs.ashbyhq.com/ramp/4745807e-82f4-4b1a-857c-dc8dadc73076"
      ]
    },
    { "name": "SQL", "category": "language", "postingCount": 21, "sampleJobUrls": ["…"] },
    { "name": "Salesforce", "category": "cloud", "postingCount": 18, "sampleJobUrls": ["…"] },
    { "name": "LLMs", "category": "ai", "postingCount": 17, "sampleJobUrls": ["…"] },
    { "name": "Claude", "category": "ai", "postingCount": 13, "sampleJobUrls": ["…"] }
  ]
}
```

#### `kind: "summary"` — one final record per run

```json
{
  "kind": "summary",
  "companiesProcessed": 4,
  "companiesFailed": [],
  "totalPostingsNormalized": 606,
  "jobRecordsPushed": 248,
  "companyRecordsPushed": 4,
  "charging": {
    "postings1kUnitsDue": 1,
    "postings1kUnitsCharged": 1,
    "companyProfilesCharged": 4,
    "note": "Counts reflect accepted Actor.charge() calls. On local/unmonetized runs the platform ignores them (no money moves); real billing starts once the PPE events exist in the Apify Console."
  },
  "companiesByStackUrl": "https://api.apify.com/v2/key-value-stores/<store-id>/records/companies-by-stack.json"
}
```

A formal JSON Schema for all three record kinds ships in [`docs/output.schema.json`](docs/output.schema.json).

#### Key-value store artifact

All company aggregates are additionally written to the run's key-value store as **`companies-by-stack.json`** (a JSON array), so you can grab the whole companies-by-technology dataset with one URL — logged at the end of every run and included in the summary record.

### Pricing (pay-per-event)

| Event | What you pay for | Suggested price |
|---|---|---|
| `postings-1k` | Each 1,000 job postings fetched and normalized (rounded up once per run) | **$1.00** |
| `company-profile` | Each company aggregate profile produced | **$0.01** |

Example: 4 companies / 606 postings = 1 × `postings-1k` + 4 × `company-profile` = **$1.04**. You are never charged for failed work — charges happen only after the corresponding records are pushed, and `maxPostingsPerCompany` caps the volume per company.

### Use cases

1. **Job search by stack** — `anyTech: ["Rust"]`, `requiredTech: ["Kafka"]`, `remoteOnly: true` across 30 companies you admire → every remote Rust job at companies also running Kafka, with links.
2. **B2B lead generation** — feed 100 target accounts and look at the `companies-by-stack.json` artifact for `"Snowflake"` → companies actively hiring people to use Snowflake, with proof (sample job URLs) for your outreach email.
3. **Market research** — run monthly over the same cohort and diff `postingCount` per technology to see which stacks are growing across your market.

### FAQ

**Which ATSes are supported?** Greenhouse, Lever (US + EU clusters), and Ashby — via their public job-board APIs. Companies on other ATSes fail gracefully with a clear reason in the summary record.

**How reliable is auto-detection from `careersUrl`?** URLs hosted on the ATS itself (boards.greenhouse.io/…, jobs.lever.co/…, jobs.ashbyhq.com/…) are exact. For company domains the Actor guesses tokens from the domain and path; when in doubt, pass the board token explicitly.

**Do filters affect the company aggregates?** No. `requiredTech`/`anyTech`/`titleIncludes`/`remoteOnly` shape the `kind:"job"` records only; aggregates always describe the company's whole board (up to `maxPostingsPerCompany`).

**How accurate is the extraction?** The dictionary matcher is precision-tuned: word boundaries, longest-match-first, case-sensitive handling for ambiguous names (Go, Swift, Spark…), and phrase-only matching for landmine words (Phoenix, Kong, Nomad, Bedrock…). It only reports technologies literally mentioned in the posting. The optional LLM fallback catches postings phrased unusually; its hits are flagged `source: "llm"` so you can filter them out if you want dictionary-only precision.

**Does it collect personal data?** No — see the note below.

**What happens when a board is huge?** One HTTP request still fetches it; `maxPostingsPerCompany` (default 500) caps what is normalized, pushed, and charged.

**Can it crawl arbitrary career pages without an API?** No — that is out of scope by design. Supporting the three public APIs keeps the Actor fast, cheap, and reliable.

### Data & GDPR note

The output contains **job postings and company-level aggregates only** — data the companies themselves publish for distribution via their ATS APIs. The Actor never collects recruiter names, e-mails, applicant data, or any other personal data. The optional LLM fallback sends only posting text (title + description) to OpenAI, and only when you supply your own API key.

### Local development

```bash
npm install
npm test          # build + unit tests (matcher fixtures, normalizers, charging math)
npx apify run     # full local run against the sample INPUT.json (3 tokens + 1 auto-detected)
```

Results land in `storage/datasets/default/` and `storage/key_value_stores/default/companies-by-stack.json`. Lint with `npm run lint`.

### Deploy

```bash
apify login
apify push
```

Then in Apify Console → your Actor → **Monetization**: create the two pay-per-event events exactly as named in the pricing table above (`postings-1k`, `company-profile`), do a test run, and publish to the Store with this README as the store page. Details in [RUNBOOK.md](RUNBOOK.md).

### Changelog

#### 0.1.0 (2026-08-14)

- Initial release: Greenhouse/Lever/Ashby fetch + normalize, ATS auto-detection from careers URLs, 455-technology dictionary extraction with alias/ambiguity handling, optional OpenAI fallback, stack filters, company aggregates + `companies-by-stack.json` artifact, pay-per-event charging.

# Actor input Schema

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

Companies to crawl. Each item is an object: <code>{ "name": "Acme", "greenhouse": "acme", "lever": "acme", "ashby": "acme", "careersUrl": "https://acme.com/careers" }</code>. All fields are optional, but each company needs at least one of <code>greenhouse</code>, <code>lever</code>, <code>ashby</code> (the ATS board token) or <code>careersUrl</code>. When only <code>careersUrl</code> is given, the Actor derives token candidates from the URL and probes Greenhouse → Lever → Ashby to auto-detect the ATS.

## `requiredTech` (type: `array`):

Keep only postings that mention ALL of these technologies. Aliases are understood — e.g. <code>postgres</code> matches PostgreSQL, <code>k8s</code> matches Kubernetes.

## `anyTech` (type: `array`):

Keep only postings that mention AT LEAST ONE of these technologies. Aliases are understood.

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

Keep only postings whose title contains at least one of these substrings (case-insensitive).

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

Keep only remote postings. Uses the ATS's own remote flag where available (Ashby, Lever), otherwise a heuristic on location and title.

## `maxPostingsPerCompany` (type: `integer`):

Postings beyond this limit are dropped (newest kept in board order). Also caps what you are charged for.

## `openaiApiKey` (type: `string`):

If set, postings with fewer than 2 dictionary hits are sent to OpenAI in batched structured calls and the extracted technologies are merged in, flagged <code>source: "llm"</code>. Leave empty to use the built-in dictionary only (no external LLM calls are ever made without a key).

## `openaiModel` (type: `string`):

Model used for the LLM fallback. Only used when an OpenAI API key is provided.

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

Optional. The three ATS APIs are public JSON endpoints that work without proxies; enable Apify Proxy only if your network requires it.

## Actor input object example

```json
{
  "companies": [
    {
      "name": "Duolingo",
      "greenhouse": "duolingo"
    },
    {
      "name": "Palantir",
      "lever": "palantir"
    },
    {
      "name": "Ramp",
      "ashby": "ramp"
    },
    {
      "name": "GitLab",
      "careersUrl": "https://boards.greenhouse.io/gitlab"
    }
  ],
  "remoteOnly": false,
  "maxPostingsPerCompany": 500,
  "openaiModel": "gpt-4o-mini"
}
```

# Actor output Schema

## `jobsAndCompanies` (type: `string`):

All kind=job, kind=company, and kind=summary records pushed by the run.

## `companiesByStack` (type: `string`):

Every company aggregate from the run as one JSON array — the B2B/lead-gen artifact.

# 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": [
        {
            "name": "Duolingo",
            "greenhouse": "duolingo"
        },
        {
            "name": "Palantir",
            "lever": "palantir"
        },
        {
            "name": "Ramp",
            "ashby": "ramp"
        },
        {
            "name": "GitLab",
            "careersUrl": "https://boards.greenhouse.io/gitlab"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yasaslive/jobs-by-stack").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": [
        {
            "name": "Duolingo",
            "greenhouse": "duolingo",
        },
        {
            "name": "Palantir",
            "lever": "palantir",
        },
        {
            "name": "Ramp",
            "ashby": "ramp",
        },
        {
            "name": "GitLab",
            "careersUrl": "https://boards.greenhouse.io/gitlab",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("yasaslive/jobs-by-stack").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": [
    {
      "name": "Duolingo",
      "greenhouse": "duolingo"
    },
    {
      "name": "Palantir",
      "lever": "palantir"
    },
    {
      "name": "Ramp",
      "ashby": "ramp"
    },
    {
      "name": "GitLab",
      "careersUrl": "https://boards.greenhouse.io/gitlab"
    }
  ]
}' |
apify call yasaslive/jobs-by-stack --silent --output-dataset

```

## MCP server setup

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

```

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/5a5fAiaV7jSgy5wkm/builds/1IIoMD10paFEqM9ZG/openapi.json
