Job Feed: 110+ Job Boards & Career Pages in 60+ Countries
Pricing
$0.50 / 1,000 unique job in aggregated feeds
Job Feed: 110+ Job Boards & Career Pages in 60+ Countries
One deduplicated, newest-first job feed from 110+ job boards in 60+ countries, 40+ ATS career systems and any company career page. Deepest in Switzerland, Germany and Austria. Daily new-jobs-only runs, radius filter, agency flag, and MCP for AI agents.
Pricing
$0.50 / 1,000 unique job in aggregated feeds
Rating
0.0
(0)
Developer
NanoScrape
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
2
Monthly active users
5 hours ago
Last modified
Categories
Share
Job Feed - Job Aggregator for Recruiters, Job Seekers and AI Agents
One deduplicated, newest-first job feed from 110+ job boards (LinkedIn, Indeed, StepStone, Jobs.ch, Arbeitsagentur.de, USAJOBS and many more), 40+ ATS career systems and any company career page. One input, one schema, one stable job_id. No login needed.
Who it is for
- Recruiters and agencies (deepest coverage in CH, DE and AT) who want new roles at direct employers, hiring signals and a daily list instead of ten browser tabs.
- Job seekers who want a daily alert for a role, city and radius.
- HR-tech and AI-agent builders who need one normalized job schema, a stable key to upsert on, and an MCP tool.
What you get
- One feed across boards, deduplicated, newest first, each row with a stable
job_id(same job from any board hashes the same). onlyNewJobs: a daily schedule delivers (and bills) only jobs you have not received before.- A radius filter (
location+radiusKm) that also drops rows boards return too far away. - An agency flag on every company (
company.is_agency) andexcludeAgenciesto drop staffing firms and job aggregators. - Company career pages: add
companyUrlsand get that employer's own openings, including company, career page and employee range.
For recruiters: a copy-paste search
{"countries":["CH"],"keywords":["Pflegefachfrau","Pflegefachmann"],"location":"Zürich","radiusKm":30,"postedWithinDays":7,"excludeAgencies":true,"maxResultsTotal":200}
Measured (local run, October 2026): 98 unique direct-employer jobs from 6 boards (jobs.ch, arbeit.swiss, jobscout24, Indeed, LinkedIn, Glassdoor); 138 board rows merged to 98, 10 agency postings left out, 24 jobs outside 30 km dropped. Cost about $0.38 for the run ($0.33 sub-actors, $0.05 job-feed), about $0.004 per job. Weekly that is about $1.65 a month, daily at most about $11; with onlyNewJobs the job-feed part drops to new jobs only while board scraping still applies.
How to start
- Pick
countries(for example["CH"]),keywordsand alocation. - Start; the prefilled input (Zürich, Software Engineer, 30 km, last 7 days) works as is.
- Schedule it daily with
"onlyNewJobs": true, and add"excludeAgencies": trueif you only want direct employers.
What it costs
Pay-per-event, no subscription. job-feed bills $0.0005 per unique job it delivers. The boards and career-site scrapers it calls are separate actors, billed separately to your Apify account at their own rates, and included in the totals below. Rows job-feed drops before delivery (caps, filters, duplicates) are not billed by job-feed, though a board may have billed its own event for them.
| Search | Per run | Per month |
|---|---|---|
CH, Pflegefachfrau/-mann, Zürich, 30 km, excludeAgencies, 98 unique jobs (measured, October 2026) | about $0.38 | about $1.65 weekly, at most about $11 daily |
CH, Laborant, Basel, 6 boards plus jobbasel-ch, 25 unique jobs (measured, October 2026) | about $0.11 | about $3.30 daily (30 runs) |
50-company career-site list (careerSites, known ATS, about 12 open roles each, about 600 jobs), weekly (estimate) | about $0.90 | about $3.60 (4 runs) |
The measured runs use list prices from each board's own rate (for example jobs-ch $0.00287 per row, arbeit-swiss and jobscout24-ch $0.003, career-site-jobs $0.001 per job) plus job-feed's $0.0005 per unique job; the career-site row is an estimate. See Supported boards for board prices and Billing and cost model for the cost_report of each run. On custom pages (LLM extractor, about $0.012 per job plus $0.01 per company) the same 50 companies cost around $8 for the first run (estimate). maxResultsTotal caps spend.
Use cases
- Agency sourcing of new roles at direct employers.
countries: ["CH","DE"], your keywords,excludeAgencies: true,onlyNewJobs: true, daily. Staffing firms and aggregators (DACH agency registry) are dropped. - Hiring-signal leads for sales. Run a role keyword (for example "DevOps Engineer") with
includeCompanyDetails, and rows carry the company name, domain,career_page_urland, where the source reports it, an employee range (employee_count_from,employee_count_to). - A daily alert for a job seeker. One city,
radiusKm: 25,postedWithinDays: 1,onlyNewJobs: true, scheduled each morning. - Life-science niche search. Combine regional boards (
includeRegionalBoards: "nearby"for a Basel or Zürichlocation) with a list of employer career pages incareerSites, so small cantonal boards and employer ATS pages land in one feed. - Feeding an AI agent. Connect the actor over MCP (below) and let your agent search jobs by country, keyword and location;
job_idtells it what it has already shown.
Example output
One row from the Pflege run, trimmed:
{"job_id": "edf36ab3f7686037","title": "Pflegefachperson für die Viszeralchirurgie / Gastroenterologie 50 -100%","company": {"name": "Universitätsspital Zürich", "is_agency": false, "agency_type": null},"location": {"raw": "Zurich", "city": "Zürich", "region": "ZH", "country": "CH"},"employment_type": "permanent","workload_pct": {"min": 50, "max": 100},"posted_date": "2026-10-16T12:31:19.714Z","language": "de","matched_query": "Pflegefachmann","apply_url": "https://www.jobscout24.ch/en/job/89adc54d-9f7b-4fca-8557-716fd099eab0/","sources": [{"platform": "jobscout24-ch", "url": "https://www.jobscout24.ch/en/job/89adc54d-9f7b-4fca-8557-716fd099eab0/"}],"_platform": "jobscout24-ch"}
Use with AI Agents (MCP)
Connect this actor to any MCP-compatible AI client.
Apify MCP server URL:
https://mcp.apify.com?tools=santamaria-automations/job-feed
For clients that only speak stdio, use mcp-remote:
{"mcpServers": {"job-feed": {"command": "npx","args": ["-y", "mcp-remote", "https://mcp.apify.com?tools=santamaria-automations/job-feed", "--header", "Authorization: Bearer YOUR_APIFY_TOKEN"]}}}
How it works
Job Feed is an orchestrator over three tiers of scrapers:
- 110+ job boards, each with a scraper built per site (LinkedIn, Indeed, StepStone, Jobs.ch, Arbeitsagentur.de, USAJOBS and many more). Selected via
platformsorcountries. career-site-jobs- direct extraction from 40+ ATS career systems (Greenhouse, Lever, Workday, Ashby, Teamtailor, Personio, SmartRecruiters, BambooHR, iCIMS, Recruitee, and more). Activated when acompanyUrlsentry matches a known ATS, or explicitly withincludeCareerSites: true.website-job-extractor- reads any other company website: it finds the careers page, detects the ATS, and falls back to LLM extraction plus a browser retry for JS-rendered sites.
More boards and career systems are added continuously.
Table of contents
- What it costs
- Use cases
- Example output
- Use with AI Agents (MCP)
- How it works
- Quick start
- Constraints
- Supported platforms
- Supported boards
- Coming soon
- Limitations
- Input reference
- Output reference
- How job IDs and dedup work
- Billing and cost model
- Rate card
- Examples
- FAQ
- Troubleshooting
- Regional boards
- Registry
- Related actors
- Run job-feed from code
- API and Apify Console
Quick start
{"countries": ["DE", "AT", "CH"],"keywords": ["Software Engineer"],"maxResultsPerQueryPerPlatform": 25}
That fires every Tier 1 platform in DACH plus Indeed, Glassdoor and LinkedIn per country (includeGlobalBoards, on by default). Reports are in the OUTPUT record, see Terminal items.
Constraints
- Platforms per run: 110+ selectable in the
platformspicker; the orchestrator gates concurrency against a RAM budget, capped bymaxTotalMemoryMbytesif you set it (see RAM and parallelism). Browser-tier actors use up to 4 GB each and queue when budget is tight. - Sub-actor timeout: 60-3600 seconds per platform (default 600).
- titleFilter: comma-separated substring list, case-insensitive OR match. Ten boards also filter natively before detail fetch at no extra cost, and every row is then checked against the same rule here (see Platform selection); the rest are filtered client-side, counted as
dropped_title_filter. - postedWithinDays precision: accepts fractional days:
0.5= last 12 hours,0.042is roughly 1 hour, use1.0/24for exactly 1 hour. - Dedup mode:
streaming(default, rows flow as each platform finishes) orbatch(buffer until all platforms complete, then emit once per uniquejob_idwith all sources inline). See Dedup mode.
Supported platforms
The registry tracks 120+ platforms. 110+ are healthy and selectable in the platforms picker: 110+ job boards plus 3 company-website sources (career-site-jobs, website-job-extractor, website-job-extractor-browser). The rest are parked or deliberately excluded and never selectable (mostly aggregator wrappers over sources this fleet already scrapes first-party, such as Adzuna, Jooble, Careerjet, and Monster). See Coming soon for what is in the build queue.
Every board has a tier: Tier 1 boards run automatically when their country is in countries[]; Tier 2 boards run only when named in platforms[]. The full list, with each board's standalone Apify actor, RAM and pricing, is in Supported boards.
Country presets
countries: ["XX"] turns on every healthy Tier 1 board for that market (table below). It is additive across countries, and additive with anything you list in platforms[]. Tier 2 boards are not in the preset and need to be named explicitly (or added with includeRegionalBoards). When platforms[] is also set, countries still add their preset and the global boards, and run_summary warnings name every board added that way. GB is accepted as an alias for UK. Countries not in the table have no preset, use platforms instead.
includeGlobalBoards (default true) additionally runs Indeed, Glassdoor, and LinkedIn once per selected country: countries: ["DE","AT"] fires two Indeed runs, one scoped to DE and one to AT. Boards you name in platforms[] are not added twice. Set it to false to run only the preset.
| Country | Preset (Tier 1, automatic) |
|---|---|
| Germany (DE) | arbeitsagentur-de, jobware-de, meinestadt-de, stellenanzeigen-de, stepstone-de |
| Austria (AT) | ams-at, karriere-at, stepstone-at |
| Switzerland (CH) | arbeit-swiss, jobs-ch, jobscout24-ch |
| United Kingdom (UK / GB) | nhs-jobs, reed-uk |
| United States (US) | usajobs-us, wellfound-us, workatastartup-us |
| Canada (CA) | jobbank-ca |
| Australia (AU) | seek |
| Belgium (BE) | forem-be, vdab-be |
| Czech Republic (CZ) | prace-cz |
| Denmark (DK) | jobindex-dk |
| Spain (ES) | infojobs-es, sepe-es |
| France (FR) | france-travail, hellowork-fr, welcome-to-the-jungle |
| Greece (GR) | kariera-gr |
| Croatia (HR) | mojposao-hr |
| Hungary (HU) | profession-hu |
| Ireland (IE) | irishjobs-ie |
| Italy (IT) | subito-it-jobs |
| Netherlands (NL) | nationale-vacaturebank-nl, werk-nl |
| Norway (NO) | finn-no |
| Poland (PL) | justjoinit, nofluffjobs-eu, pracuj-pl |
| Portugal (PT) | net-empregos-pt |
| Romania (RO) | anofm-ro, ejobs-ro |
| Russia (RU) | hh-ru, trudvsem-ru |
| Sweden (SE) | arbetsformedlingen-se |
| Slovakia (SK) | profesia-sk |
| Ukraine (UA) | dcz-gov-ua |
Tier 2 boards (opt-in, name them in platforms[]):
- DE:
stellenmarkt-de,nicejob-de. AT:westjob-at. PL:praca-gov-pl. UA:work-ua. - CH:
jobbasel-ch,ostjob-ch,jobzueri-ch,jobbern-ch,jobmittelland-ch,zentraljob-ch,myjob-ch,schaffu-ch,sozjobs-ch,ictjobs-ch,swissdevjobs-ch,carriera-ch,jobup-ch,alpha-topjobs-ch,lavoro-cdt-ch,ge-ch-emploi. - UK:
totaljobs-uk,tes-uk,jobserve-uk,charityjob-uk,guardianjobs-uk,teaching-vacancies-uk,targetjobs-uk,jobs-ac-uk,escape-the-city,cwjobs-uk,prospects-uk,hays-uk,milkround-uk. - US:
healthecareers-us,levels-fyi-us,rigzone-us,wayup-us,snagajob-us,craigslist-jobs,dribbble-jobs,biospace,higheredjobs. - No country preset:
naukri(India),jobkorea-kr(South Korea),bayt-me, and the international or remote boards (linkedin,glassdoor,efinancialcareers,jobrapido,computrabajo,idealist,ycombinator,michaelpage-intl,remoteok,remotive,weworkremotely,dice,builtin,cryptojobslist,web3career,e-fellows,euraxess). - Research jobs (EU-wide):
euraxess(PhD, postdoc, research staff, lab technicians) belongs to no preset and is never added byincludeGlobalBoards; name it inplatforms[].countriesare forwarded to its own country filter (OR-combined),locationas is.countriesalso activates that country's preset, so add"includeGlobalBoards": falseand pick boards yourself for EURAXESS alone, or uselocationwith a country name.
That is 40+ Tier 1 boards across 25+ countries, plus the three global boards, plus career-site-jobs (tied to no country: use includeCareerSites or companyUrls).
Supported boards
Every source selectable in the platforms picker today (113 of them: 110 job boards plus 3 company-website sources: career-site-jobs for 40+ ATS systems, website-job-extractor and website-job-extractor-browser for any other company website). Each links to its standalone Apify actor, which you can also run on its own. RAM is each actor's own actor.json ceiling, the value job-feed budgets against (see RAM and parallelism). Prices are the live pay-per-event rates of each actor; "-" means no priced event.
| Board | Apify actor | Country | RAM (MB) | Pricing per event |
|---|---|---|---|---|
alpha-topjobs-ch | alpha-topjobs-ch-scraper | CH | 128 | start $0.001 |
ams-at | ams-at-scraper | AT | 256 | start $0.05, item $0.005 |
anofm-ro | anofm-ro-scraper | RO | 128 | SERP $0.003, detail $0.005, start $0.001 |
arbeit-swiss | arbeit-swiss-scraper | CH | 512 | start $0.0001, item $0.003 |
arbeitsagentur-de | arbeitsagentur-de-scraper | DE | 512 | SERP $0.008, detail $0.02, start $0.05 |
arbetsformedlingen-se | arbetsformedlingen-se-scraper | SE | 256 | SERP $0.003, detail $0.005, start $0.001 |
bayt-me | bayt-scraper | ME | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
biospace | biospace-scraper | US | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
builtin | builtin-scraper | REMOTE | 128 | start $0.001, item $0.003 |
career-site-jobs | career-site-jobs-scraper | ANY | 128 | start $0.001, career-job-result $0.001 |
carriera-ch | carriera-ch-scraper | CH | 128 | SERP $0.003, detail $0.005, start $0.001 |
charityjob-uk | charityjob-uk-scraper | UK | 128 | SERP $0.0014, detail $0.003, company $0.005, start $0.001 |
computrabajo | computrabajo-scraper | INTL | 128 | start $0.001, item $0.003 |
craigslist-jobs | craigslist-jobs-scraper | US | 128 | SERP $0.003, detail $0.005, start $0.001 |
cryptojobslist | cryptojobslist-scraper | REMOTE | 128 | start $0.001, item $0.003 |
cwjobs-uk | cwjobs-uk-scraper | UK | 128 | SERP $0.0012, detail $0.0025, company $0.005, start $0.001 |
dcz-gov-ua | dcz-gov-ua-scraper | UA | 128 | SERP $0.003, detail $0.005, start $0.001 |
dice | dice-scraper | REMOTE | 128 | start $0.001, item $0.003 |
dribbble-jobs | dribbble-jobs-scraper | US | 128 | SERP $0.003, detail $0.005, start $0.001 |
e-fellows | e-fellows-scraper | REMOTE | 128 | SERP $0.0015, detail $0.003, start $0.001 |
efinancialcareers | efinancialcareers-scraper | INTL | 256 | SERP $0.003, detail $0.005, start $0.001 |
ejobs-ro | ejobs-ro-scraper | RO | 128 | SERP $0.003, detail $0.005, company $0.002, start $0.001 |
escape-the-city | escape-the-city-scraper | UK | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
euraxess | euraxess-scraper | INTL | 128 | SERP $0.003, detail $0.005, start $0.001 |
finn-no | finn-no-scraper | NO | 256 | SERP $0.003, detail $0.005, start $0.001 |
forem-be | forem-be-scraper | BE | 128 | SERP $0.003, detail $0.005, start $0.001 |
france-travail | france-travail-scraper | FR | 256 | SERP $0.003, detail $0.005, start $0.001 |
ge-ch-emploi | ge-ch-emploi-scraper | CH | 128 | SERP $0.003, detail $0.005, start $0.001 |
glassdoor | glassdoor-scraper | INTL | 128 | SERP $0.003, detail $0.002, company $0.005, start $0.001 |
guardianjobs-uk | guardianjobs-scraper | UK | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
hays-uk | hays-scraper | UK | 128 | SERP $0.003, detail $0.005, start $0.001 |
healthecareers-us | healthecareers-scraper | US | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
hellowork-fr | hellowork-fr-scraper | FR | 128 | SERP $0.003, detail $0.005, start $0.001 |
hh-ru | hh-ru-scraper | RU | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
higheredjobs | higheredjobs-scraper | US | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
ictjobs-ch | ictjobs-ch-scraper | CH | 128 | start $0.0001, item $0.003 |
idealist | idealist-scraper | INTL | 128 | start $0.001, item $0.003 |
indeed | indeed-http-scraper | INTL | 256 | SERP $0.003, start $0.001, job-with-description $0.005, company-enriched $0.005 |
infojobs-es | infojobs-scraper | ES | 128 | SERP $0.003, detail $0.005, company $0.002, start $0.001 |
irishjobs-ie | irishjobs-ie-scraper | IE | 128 | SERP $0.003, detail $0.005, start $0.001 |
jobbank-ca | jobbank-ca-scraper | CA | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
jobbasel-ch | jobbasel-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
jobbern-ch | jobbern-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
jobindex-dk | jobindex-dk-scraper | DK | 128 | SERP $0.003, detail $0.005, start $0.001 |
jobkorea-kr | jobkorea-kr-scraper | KR | 128 | SERP $0.003, detail $0.005, start $0.001 |
jobmittelland-ch | jobmittelland-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
jobrapido | jobrapido-scraper | INTL | 128 | SERP $0.003, detail $0.005, start $0.001 |
jobs-ac-uk | jobs-ac-uk-scraper | UK | 128 | SERP $0.0016, detail $0.003, company $0.005, start $0.001 |
jobs-ch | jobs-ch-scraper | CH | 128 | start $0.001 |
jobscout24-ch | jobscout24-ch-scraper | CH | 256 | start $0.0001, item $0.003 |
jobserve-uk | jobserve-uk-scraper | UK | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
jobup-ch | jobup-ch-scraper | CH | 128 | start $0.001 |
jobware-de | jobware-de-scraper | DE | 512 | SERP $0.0037, detail $0.0088, start $0.005 |
jobzueri-ch | jobzueri-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
justjoinit | justjoinit-scraper | PL | 128 | SERP $0.003, detail $0.005, start $0.001 |
kariera-gr | kariera-gr-scraper | GR | 128 | SERP $0.003, detail $0.005, start $0.001 |
karriere-at | karriere-at-scraper | AT | 128 | SERP $0.003, detail $0.005, start $0.001 |
lavoro-cdt-ch | lavoro-cdt-ch-scraper | CH | 128 | SERP $0.003, detail $0.005, start $0.001 |
levels-fyi-us | levels-fyi-scraper | US | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
linkedin | linkedin-scraper | INTL | 128 | start $0.001, item $0.001 |
meinestadt-de | meinestadt-de-scraper | DE | 512 | SERP $0.0035, detail $0.0085, start $0.005 |
michaelpage-intl | michaelpage-scraper | INTL | 128 | SERP $0.003, detail $0.005, start $0.001 |
milkround-uk | milkround-uk-scraper | UK | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
mojposao-hr | mojposao-hr-scraper | HR | 128 | SERP $0.003, detail $0.005, start $0.001 |
myjob-ch | myjob-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
nationale-vacaturebank-nl | nationale-vacaturebank-nl-scraper | NL | 256 | SERP $0.003, start $0.001 |
naukri | naukri-scraper | IN | 4096 | SERP $0.012, start $0.005 |
net-empregos-pt | net-empregos-pt-scraper | PT | 128 | SERP $0.003, detail $0.005, start $0.001 |
nhs-jobs | nhs-jobs-scraper | UK | 128 | SERP $0.003, detail $0.005, start $0.001 |
nicejob-de | nicejob-de-scraper | DE | 512 | start $0.0001, item $0.003 |
nofluffjobs-eu | nofluffjobs-eu-scraper | PL | 128 | SERP $0.003, detail $0.005, company $0.002, start $0.001 |
ostjob-ch | ostjob-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
praca-gov-pl | praca-gov-pl-scraper | PL | 128 | SERP $0.003, detail $0.005, start $0.001 |
prace-cz | prace-cz-scraper | CZ | 128 | SERP $0.003, detail $0.005, start $0.001 |
pracuj-pl | pracuj-pl-scraper | PL | 128 | SERP $0.003, detail $0.005, start $0.001 |
profesia-sk | profesia-sk-scraper | SK | 128 | SERP $0.003, detail $0.005, start $0.001 |
profession-hu | profession-hu-scraper | HU | 128 | SERP $0.003, detail $0.005, start $0.001 |
prospects-uk | prospects-uk-scraper | UK | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
reed-uk | reed-uk-scraper | UK | 128 | SERP $0.003, detail $0.005, start $0.001 |
remoteok | remoteok-scraper | REMOTE | 128 | start $0.001, item $0.003 |
remotive | remotive-scraper | REMOTE | 128 | start $0.001, item $0.003 |
rigzone-us | rigzone-us-scraper | US | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
schaffu-ch | schaffu-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
seek | seek-scraper | AU | 128 | start $0.001, item $0.0025 |
sepe-es | sepe-es-scraper | ES | 128 | SERP $0.003, detail $0.005, start $0.001 |
snagajob-us | snagajob-us-scraper | US | 2048 | SERP $0.012, detail $0.015, company $0.008, start $0.005 |
sozjobs-ch | sozjobs-ch-scraper | CH | 256 | start $0.0001, item $0.003 |
stellenanzeigen-de | stellenanzeigen-de-scraper | DE | 512 | SERP $0.0025, detail $0.0075, start $0.005 |
stellenmarkt-de | stellenmarkt-de-scraper | DE | 128 | SERP $0.003, detail $0.005, start $0.001 |
stepstone-at | stepstone-at-scraper | AT | 512 | SERP $0.0037, detail $0.008, start $0.001 |
stepstone-de | stepstone-de-scraper | DE | 128 | start $0.001 |
subito-it-jobs | subito-it-jobs-scraper | IT | 128 | SERP $0.003, company $0.005, start $0.001, phone-reveal-result $0.001 |
swissdevjobs-ch | swissdevjobs-ch-scraper | CH | 128 | start $0.0001, item $0.003 |
targetjobs-uk | targetjobs-uk-scraper | UK | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
teaching-vacancies-uk | teaching-vacancies-uk-scraper | UK | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
tes-uk | tes-uk-scraper | UK | 128 | SERP $0.004, detail $0.006, company $0.005, start $0.001 |
totaljobs-uk | totaljobs-scraper | UK | 128 | SERP $0.003, detail $0.005, start $0.001 |
trudvsem-ru | trudvsem-ru-scraper | RU | 128 | SERP $0.003, detail $0.005, start $0.001 |
usajobs-us | usajobs-federal-scraper | US | 128 | SERP $0.003, detail $0.005, start $0.001 |
vdab-be | vdab-be-scraper | BE | 128 | SERP $0.003, detail $0.005, start $0.001 |
wayup-us | wayup-us-scraper | US | 128 | detail $0.005, start $0.001 |
web3career | web3career-scraper | REMOTE | 256 | start $0.001, item $0.003 |
website-job-extractor | website-job-extractor | ANY | 256 | job-result $0.004, company-enriched $0.01, start $0.0001, job-result-managed $0.012 |
website-job-extractor-browser | website-job-extractor-browser | ANY | 2048 | browser-job-result $0.008, browser-company-enriched $0.05, browser-page-processed $0.005, start $0.0001, browser-job-result-managed $0.024 |
welcome-to-the-jungle | welcome-to-the-jungle-scraper | FR | 128 | SERP $0.001, detail $0.001, start $0.001 |
wellfound-us | wellfound-jobs-scraper | US | 128 | SERP $0.006, detail $0.01, company $0.005, start $0.001 |
werk-nl | werk-nl-scraper | NL | 128 | SERP $0.003, detail $0.005, start $0.001 |
westjob-at | westjob-at-scraper | AT | 512 | start $0.0001, item $0.003 |
weworkremotely | weworkremotely-scraper | REMOTE | 128 | item $0.003, start $0.001 |
work-ua | work-ua-scraper | UA | 2048 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
workatastartup-us | workatastartup-scraper | US | 128 | SERP $0.003, detail $0.005, company $0.005, start $0.001 |
ycombinator | ycombinator-scraper | INTL | 128 | hn-item-result $0.0005, start $0.001 |
zentraljob-ch | zentraljob-ch-scraper | CH | 512 | start $0.0001, item $0.003 |
Regenerate this table with python3 actors/jobs/job-feed/scripts/gen_board_table.py.
RAM and parallelism
Your Apify account has one memory limit shared by every run on it, not just those job-feed starts, so job-feed budgets sub-actor RAM instead of ignoring it. It first tries to detect the memory actually available (GET /v2/users/me/limits, then GET /v2/users/me). If you set maxTotalMemoryMbytes, the lower of your value and the detected figure wins. If detection is not possible it uses a flat 4096 MB, which fits every plan including Free. job-feed itself adds 256 MB on top.
Each selected board requests the RAM shown in the table above. Boards that fit start immediately, the rest wait, and as RAM frees up the largest waiting request that fits is admitted first so big browser-tier boards are not starved behind small ones. A board whose own need exceeds the whole budget fails fast with insufficient memory budget.
Coming soon
Built and launching shortly: none right now. Every healthy registry platform is already public and selectable (113 of 113).
The job-platform-actors session ships an average of 5+ new platforms per week. Highest-priority job platforms currently in the build queue (auto-generated from docs/build-queue/pipeline.json, top 5 by score, deduped against the registry, corporate single-employer pages excluded -- those are covered by career-site-jobs):
| Rank | Platform | Country | Host | Anti-bot | Notes |
|---|---|---|---|---|---|
| 1 | simplyhired-us | 🇺🇸 US | simplyhired.com | 3.0 | Indeed-owned aggregator |
| 2 | teamblind | 🇺🇸 US | teamblind.com | 1.5 | Anonymous tech community + jobs |
| 3 | kaiser-permanente-jobs | 🇺🇸 US | jobs.kp.org | 3.0 | |
| 4 | boss-zhipin-cn | 🏳️ CN | zhipin.com | 3.0 | |
| 5 | 51job-cn | 🏳️ CN | 51job.com | 3.0 |
719 more jobs-platforms queued behind these (76 require Camoufox / browser-tier). Regenerate this table with python actors/jobs/job-feed/scripts/gen_coming_soon.py.
Limitations
- Some boards sort by relevance only. Without a native date sort,
sortBy: "newest"is a best-effort freshness filter over relevance-ordered results. remotecois parked. remote.co hides employer data behind a paywall, so it is not selectable (see Coming soon).wellfound-uscosts more per result ($0.006 per listing, plus $0.010 with details): every request uses an anti-bot proxy.- URL-only platforms need a URL.
career-site-jobs,website-job-extractor, andwebsite-job-extractor-browserdo nothing withoutcompanyUrls,careerSites, orsearchUrls. If your whole selection is made of them and none is supplied (for exampleincludeCareerSites: truealone), the run is rejected at input validation: exit code 2,statusMessagestarting"FAILED: ", nothing dispatched or billed. - The browser retry has no guaranteed win. It re-runs a zero-job URL through an extractor roughly 3-5x more expensive. It can still return zero jobs (login wall, geofence). See Company career pages.
Input reference
The input schema carries a tooltip for every field. This section covers defaults and the gotchas.
Platform selection
countries[]- ISO codes with a preset (see Country presets). Activates each country's Tier 1 boards.platforms[]- hand-picked board IDs. Additive tocountries. Tier 2 boards run only when named here.includeGlobalBoards- defaulttrue, see Country presets.keywords[]- search terms, each forwarded to every selected board. Results are deduplicated across keywords and boards, andmatched_queryon each row records which keyword found it. AliasessearchQueries,search_queries,queriesfillkeywordswhen it is empty.location- city, region, or state, forwarded in whatever form each board accepts. See Location and language.radiusKm- search radius, default 25, max 200, clamped to each board's own maximum.enforceRadius- defaulttrue. Active only whenlocationis set andradiusKm> 0. Some boards ignore the radius (linkedin,jobscout24-ch,karriere-atand others), so job-feed drops rows clearly farther thanradiusKm(plus a 10% or 3 km margin) from the geocodedlocation, before any charge oronlyNewJobsbookkeeping, using the row's coordinates or itslocation.city. Rows are KEPT when city or coordinates are missing or unknown to the CH/DE/AT/LI place table, the row's country is outside those four, the location is country-level (Schweiz), or the job is remote. A canton or Land (Vaud,Steiermark) is dropped only when its whole area lies beyond the radius. If the search location cannot be resolved or is outside CH/DE/AT/LI, the filter is skipped.run_summary.radius_filterreports{applied, reason, center, radius_km, dropped, kept_unknown}.sortBy-"newest"(default, best for scheduled scrapes),"relevance","salary", or""(forward nothing, board default order). A board that does not know the value ignores it.titleFilter- comma-separated substrings, case-insensitive OR match ontitle, for example"sales manager, account executive". Matching is by substring inside each word, solaboralso findsChemielaborant; the wordsCollaborator,CollaborateurandKollaborationare excluded for terms shorter thancollabor/kollabor. Ten boards filter natively before detail fetch at no extra cost (arbeitsagentur-de,stepstone-de,france-travail,pracuj-pl,indeed,linkedin,sepe-es,prospects-uk,milkround-uk,nationale-vacaturebank-nl); the filter is forwarded to them, and every row from every board is then checked against the same rule here after fetch, counted asdropped_title_filter(the part from native boards also asdropped_title_filter_native_board).postedWithinDays- fractional days (0= off,0.5= 12 hours). Boards with a native age filter get it forwarded (rounded up to their unit), for the rest rows outside the window are dropped after normalization, counted asdropped_stale.searchUrls[]- direct search URLs for advanced use, routed to the board or ATS that matches the URL.jobUrls[]- single job posting URLs for an alive check plus full data, see Direct job URLs.
Direct job URLs (jobUrls)
Paste single job posting URLs (max 500) to check whether they are still online and to get the full job data. Each URL is routed by host to a board that takes detail URLs; one sub-actor run per board.
{ "jobUrls": ["https://www.seek.com.au/job/91878509", "https://www.idealist.org/en/nonprofit-job/abc123-job-title-org-name-city"] }
- Boards supported now:
seek(incl. JobStreet and JobsDB) andidealist. Other URLs are not run and not charged; they are listed inrun_summary.job_urls.unsupportedwithno_detail_support,unknown_hostorinvalid_url. If nothing can be routed the run stops with exit code 1 before any charge. jobUrlsalone runs no keyword search. Combined withcountries,platformsorsearchUrlsit adds the lookups to that run.- Output: normal job rows, each with
job_status(open,expired,unknown) andinput_refsof{source: "jobUrls", field: "jobUrls", index, url}. URLs that return no row get a plain HTTP check (404/410 or a redirect to a listing page =expired), reported inrun_summary.job_urls.status_only, never as a row. - Cost: the normal sub-actor rows, nothing extra.
Result caps
Three caps, applied from the inside out:
maxResultsPerQueryPerPlatform- raw rows per keyword per board. AliasmaxResultsPerQuery. When above 0 it wins over cap 2.maxResultsPerPlatform- raw rows per board across all keywords, before dedup. AliasmaxResults. With several keywords and cap 1 at 0, each keyword getsceil(cap / keywords)(3 keywords against 10 gives 4 each, so up to 12 raw rows). 0 = each board's own default.maxResultsTotal- unique jobs delivered after dedup, across all boards. Once reached, job-feed stops accepting and billing further jobs. Sub-actor calls already running still finish, but their extra rows are dropped and never billed by job-feed. 0 = unlimited.
To avoid paying for rows it would then discard, job-feed also clamps the per-query cap sent to each board to max(1, floor(maxResultsTotal / (boards * keywords))). If a run mixes boards with companyUrls, the boards see half of maxResultsTotal for this split.
On a companyUrls or careerSites run, maxResultsPerPlatform is a per-extractor budget split across its companies with max(1, floor(cap / companies)). Six rows over 2 companies gives 3 each (forwarded as maxJobsPerCompany to career-site-jobs and maxResultsPerCompany to the extractors). With more companies than the cap, every company still gets 1, so the total can run slightly over budget in that one case. Rows beyond a company's share are trimmed, never billed by job-feed, and counted as dropped_over_cap (see Company career pages).
Content options
includeJobDetails- fetch the full description, salary, and requirements from each detail page. Some boards charge more per detail than per SERP row. Off by default. WithcompanyUrlsorcareerSitesit also reads each posting's employer page, not only board detail pages (job-detail-resultevent per posting, once priced).detailCountries- ISO-2 codes. WithincludeJobDetails, career-site-jobs reads detail pages only for rows of those countries; other rows are still returned and charged. Invalid entries are ignored with a warning.maxResultsPerCompany- per career site:-1= every job (career-site-jobs: up to 5000 per company), a positive number caps each company,0keeps the current behaviour (500 per company when no cap is set).includeContactData- fillcontacts[](recruiter name, email, phone) where the source has them. ImpliesincludeJobDetails. About 28 boards expose contacts.includeCompanyDetails- fetch company profiles (about,industry,size_range,social_urls,active_jobs, and more). ImpliesincludeJobDetails. About 28 boards support it.includeCompaniesWithoutJobs- defaulttrue, adds free zero-job company rows, see Company career pages.minRelevanceScore- 0-100, default 0 (keep all). Each keyword is scored as the share of its tokens found in the title (case and umlaut folded), and a row scores as its best keyword. 50 drops rows where fewer than half of the best keyword's tokens match, 100 keeps only rows where every token of some keyword matches.excludeAgencies- defaultfalse. Drops rows whosecompany.is_agencyis true (staffing, executive search, job aggregators in the DACH agency registry at high or medium confidence, or a name with Zeitarbeit, Personalvermittlung, Personalberatung and similar) before they are charged or marked seen byonlyNewJobs.licence_onlyfirms are never dropped. The count isagencies_excludedinrun_summary.includeReportRowsInDataset- defaultfalse, see Terminal items in the OUTPUT record.
Company career pages
Three sub-actors can read a company career page:
| Actor | Reads | Browser or LLM | Roughly |
|---|---|---|---|
career-site-jobs | Pages on a known job system (Greenhouse, Workday, Lever, Workable, and 35+ more) | No | $0.001/job |
website-job-extractor | Any career page over plain HTTP | LLM, managed by default | $0.012/job managed, plus $0.01/company |
website-job-extractor-browser | Any career page, including JS-only apps | Real browser and LLM | roughly 3-5x the HTTP rate |
career-site-jobs is always cheapest when it applies. Inputs:
companyUrls[]- one career-page URL per company. A URL matching a known ATS fingerprint goes tocareer-site-jobsdirectly (regardless ofplatforms, no API key needed), anything else to the extractor. Via the API an item may also be an object{"url": "...", "country": "DE", "company": "..."}(countryandcompanyoptional;countryworks like oncareerSites).careerSites[]- companies whose job system you already know, for example fromnext_run_input. Each entry is a plain URL (auto-detected) or{"url": "...", "ats": "...", "company": "..."}; optionalwebsite,employer(shared group boards such asvitrea-gesundheit.onlyfy.jobs) andcountry(ISO alpha-2 default for rows without a location). When you setcountriesand an entry has nocountryof its own, job-feed sends your countries to career-site-jobs on every company query (one country ascountry, several ascountries): rows of known other countries are dropped before they are charged, rows with an unresolved location are kept with country null. The same applies tocompanyUrlsand directory-sourced companies. A per-URLcountry(oncompanyUrlsobjects orcareerSites) also applies to rows read by the website extractor, including handover and fallback rows: rows whose location country is known and different are dropped before they are charged and counted indropped_off_country; rows with an unknown country are kept. Your ownlocationis forwarded unchanged. Supportedatsvalues are in the input schema. Works on its own.companyList- pick employers from the built-in company directory instead of listing URLs. Filters (all optional, AND across fields, OR within a list):regions(ch-de,de),industries,subIndustries,countries,headcount(1000+,250-1000,50-250,<50,unknown), pluslimit(default 50, maximum 300, taken in alphabetical order of domain). An empty object, or one that matches nothing, is ignored with a warning when other inputs select platforms; on its own it ends the run before anything is dispatched or billed (Invalid input: companyList ..., exit code 1). Rows from these companies carryinput_refsentries{source: "companyList", field: "companyList", domain, index}(indexis the position in the expanded list). Companies on a job systemcareer-site-jobsreads go there, the rest go to the extractor likecompanyUrls; companies you already list yourself are skipped. The directory is an embedded snapshot (dated inrun_summary.company_list.snapshot_date, currently 2026-10-21) that refreshes with each build. See Company lists.includeCareerSites- defaultfalse. Turns oncareer-site-jobswithout a matching URL. It has no keyword search: with nocompanyUrls,careerSitesor ATS-matchingsearchUrlsthe run is rejected, see Limitations.includeJobDetails(aliasincludeFullPostingText),maxFullTextChars(default 12000, 500 to 50000),fullTextLlmFill- for pages read by the extractor, each posting's detail page is read (text incl. PDFs indescription_md, deadline, salary, contact, workload, languages).fullTextLlmFill: falseskips the LLM fill call.
keywords, location, postedWithinDays and sortBy are forwarded to all three and applied server-side before the per-company cap and before charging. When a company's jobs legitimately do not match, its per_platform[] entry carries a company_notes[] line and it is not retried in the browser.
Zero-job company rows. A company read successfully with no open roles (or none matching) is one dataset row with _type: "company_no_matching_jobs": the company (name, domain, url, career_page_url, ats_detected), jobs_on_site (a number only when the site-wide total is known, else null), jobs_matching: 0, a reason (no_jobs_found, filtered_out_by_filters, no_location_match_on_site), a note, the filters and scraped_at. Never charged, not counted in unique_jobs. Turn includeCompaniesWithoutJobs off for job rows only, or filter on _type.
Picking HTTP or browser. For URLs matching no known ATS, naming only website-job-extractor in platforms[] means HTTP only (a JS-rendered page returns a zero-row reason); only website-job-extractor-browser sends every such URL to a real browser; both or neither (default) means HTTP first, then a scoped browser retry for companies flagged JS-rendered or with an unexplained zero. Companies that returned jobs or whose zero your filters explain are never re-sent. The legacy enableBrowserFallback: true forces the default mode. The retry gets max(subactorTimeoutSecs, 1800) seconds (capped by the run's remaining time minus 90 s); on timeout rows already pushed are kept as TIMED-OUT, and the zero-row reason is replaced by the retry's outcome.
Company lists
companyList reads every matching employer of the built-in directory (about 1,100 Swiss and German employers), so you need no career URLs. Use it for sector sweeps.
{ "companyList": { "regions": ["ch-de"], "industries": ["biotech"], "limit": 50 }, "maxResultsPerCompany": 20 }
Output: normal job rows with an input_refs entry {source: "companyList", domain, index}, and run_summary.company_list = {matched, used, capped_at, snapshot_date}. Cost: the lookup is free, each company costs the normal sub-actor price.
Details: limit default 50, maximum 300; a filter matching nothing is ignored with a warning, or ends the run before billing if it is the only input; the snapshot refreshes with each build.
Handing a job system to the next run. Rows read through a known ATS carry ats_system and ats_url. The OUTPUT record's next_run_input moves each company whose job system was found from companyUrls to a careerSites entry, for example {"url": "https://kenvue.wd1.myworkdayjobs.com/en-US/kenvue_careers", "ats": "workday", "company": "kenvue.com", "website": "https://www.kenvue.com/careers"}. It carries only {companyUrls, careerSites}: merge them into your input and keep your own limits, otherwise each site is read in full. Example, Chewy (264 roles): about $3.20 via the managed extractor, then about $0.27 per run via career-site-jobs.
When career-site-jobs cannot read a company. If it ends unreadable (invalid_query, board_not_found, unsupported_platform, blocked, parse_error, connection_error, or a failed sub-run), job-feed falls back to the extractor in the same run; those rows are deduplicated, capped and billed like any extractor row. run_summary.csj_fallback reports {attempted, recovered, rows, companies, skipped_reason}. Turn off with csjFallbackToExtractor: false. The reverse also runs: when the extractor recognises a supported job system and finds 0 jobs, job-feed sends the company to career-site-jobs once (run_summary.wje_handover; turn off with sameRunHandover: false). A careerSites entry failing for a structural reason moves back to next_run_input.companyUrls (listed in handover_demoted); temporary failures keep it. With handoverSharedBoards on (default), shared group boards are handed over with the employer found. A bare vendor root names no company (job-boards.greenhouse.io/, career2.successfactors.eu, app.jazz.co, myworkdayjobs.com, a Prospective careercenter without its id): it is never sent to career-site-jobs, so it is not charged, and it is listed in companies_not_read as vendor_root with the reason. If the careerSites entry carries the company's website (or a domain in company), that site is read by the extractor instead. A careerSites entry without a url is ignored with a warning; demo tenants are never handed over. If a sub-actor reports a different job-system list version, run_summary.manifest_mismatch lists it. Any other unexplained zero (for example js_rendered_no_browser) is listed in run_summary.companies_not_read, so no requested company disappears silently.
Managed LLM key
Without your own openrouterApiKey, job-feed forwards no key into website-job-extractor runs. It tags the run with NanoScrape's managed key profile, and the extractor resolves the key server-side and bills at its own managed rate, see that actor's README for current prices and caps. Supplying openrouterApiKey (or the geminiApiKey / groqApiKey fallbacks) switches the run to standard billing on your own account.
Location and language
location is free text and most boards accept it as-is. Boards with a closed list, or no location field, are resolved by job-feed when possible:
swissdevjobs-chtakes only its own city names ("Zurich", not"Zürich"); job-feed matches case- and accent-insensitively.arbeit-swissneeds a 2-letter canton code, resolved from any canton or main city.sozjobs-ch(7 German-Swiss macro-regions, French and Italian cantons intentionally no match),alpha-topjobs-ch(6 JobCloud region IDs, Ticino none) andlavoro-cdt-ch(Ticino only: Lugano, Mendrisio, Locarno, Chiasso) use closed lists resolved via the canton.healthecareers-usneeds a full US state name (abbreviations and"City, ST"are resolved),craigslist-jobsregional site slugs for major metros.jobs-ch,ictjobs-chand the three company-site actors take free text.ge-ch-emploihas no location field and lists only Geneva roles.
When a mapping fails, or the board has no location field, job-feed logs a warning and post-filters that board's rows instead: rows whose own location text does not contain your location (or have none) are dropped, never billed, and counted as dropped_off_location. Rows flagged remote are kept when they name no country or a matching one. This only triggers when you set location, never on top of a successful native filter. The three company-site actors are always post-filtered as a safety net.
language on each row is inferred. Swiss listings use a canton heuristic (Geneva, Vaud, Fribourg, Neuchatel, Jura, Valais to fr, Ticino to it, else de); otherwise a country-to-language table (DE/AT to de, FR to fr, US/GB/CA/AU/IE/IN/PK to en, and about 20 more); otherwise a conservative stopword guess over title and description. With no signal it is "", never a default "de".
Dedup mode
streaming(default) - rows are pushed as each board finishes. The first board to report ajob_idowns the row, later reports go intosources_manifestunderadditional_sources_by_job_id.sources[]has length 1. Best for real-time use, fastest first result, and aborts (you pay only for rows already delivered).batch- rows are buffered until all boards finish, then emitted once perjob_idwith every reporting board insources[]. No partial delivery if the run is killed, slower first result, more memory.
Billing is identical: one event per unique job_id. See How job IDs and dedup work.
Only new jobs
Off by default. Set onlyNewJobs: true on a scheduled run and job-feed skips jobs an earlier run already delivered: they are not pushed and not charged the orchestrator-unique-job fee. Delivered job_ids are kept in a named key-value store on your own account (seenJobsStoreName, default job-feed-seen-<hash of your search input>) and forgotten after seenJobsTtlDays (default 30, max 180). The first run delivers everything. run_summary.only_new shows seen_skipped, new_pushed, store_size and ttl_days.
You still pay the sub-actor fees for repeats, because the platforms run before job-feed knows a job is old. postedWithinDays stays your main cost control.
Job lifecycle across runs
trackJobLifecycle: true on a scheduled run with a fixed input tells you how long each job has been online and which jobs closed since the last run.
{ "countries": ["CH"], "keywords": ["data engineer"], "trackJobLifecycle": true, "seenJobsStoreName": "my-ch-data-jobs" }
Output: each row gets lifecycle: {first_seen_at, last_seen_at, days_online, runs_seen}; the OUTPUT record gets closed_jobs (job_id, title, company, url, first_seen_at, last_seen_at, days_online). Cost: free, only small KV storage on your account.
Details:
- Closed jobs are never pushed or charged; at most 500 are listed (
closed_jobs_truncatedfor the rest). - Closed means "not seen in a complete, uncapped run of the same search". Closed detection runs only if the search settings (including
postedWithinDays) equal the last tracked run's. It is checked per platform: jobs of a platform that failed, was partial or hitmaxResultsTotal,maxResultsPerQueryPerPlatformormaxResultsPerPlatformare never closed. When no platform was covered completely,run_summary.lifecycle.closed_detectionisskipped_incompleteandclosed_countis 0;closed_detection_sources_skippedlists the platforms left out. A platform with no cap set by you is judged against the sub-actor's own default result limit, and counts as incomplete if that limit is unknown. Jobs stored by builds before 0.9.148 are never closed by absence. Builds 0.9.144 to 0.9.147 could list still-open jobs as closed on capped runs. - History sits in the
onlyNewJobsstore (LIFECYCLE-*records); unseen entries drop after 120 days, newest 10,000 jobs kept. Deleting the store resets it (and theonlyNewJobslist).
Company hiring summary
Free, off by default. Set companySummary: true to get one summary per company from the jobs of this run: open roles, boards, top countries and cities, roles by function and seniority, newest and oldest posting, median days since posted. It is written to the key-value store only (no dataset rows, no extra charge): the top 200 companies are in the OUTPUT record under company_summary, up to 5,000 in the COMPANY_SUMMARY record. Add companySummaryStoreName to keep a history and get trend per company. With onlyNewJobs, open_roles counts only this run's delivered rows.
{ "countries": ["CH"], "keywords": ["QA"], "companySummary": true, "companySummaryStoreName": "my-hiring-trends" }
Company hiring signals (outputMode companies)
Set outputMode to companies to get one company_hiring row per companyUrls entry instead of job rows. It answers "is this company hiring, and for what?" for a list of websites.
{"companyUrls": [{"url": "https://www.example.com", "company": "Example GmbH", "country": "DE", "ref": "ChIJN1t_tDeuEmsRUsoyG83frY4"}],"outputMode": "companies","maxResultsPerCompany": 5,"includeGlobalBoards": false}
ref is an optional string (up to 200 characters) on a companyUrls object. It is echoed unchanged as ref on the company row, as company.ref on every job row of that URL (jobs mode too), and in input_refs[].ref, including rows found through a same-run handover, the career-site-jobs to extractor fallback and the browser retry.
| Field | Meaning |
|---|---|
ref | The ref you sent (null if none). |
company, website | As sent. |
hiring_checked | false only for not_read. |
hiring_status | hiring, no_open_roles or not_read. |
hiring_open_roles_count | The site-wide number of open roles when the reader reports one for this request, else the distinct roles read. null for not_read. |
hiring_open_roles_count_is_capped | true only when no site total is known and the roles read equal maxResultsPerCompany. |
hiring_roles | Up to maxResultsPerCompany titles. |
hiring_roles_detail | Same order and length as hiring_roles: {title, location_raw, city, postal_code, country} (null for unknown parts). [] unless hiring. |
hiring_evidence | hiring: ats_board, careers_page or job_board (in that precedence). no_open_roles: ats_zero, careers_page_empty or no_careers_page. null for not_read. |
hiring_sources | ATS names (greenhouse, lever, workday, ...) or website. |
hiring_jobs_url | The careers page or ATS board, else null. |
hiring_checked_at | ISO 8601 time. |
not_read_reason | Why the company was not read, else null. A repeated URL keeps its first entry; the repeat gets not_read with duplicate_url. |
How the mode behaves:
- Only
companyUrlsare read.countries,platforms,searchUrls,jobUrls,careerSites,companyList,includeCareerSitesand the job boards are ignored with a warning. Use the per-URLcountryto keep only roles in that country; it is applied before any charge. includeCompaniesWithoutJobsis implied. No job rows and nocompany_no_matching_jobsrows are pushed.onlyNewJobsandtrackJobLifecycleare ignored with a warning.companySummarystill works.- Billing is unchanged: the
orchestrator-unique-jobevent is charged once per unique role read (at mostmaxResultsPerCompanyper company), after the company row has landed. Thecompany_hiringrow itself is free.run_summary.companies_modereportscompanies,hiring,no_open_roles,not_readandroles_charged, which equals the events charged. Sub-actor fees are billed as usual. hiringBrowserFallback(defaultfalse) decides whether career sites that need a browser (JS-rendered) are retried. A browser read costs about $0.18 per place versus $0.01-0.03 for normal reads, so by default those places getnot_readwithnot_read_reasonneeds_browser,hiring_checkedfalse and no charge.run_summary.companies_mode.needs_browsercounts them (a subset ofnot_read). Set it totrueto retry them in a browser. It is ignored in jobs mode.- A homepage that was fetched fine but has no careers, jobs or job-system link is
no_open_roleswithhiring_evidenceno_careers_page(count 0, no reason). If the fetch failed or is unknown, it staysnot_read(dns_errormeans the domain does not resolve). None of these is retried in a browser. - Websites on directories, social and profile sites (jameda.de, facebook.com, yelp.*, local.ch, ...) are not read and cost nothing:
not_readwithnot_read_reasondirectory_profile_url. In jobs mode they are skipped with a warning. not_read_reasoncharge_limitmeans the source was not started because your max charge was used up.not_read_reasonats_handover_availablemeans the site uses a job system we could not read in this run. When the same-run handover to the job-system reader succeeds, the company is read and getshiringorno_open_rolesinstead.- In companies mode
maxResultsPerCompanyis at most 50 (also the number of titles inhiring_roles); higher values and-1are lowered to 50 with a warning. If two entries resolve to the same board, both list the role and it is charged once.hiring_jobs_urlfalls back to the listing page the roles were read from. - A company with roles is pushed as soon as its sub-actor result is processed. Companies without roles are pushed when the run's fallback and browser passes are done, because those passes can still find roles.
- With a country, keyword, location, posted-within or title filter active, the reader's site total would count roles you filtered out, so
hiring_open_roles_countis then the roles read andhiring_open_roles_count_is_cappedtells whether the cap was hit.
Advanced concurrency
maxParallelPlatforms- sub-actor runs kept in flight at once (default 8, 1-32). Apify's concurrent-run limit is account-wide, so job-feed lowers this cap to fit it (a free account runs 3 at a time). A402 concurrent-runs-limit-exceededon start is retried with backoff.perHostConcurrency- concurrent runs sharing a host (default 2, max 6).globalConcurrency- legacy in-flight cap (default 0, max 16); the lower of it andmaxParallelPlatformswins.maxTotalMemoryMbytes- sub-actor RAM budget, 0 = 4096 MB. See RAM and parallelism.subactorTimeoutSecs- per-board timeout (default 600, 60-3600). The scoped browser retry has its own longer budget.
Partial failure and statusMessage
statusMessage summarizes the run ("14/14 platforms succeeded", failed boards, the dedup summary, a reason when a healthy board returned nothing, "+ 2 companies with no matching jobs", any dropped_over_cap). Partial failure still ends SUCCEEDED with every job the working boards returned.
A board job-feed stops waiting on is gracefully aborted and its pushed rows are kept through normal dedup and billing. Its per_platform[] entry reports status: "TIMED-OUT" with the salvaged raw_results and a reason; it counts as neither succeeded nor failed. Near the run timeout, boards that cannot usefully start are skipped (SKIPPED_DEADLINE) and running boards are stopped shortly before the deadline (run_summary.stopped_for_deadline, deadline_stopped_boards), see Run timeout.
Two cases end FAILED with statusMessage starting "FAILED: ": exit code 2, selection rejected at validation (only URL-only platforms and no URL), nothing billed; exit code 1, every dispatched platform failed and nothing was delivered. A board that only finished through a salvaged TIMED-OUT run does not count as succeeded, but if it delivered and billed a real row the run is not failed on that basis.
LLM keys
All optional, used only when a URL is routed to website-job-extractor or its browser variant. Leave them blank for managed mode (see Managed LLM key). The forced primary model is deepseek/deepseek-chat-v3-0324.
openrouterApiKey- primary key, standard BYOK billing on your account.geminiApiKey- cross-provider fallback, used only if the whole OpenRouter side fails.groqApiKey- second fallback (Llama-3.3-70b), after Gemini.
Output reference
Key fields only. The dataset schema (Output tab in the Console) lists every field, and coverage varies per board. Rough coverage: salary about 70 boards, description_text about 60, expires_at about 45, requirements about 30, experience_level about 25, skills about 20, contacts about 28, workload_pct CH and SE boards, and technologies only about 6 (swissdevjobs-ch, workatastartup-us, nofluffjobs-eu, justjoinit, welcome-to-the-jungle). location.postal_code appears only where the board emits one.
Job-level fields
| Field | Type | Description |
|---|---|---|
job_id | string | First 16 hex chars of SHA-256 over folded title, company, city. Stable across runs and boards. Same-board collisions get an 8-hex suffix. |
job_id_aliases | string[] | job_id plus the ids of rows merged into this posting when it was pushed. A merged posting keeps the first arriving row's id, so it can return under another job_id next run: check [job_id, ...job_id_aliases] against what you have seen. Rows merged later are in sources_manifest.additional_job_id_aliases_by_job_id. |
title | string | Job title, HTML stripped. |
company | object | See below. |
location | object | raw, city, region, country (ISO alpha-2, UK always GB), remote (onsite, hybrid, remote), postal_code. |
employment_type | string | Closed enum: permanent, temporary, contract, internship, apprenticeship, or "". A raw board value that maps to none of these becomes "". |
workload_pct | object | {min, max} as 0-100, for CH and SE boards. |
salary | object | {min, max, currency, period, text, source}, never currency-converted. source is stated or hidden_field (a band for an ad that hides its salary). |
posted_date, expires_at | string | ISO8601 posting date and application deadline. |
job_status | string | Canonical posting status: open, expired or unknown (when the board reports none). Mapped from the board's job_status, is_active, expired or is_expired. |
job_url_status | string | Career-site-jobs only: advertisement_off_apply_url means job_url is the apply page; portal_frontend_off means it does not open. |
description_text, description_html, description_md | string | Full description in three forms. Needs includeJobDetails on most boards. |
requirements, skills, technologies | string[] | Structured lists where the board provides them. |
department, experience_level, education_level, industry | string | Organizational and seniority metadata, board-dependent. |
apply_url | string | Application URL. |
contacts | object[] | Recruiter contacts (first_name, last_name, email, phone, position, raw, source_platform), needs includeContactData. source_platform is the board a contact came from and survives merging of duplicate rows. |
matched_query | string | The keyword that surfaced the row. If a board decorates it (accountant in Dubai), job-feed rewrites it back to your plain keyword. |
relevance_score, relevance_matched_tokens | float, string[] | Share of keyword tokens found in the title (0.0-1.0, 1.0 when no keywords were given), and which ones. |
sources | object[] | Origin list (platform, url, source_id, first_seen, ...). One entry in streaming mode, all in batch. |
first_seen, last_seen, content_hash | string | Run timestamps and a hash over mutable fields for cross-run change detection. |
lifecycle | object | Only with trackJobLifecycle: first_seen_at, last_seen_at, days_online, runs_seen across runs. See Job lifecycle across runs. |
language | string | Inferred listing language, "" when unknown. |
_platform | string | The board that first reported the row. |
application_deadline, reference_number, is_easy_apply, street_address, working_hours, languages | mixed | Detail fields from the website extractor with includeJobDetails. Omitted when not reported. |
updated_at, start_date, end_date, starts_immediately, work_forms, locations, hours_per_week | mixed | Source dates and work details when a board reports them. updated_at is the source's refresh date (jobbasel-ch), posted_date stays the first publication. start_date, end_date (ISO), starts_immediately and work_forms come from arbeit-swiss; an empty work_forms means none, absent means not reported. locations lists every city of a multi-city ad (stepstone-de, stellenanzeigen-de). hours_per_week is {min, max} (stellenanzeigen-de); the text as written is in working_hours. Omitted when not reported. |
hiring_organization | string | The employing entity when it differs from the board's company (for example company Johnson & Johnson, hiring_organization Cilag AG), as reported by career-site-jobs or another sub-actor. Omitted when empty. On a shared group board each row keeps its own company.name; the group entry you gave stays in input_refs. |
input_refs | object[] | Only on rows that came from a careerSites or companyUrls entry: which entries, as {source, index, url, company, employer} (source is careerSites or companyUrls, index is the 0-based position in that input array, url, company and employer are your values verbatim, null when not given). An array because the same job found through two entries lists both. Use this, not company.name, to attribute rows to your input: company.name is the employer's display name (detected names replace your label, boards shared by several brands carry the brand). Omitted on board and keyword rows. |
ats_system, ats_url, ats_tenant, career_page_url | string | Job-system provenance (for example workday, greenhouse, phenom), empty on board rows. ats_tenant and career_page_url come from the extractors only, and ats_url is a best-effort match for career-site-jobs. Distinct from company.ats_detected, a best-effort guess made for every board. See Company career pages. |
Company sub-fields
| Field | Type | Description |
|---|---|---|
name | string | Company name. On career pages: your careerSites label, else the page's declared name, else a cleaned brand from the domain (new.abb.com becomes ABB). Every row of one company, including its zero-job row, uses the same name, and company.domain keeps the real domain. |
url, domain, logo_url | string | Own website, registrable domain, logo. |
about, industry, size_range | string | Profile text, sector, headcount bucket. |
employee_count_from, employee_count_to | integer | Headcount range bounds, to null if open-ended. |
rating, review_count | float, integer | Employer rating (scale varies) and review count. |
founding_year | integer | Four-digit year. |
benefits | string[] | Perks list. |
phone, email, headquarters | string | Company-level contact data. |
address | object | {street, city, region, postcode, country}. |
profile_url, career_page_url | string | Board's company profile (first reporter only) and the company's own careers page. |
ats_detected | string | ATS recognized from apply_url. |
social_urls | object | Links keyed by network. |
active_jobs_count, active_jobs | integer, object[] | Open-role count, and up to 20 {title, url} entries. |
recruiters | object[] | Recruiter roster (dice only): name, title, email, profile_url. |
is_agency | boolean | Always present. True when the company is in the DACH agency registry with high or medium confidence, or its name has an agency keyword (Zeitarbeit, Personalvermittlung, Personalberatung, Staffing, Recruit). |
agency_type | string, null | staffing, executive_search, rpo, job_aggregator, engineering_services or placeholder; null when unknown. |
agency_confidence | string, null | high, medium, licence_only or keyword. licence_only firms only hold a staffing licence, often as a side business. |
is_agency_signal | boolean | True for any registry hit, including licence_only, or a keyword hit. Use it for scoring, not hiding. |
platform_extras | object | Passthrough of board-specific company_* fields. |
Terminal items in the OUTPUT record
Every run produces four _type-tagged items: run_summary, cost_report, relevance_report, sources_manifest. By default they live only in the run's OUTPUT key-value-store record, next to a live-updated subruns[] manifest (one entry per board: actor, build, run_id, console_url, dataset_id, status, timing, memory, charged amount, charged_event_counts). If the run had companyUrls or careerSites, OUTPUT also carries next_run_input. Set includeReportRowsInDataset: true to also append the four items to the dataset; if you export to CSV or Excel, drop rows with a non-empty _type (or keep only rows that have a job_id), since they do not share the job schema.
run_summary- totals,dedup_summary,completeness, andper_platform[]withplatform_id,status,raw_results,elapsed_ms, plus when relevantcountry(global-board sub-runs),reason,dropped_over_cap, anddropped_off_location,agencies_excludedwhenexcludeAgenciesis on, andradius_filterwhenenforceRadiusran.companies_no_openingslists companies whose career page was read and says there are no vacancies (they are not incompanies_not_read).llm_failures(companies,calls,entries, and arerun.companyUrlsfragment to paste) lists companies where extraction calls failed, including partial failures (error_category"partial"); a company with 0 jobs and failed calls also stays incompanies_not_readwith reasonllm_failed. Both need a website-job-extractor build that reportsjobs_status. Alsocompanies_not_read(withcompanies_not_read_count), the list of requestedcareerSitesandcompanyUrlsentries that gave no jobs because they could not be read; alsocompanies_without_jobs,companies_partially_read(company, url, platform_id, reason, rows, input_refs) lists career sites that returned rows but were cut short by the time budget or a wind down, never also incompanies_not_read; and on early stopsstopped_for_deadlineanddeadline_stopped_boards. Company career-site rows now getlocation.countryfrom the location text when it names one ("CH", "CHE Baden", "Schweiz - Burgdorf", "Brazil: Rio de Janeiro"); rows with no location stay empty and are counted incompany_rows_without_location. Rows from acompanyListentry use the directory name instead of a host-derived one.cost_report-total_cost_usdand aper_platform[]breakdown, see Billing and cost model.relevance_report- per-board on-topic rate, a hint to drop boards that return off-topic rows.sources_manifest- cross-board origins for jobs seen on several boards (streaming mode). It also carriesadditional_job_id_aliases_by_job_id(job_id to the ids of rows merged after that row was pushed). In streaming mode it also carriesadditional_input_refs_by_job_idfor a job already delivered that a secondcareerSitesorcompanyUrlsentry found later.
input_refs (same {source, index, url, company, employer} objects) also appears on every companies_not_read entry, no-match row, and csj_fallback.companies[] and wje_handover.companies[] entry. The OUTPUT record's next_run_input_refs ({companyUrls: [...], careerSites: [...]}) maps each item of next_run_input (same index) to the input entries it descends from; it is kept outside next_run_input so that fragment stays pasteable.
How job IDs and dedup work
job_id is the first 16 hex characters of SHA-256(foldText(title) | foldText(company) | foldText(city)). A job with no city uses region and country instead, so remote listings do not collide on an empty city. The same job at the same company in the same city hashes identically from any board, today or next month. Upsert on it.
foldText normalizes case, whitespace, umlauts, gender suffixes ((m/w/d), (f/m/x), (a), :in), trailing workload markers (", 80-100%"), and dash variants.
When matching the same job across boards, titles that differ only by gender tags anywhere in the title ((m/w/d), (all genders)), inclusive endings (*in, :in, /in, Innen, French (e)), pensum (100% against 80-100%), a job reference number, (unbefristet) or a trailing city are treated as one title, while two explicit pensum ranges that do not overlap (50% against 100%), Senior against Junior, and trainee or apprentice titles stay separate rows; job_id and the shown title are unchanged. Cross-board matches are folded into one row. Same-board postings with identical folded title, company, and city are never merged: every posting after the first gets an 8-hex suffix from its own source_id (b39d05cf0914e480 and b39d05cf0914e480-2e000388). Two distinct USAJOBS "Accountant (Staff)" postings at Fort Belvoir are a real case.
Every run's run_summary and statusMessage carry a line like
12 rows from boards → 11 unique jobs (1 merged: arbeit-swiss ×1)
The parenthetical lists boards that contributed merged rows, most first. On a companyUrls run it can also show trimmed rows: 29 rows from boards → 12 unique jobs (0 merged, 17 over cap).
Billing and cost model
job-feed bills one event, orchestrator-unique-job ($0.0005), per unique job delivered after dedup and maxResultsTotal. Rows dropped before delivery (caps, titleFilter, postedWithinDays, location post-filter, onlyNewJobs repeats, zero-job rows) are never billed by job-feed.
Every board job-feed calls is a separate actor with its own pay-per-event rates (SERP scans, detail fetches, contacts, LLM extraction), billed independently to the same Apify account at the same rate as running it standalone. When you set a max charge, it also limits the sub-actors job-feed starts for you: each gets a share of what is left, and a source that would get too little is skipped (run_summary.charge_budget_exhausted, subruns_skipped_for_budget). A sub-actor may bill its own events for rows job-feed later trims. On companyUrls and careerSites runs the sub-actor's company fee applies whenever the career site was read successfully, even with no open jobs.
The cost_report in the OUTPUT record separates what compute cost from what you were billed:
per_platform[].user_charged_usd- the sub-run's usageTotalUsd from Apify (compute cost, not the event bill, on the actor owner's own account), else events times list price capped at its max charge. Re-read 3 times, 10 s apart.charges_final: falsemeans the budget ran out and the figure may be a slight undercount.per_platform[].price_basis-""normally,"free_tier"(tiered event priced at the conservative FREE tier) or"actor_fallback"(no pricing info). Either means a conservative estimate.estimated_cost_usdis a registry-based fallback;platform_compute_usdis the sub-run's CU cost for internal monitoring and not part of your bill.orchestrator_charged_events,subactor_cost_total_usd,orchestrator_cost_usd(unique_jobs x $0.0005),total_cost_usd(their sum) andcost_per_unique_job_usd.overlap_matrix/redundancy_hint- which boards return the same jobs, and which to drop without losing coverage.
Rate card
| Event | Trigger | Price |
|---|---|---|
orchestrator-unique-job | Each unique job emitted after dedup and the optional maxResultsTotal cap | $0.0005 |
Sub-actor events are billed by the sub-actors on top, see Supported boards and the worked examples in What it costs. A real DE-Munich Software Engineer run over 4 boards (25 SERP rows each, no detail fetch) gave 85 unique jobs, $0.478 sub-actor subtotal plus $0.042 orchestrator fee: $0.522 total, $0.006 per unique job.
Examples
All inputs are literal and can be pasted as is.
- Single country:
{"countries": ["CH"], "keywords": ["software engineer"]}fires 6 boards, the CH preset (jobs-ch,arbeit-swiss,jobscout24-ch) plus Indeed, Glassdoor and LinkedIn scoped to CH. - Several countries:
{"countries": ["DE", "AT"], "keywords": ["software engineer"], "maxResultsPerPlatform": 5}fires 14 boards. The cap of 5 splits as[3, 2]across the two countries for the global boards (AT first), while ordinary boards get the full 5. - Preset plus a board:
{"countries": ["DE"], "platforms": ["wellfound-us"], "keywords": ["backend engineer"], "includeGlobalBoards": false}.platformsis additive: 5 DE boards pluswellfound-us. WithoutincludeGlobalBoards: false, 9 in total. - Several keywords:
{"platforms": ["swissdevjobs-ch", "ycombinator"], "keywords": ["react", "engineer"]}. Each row keeps the keyword that surfaced it inmatched_query. - Company career pages:
{"companyUrls": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/figma", "https://www.amazon.jobs/en/search?base_query=software+engineer"], "keywords": ["backend"]}. The first two match ATS fingerprints (career-site-jobs), the third goes towebsite-job-extractorin managed mode. - Location:
{"countries": ["CH"], "keywords": ["engineer"], "location": "Zürich"}.jobs-chtakes the text,arbeit-swissresolves cantonZH,jobscout24-chits own region enum. A board that cannot honor it (for examplege-ch-emploi, Geneva only) is post-filtered: mismatches are dropped uncharged and reported asdropped_off_location. - Newest, last day:
{"countries": ["UK"], "keywords": ["product manager"], "postedWithinDays": 1, "sortBy": "newest"}. - Empty input
{}applies the code defaultscountries: ["DE"],keywords: ["Software Engineer"],maxResultsPerQueryPerPlatform: 10,maxResultsTotal: 50(Apify's automated QA runs{}, and a schemaprefillis only a Console hint). That is 8 boards sharing 50 rows, each capped at 6. - Per-company cap:
{"companyUrls": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/figma"], "maxResultsPerPlatform": 6}givesfloor(6/2)=3per company. Rows over the cap are dropped, reported asdropped_over_capand never billed by job-feed, though a sub-actor may have billed its own events for them. - Tight budget:
{"countries": ["DE"], "keywords": ["software engineer"], "includeGlobalBoards": false, "maxResultsPerQueryPerPlatform": 5, "maxResultsTotal": 20, "maxTotalMemoryMbytes": 700, "maxParallelPlatforms": 2}. Boards that do not fit the RAM budget queue, largest fitting request first.
FAQ
Why fewer results than my cap? Caps are upper bounds. A board may have fewer matches, and dedup and maxResultsTotal trim further. A SUCCEEDED board with raw_results: 0 always carries a reason in its per_platform[] entry.
Why does my company career page return zero jobs? A URL matching a known ATS went to career-site-jobs (check its reason). Otherwise it went to website-job-extractor, and a JS-only page may need the browser variant, see Company career pages.
Does a partial failure fail the run? No. It ends SUCCEEDED. Only a total failure, or a selection of URL-only platforms with no URL, ends FAILED, see Partial failure and statusMessage.
How do I avoid duplicates across runs? Use onlyNewJobs: true (Only new jobs), or upsert on the stable job_id in your own storage.
Where are run_summary and the other reports? In the OUTPUT key-value-store record, unless includeReportRowsInDataset is on, see Terminal items in the OUTPUT record.
Does it work on the Free plan? Yes. Boards that do not fit the RAM budget queue instead of failing, see RAM and parallelism.
How long does a run take? Under a minute for a few lean boards, several minutes for a large preset. On timeout job-feed keeps collected rows, see Run timeout.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
FAILED, exit code 2, nothing dispatched | Only URL-only platforms selected with no companyUrls, careerSites or searchUrls. Add a URL or select keyword boards too. |
FAILED, exit code 1 | Every dispatched board failed. Read statusMessage for each reason. |
career-site-jobs is SKIPPED_NO_INPUT but the run SUCCEEDED | Mixed selection without a URL for it. Expected. |
| Fewer rows than expected from a preset | includeGlobalBoards off, or a board returned 0 that day. Check per_platform[].reason. |
| Extractor returned 0 on a custom page | JS-rendered (js_rendering_suspected). Add website-job-extractor-browser to platforms. |
Two jobs share a job_id prefix | Same-board collision, disambiguated by the -XXXXXXXX suffix. Both are real. |
location seems ignored on a board | Closed location list or no field. Look for location does not match in the log. |
titleFilter or postedWithinDays seems not to apply | Filtered client-side on most boards. Check dropped_title_filter and dropped_stale. |
| A cap seems ignored | Misspelled field. Look for WARN input contains unrecognized keys. |
user_charged_usd is 0 or low on a board that returned jobs | Apify charged-event lag. Check charges_final, re-fetch later; estimated_cost_usd is the fallback. |
| A board you expected is not in the preset | It is Tier 2 for that country. Add it via platforms[]. |
Regional boards
includeRegionalBoards (default off) adds the small tier-2 regional boards to a countries run. nearby adds the healthy boards whose canton contains your location (Basel gives Jobbasel, Zurich gives Jobzueri, Lugano gives Carriera and Lavoro CDT). all adds every healthy tier-2 board of the selected countries, including the profession boards. If the location cannot be found, or lies outside the selected countries, nothing is added and a warning is returned. The boards added are listed in run_summary.platform_selection.regional_boards_added. Boards you name in platforms are never run twice.
Registry
Place data for enforceRadius comes from GeoNames (CC BY 4.0): places of DE and AT with 500+ inhabitants and all populated places of CH and LI, about 940 KB in registry/places.json, plus canton and Land bounding boxes in registry/regions.json (see registry/NOTICE-geonames.txt).
registry/platforms.json is the single source of truth. Status values: healthy (in country defaults), degraded and unvalidated (only when named explicitly), parked (pulled, never runs), excluded (deliberately never selectable). A platform can also carry release: "coming_soon": built and routed, but its Apify actor is not public yet, so it is excluded from the platforms enum and country defaults.
To add or remove a board, run from actors/jobs/job-feed/ (then apify push): scripts/refresh_registry_from_apify.py (refreshes subactor_registry.json), scripts/sync_platform_enum.py (regenerates the platforms enum), scripts/gen_coming_soon.py and scripts/gen_board_table.py (regenerate Coming soon and Supported boards).
Related actors
When a job row carries company.url or company.domain (emitted by roughly 15 platforms in SERP mode and most platforms in detail mode), you can feed those URLs directly into contact-extraction actors to enrich with decision-maker emails and phone numbers:
- website-email-scraper - extracts emails and phone numbers from any website.
- website-contact-extractor - LLM-driven contact extraction with role classification, LinkedIn profiles, and pitch-relevance ranking.
- website-job-extractor - the extractor job-feed uses for any career page, also usable standalone.
- indeed-http-scraper - the Indeed board on its own.
- glassdoor-scraper - the Glassdoor board on its own.
Run job-feed from code
job-feed is santamaria-automations/job-feed (actor ID zoDyIgISr7yFPyLJd). Runs can take minutes with several boards, so prefer start() plus a webhook over blocking.
curl -X POST "https://api.apify.com/v2/acts/santamaria-automations~job-feed/runs?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"countries":["CH"],"keywords":["Laborant"],"location":"Basel","onlyNewJobs":true}'
The response carries id and defaultDatasetId immediately. Poll GET /v2/actor-runs/{runId} until status is SUCCEEDED, then read GET /v2/datasets/{defaultDatasetId}/items. The same actor is also addressable as POST /v2/acts/zoDyIgISr7yFPyLJd/runs.
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('santamaria-automations/job-feed').call({countries: ['CH'], keywords: ['Laborant'], location: 'Basel', onlyNewJobs: true,});const { items } = await client.dataset(run.defaultDatasetId).listItems();const output = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTPUT');
from apify_client import ApifyClientclient = ApifyClient(token=os.environ["APIFY_TOKEN"])run = client.actor("santamaria-automations/job-feed").call(run_input={"countries": ["CH"], "keywords": ["Laborant"], "location": "Basel", "onlyNewJobs": True})items = client.dataset(run["defaultDatasetId"]).list_items().itemsoutput = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")
Get notified when a run finishes
In the Console, open https://console.apify.com/actors/zoDyIgISr7yFPyLJd/integrations, add an HTTP webhook on "Run succeeded" (and failed, timed out, aborted if you want them), and point it at YOUR_WEBHOOK_URL. The default payload template works; resource.defaultDatasetId and resource.defaultKeyValueStoreId are what you need to fetch the rows and the OUTPUT record. Apify event types are ACTOR.RUN.CREATED, ACTOR.RUN.SUCCEEDED, ACTOR.RUN.FAILED, ACTOR.RUN.ABORTED, ACTOR.RUN.TIMED_OUT and ACTOR.RUN.RESURRECTED. Acknowledge with a 200 immediately, then process.
To attach a webhook to a single run, pass the webhooks query parameter on the run-creation URL: a base64-encoded JSON array of {eventTypes, requestUrl, payloadTemplate} objects.
Run timeout
The default run timeout is 3600 seconds, set under Run options in the Console or with ?timeout=7200 on the run-creation call. A large countries/keywords combination with includeJobDetails: true can legitimately need more. If the timeout is reached anyway, running boards are stopped gracefully with their rows kept (see Partial failure and statusMessage), boards not yet started are skipped, and the run still ends SUCCEEDED with what it collected.