Jobs & Companies by Tech Stack — Greenhouse/Lever/Ashby
Pricing
from $0.00005 / actor start
Jobs & Companies by Tech Stack — Greenhouse/Lever/Ashby
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.
Pricing
from $0.00005 / actor start
Rating
0.0
(0)
Developer
Eonix Pvt Ltd
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
9 days ago
Last modified
Categories
Share
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:
- Job postings filtered by stack — e.g. "remote jobs that mention Rust AND Kafka".
- 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
- For each company you provide, the Actor resolves an ATS board:
- If you pass a board token (
greenhouse,lever, orashby), 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").
- If you pass a board token (
- 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). - 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 Nativenever double-counts asReact). Ambiguous names are guarded —Gonever fires inside "Google" or "go-to-market",Swiftignores "SWIFT payments",Phoenix/Kongnever fire on city names. - 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 flaggedsource: "llm". - Postings matching your filters are pushed as
kind: "job"records; per-company aggregates are pushed askind: "company"records and also written to the key-value store ascompanies-by-stack.json. A finalkind: "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:
{"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):
{"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):
{"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
{"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.
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
- Job search by stack —
anyTech: ["Rust"],requiredTech: ["Kafka"],remoteOnly: trueacross 30 companies you admire → every remote Rust job at companies also running Kafka, with links. - B2B lead generation — feed 100 target accounts and look at the
companies-by-stack.jsonartifact for"Snowflake"→ companies actively hiring people to use Snowflake, with proof (sample job URLs) for your outreach email. - Market research — run monthly over the same cohort and diff
postingCountper 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
npm installnpm 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
apify loginapify 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.
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.jsonartifact, pay-per-event charging.