Workable Jobs by Company: ATS Career Page Postings API
Pricing
from $1.05 / 1,000 job row returneds
Workable Jobs by Company: ATS Career Page Postings API
Published jobs of any company hosted on Workable, read through the account's public jobs endpoint: title, department, function, city, state, country, remote flag, employment type, publish date, apply link and optional description. Two extra modes count open jobs per department and per location.
Pricing
from $1.05 / 1,000 job row returneds
Rating
0.0
(0)
Developer
Samat Makatov
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Share
Published jobs of any company hosted on Workable, read through the account's public jobs endpoint: title, department, function, city, state, country, remote flag, employment type, publish date, apply link and optional description. Two extra modes count open jobs per department and per location.
Give it the slugs of the career pages you care about (devsinc-17, prox-works, wantable-careers) and every published opening of those companies comes back as a clean row — company, job title, department, city, region, country, ISO country code, remote flag, employment type, seniority, education, job function, industry, publish date, days online, apply URL, and the full job description on request. The rows come from the employer's own career page, so the company behind a posting is never a guess. Built for recruiters and sourcing agencies watching a list of accounts, for go-to-market teams who treat a new opening as a buying signal, and for job-market researchers. No API key, no login, no browser, no proxy: one keyless request per career page.
Use cases
- Sourcing watchlist: keep the openings of 20–200 target employers in one dataset, refreshed on a schedule;
onlyNewwrites only the postings that appeared since the last run. - Hiring signal for B2B sales:
postedWithinDays: 30over your account list shows who started hiring — a team that grows is a team with a budget. - One job family across many employers:
titleKeywords: ["engineer"]plusexcludeTitleKeywords: ["intern"]turns a list of career pages into a role-specific feed. - Remote-only job feed:
workplace: "remote"keeps the postings employers marked as remote, ready for a newsletter or a job board. - AI screening and keyword audits:
includeDescription: trueadds the description as HTML and as plain text, so an LLM can score a posting against a profile without a second fetch. - Org and market research:
mode: "departments"shows which teams a company is growing,mode: "locations"which countries it hires in — one row per department or country with its open-job count.
Input
| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
accounts | string[] | required | Career page slugs, e.g. ["devsinc-17", "prox-works"]. A whole career page URL works too (https://apply.workable.com/devsinc-17/); a link to a single job (/j/<code>) is rejected with a message. See Finding the slug. |
mode | string | jobs | jobs, departments, locations — see Modes |
titleKeywords | string[] | empty | Keep a job when its title contains any of these (case-insensitive) |
excludeTitleKeywords | string[] | empty | Drop a job when its title contains any of these; applied after titleKeywords |
departments | string[] | empty | Keep jobs whose department contains any of these. Free text per company — mode: "departments" prints the exact list |
locationContains | string[] | empty | Text match over city, region and country of every location of a job |
countryCodes | string[] | empty | Exact two-letter ISO 3166-1 codes, e.g. ["US", "GB", "PK"]; mode: "locations" prints the codes a page uses |
workplace | string | any | any, remote, onsite — from the employer's own remote flag |
employmentTypes | string[] | empty (all) | Employment types; setting it drops postings where the employer left the field blank |
experienceLevels | string[] | empty (all) | Experience levels; same rule about blank values |
postedWithinDays | integer | empty (no limit) | 1–3650. Published in the last N days; the source's publish date has day resolution |
onlyNew | boolean | false | Write only postings that were not delivered by an earlier run of this actor (jobs mode only) |
includeDescription | boolean | false | Add descriptionHtml and descriptionText; same request, no extra source call |
sortBy | string | publishedDesc | publishedDesc, publishedAsc, titleAsc, departmentAsc, pageOrder — see Sort orders |
maxItems | integer | 50 | 1–5000 rows in total, across all career pages |
maxItemsPerAccount | integer | empty (no cap) | 1–2000 rows per career page, applied before maxItems |
fields | string[] | all | Keep only these output fields, in this order |
Reference
Finding the slug
A hosted career page looks like https://apply.workable.com/<slug>/ — the <slug> part is what this actor takes: apply.workable.com/devsinc-17/ → devsinc-17. Companies usually link that page from their own site as "Careers" or "Open positions"; the slug is frequently not the company name (wantable-careers for Wantable, prox-works for Proximity Works). You may paste the whole URL and the slug is read from it, in any letter case. A link to one posting (apply.workable.com/j/F9263E6A0E) contains no slug and is refused with a message instead of being guessed at. A slug the platform does not serve comes back as one row with found: false and an explanation, and the run still succeeds.
Modes
mode | One row per | Fields you get |
|---|---|---|
jobs | published job | the full job row below; all filters apply |
departments | department of the career page | name, openJobs, filterUrl, facetType: "department" |
locations | location of the career page | name, locationCode (ISO 3166-1 alpha-2), openJobs, facetType: "location" |
The dictionary modes ignore the job filters — they describe a whole career page. They are the cheapest way to learn the values departments and countryCodes expect, and a product in themselves: which teams is this employer growing, in which countries does it hire.
Employment types
Full-time, Part-time, Contract, Temporary, Internship, Other. This is a closed list: the input schema rejects anything else before the run starts, and the actor reads the values you pass in any letter case (full-time, FULL-TIME). The field is optional for the employer: on small career pages 10–50 % of postings leave it blank, and those are dropped as soon as you set the filter.
Experience levels
Internship, Entry level, Associate, Mid-Senior level, Director, Executive, Not Applicable. A closed list and employer-optional, exactly like the employment types above.
Sort orders
sortBy | Order |
|---|---|
publishedDesc | newest publish date first (default) |
publishedAsc | oldest publish date first |
titleAsc | job title A–Z |
departmentAsc | department A–Z, then title |
pageOrder | exactly as the career page returns the postings |
Rows are ordered across all career pages of a run, after maxItemsPerAccount was applied, so maxItems keeps the newest postings of the whole watchlist. In the dictionary modes rows are ordered by open-job count, largest first.
Filter semantics
Filters are combined with AND, values inside one filter with OR: titleKeywords: ["engineer", "designer"] with countryCodes: ["US"] means "engineer or designer, in the United States". Filtering happens on the complete response of each career page, so nothing is lost to paging. countryCodes compares the code of the job's own location record; locationContains is a text match over city, region and country, including the extra locations of a multi-location posting. A job without a department cannot satisfy a departments filter and is dropped. Because department names are free text each company types itself, a misspelt one is matched against the names that career page really uses (Enginering → Engineering, written to the log); a value that resembles nothing is kept as typed and the log prints the page's own list. Either way a filter returns fewer rows — it never widens the search.
Examples
Every opening of one employer, newest first
{ "accounts": ["devsinc-17"], "mode": "jobs", "maxItems": 25 }
Remote-only feed from several career pages
{ "accounts": ["remote-recruitment", "remotebase"], "mode": "jobs", "workplace": "remote", "maxItems": 25 }
Engineering roles across a sourcing watchlist
{ "accounts": ["prox-works", "devsinc-17", "remotebase"], "mode": "jobs", "titleKeywords": ["engineer"], "excludeTitleKeywords": ["intern"], "maxItems": 20 }
New postings as a hiring signal (last 30 days)
{ "accounts": ["remote-recruitment", "devsinc-17", "prox-works"], "mode": "jobs", "postedWithinDays": 30, "sortBy": "publishedDesc", "maxItems": 25 }
Full-time United States jobs only
{ "accounts": ["aetos-systems-inc", "wantable-careers", "scalable"], "mode": "jobs", "countryCodes": ["US"], "employmentTypes": ["Full-time"], "maxItems": 25 }
Job descriptions for an LLM screen
{ "accounts": ["wantable-careers"], "mode": "jobs", "includeDescription": true, "maxItems": 5 }
Which teams is this company growing
{ "accounts": ["devsinc-17", "prox-works"], "mode": "departments", "maxItems": 30 }
Where does it hire
{ "accounts": ["remote-recruitment", "prox-works"], "mode": "locations", "maxItems": 30 }
Output
One real row of a cloud run (input: {"accounts": ["devsinc-17"], "mode": "jobs", "maxItems": 25}):
{"account": "devsinc-17","companyName": "Devsinc","title": "Functional Consultant","shortcode": "3F21775CBA","requisitionCode": null,"department": "Cluster Head","function": "Consulting","industry": "Computer Hardware","experienceLevel": "Mid-Senior level","educationLevel": "Bachelor's Degree","employmentType": "Full-time","isRemote": false,"city": "Lahore","region": "Punjab","country": "Pakistan","countryCode": "PK","locationText": "Lahore, Punjab, Pakistan","extraLocations": [],"locationCount": 1,"publishedOn": "2026-09-25","createdOn": "2024-11-28","daysSincePublished": 1,"url": "https://apply.workable.com/j/3F21775CBA","applyUrl": "https://apply.workable.com/j/3F21775CBA/apply","careerPageUrl": "https://apply.workable.com/devsinc-17/","found": true,"error": null,"fetchedAt": "2026-09-26T20:47:46.177Z"}
| Field | Type | Meaning |
|---|---|---|
account | string | Career page slug that produced the row (always filled) |
companyName | string | Company name as the career page publishes it (always filled) |
title | string | Job title (always filled) |
shortcode | string | The platform's code for the posting, also the last part of url (always filled) |
requisitionCode | string | null | The employer's internal code — optional, often empty |
department | string | null | Free-text team name; empty on career pages that use none |
function | string | null | Job function, e.g. Art/Creative, Consulting — employer-optional |
industry | string | null | Industry of the employer as chosen for the posting — employer-optional |
experienceLevel | string | null | Seniority from the dictionary — employer-optional |
educationLevel | string | null | Required education, e.g. Bachelor's Degree — employer-optional |
employmentType | string | null | Employment type — employer-optional |
isRemote | boolean | The employer's remote flag (always present; one flag, no hybrid value) |
city | string | null | City of the first location; empty for country-wide postings |
region | string | null | State, province or region of the first location |
country | string | null | Country name of the first location |
countryCode | string | null | ISO 3166-1 alpha-2 code of the first location — what countryCodes filters on |
locationText | string | null | City, Region, Country of the first location, skipping what is empty |
extraLocations | string[] | The same text for every further location of the posting (empty for single-location jobs) |
locationCount | number | How many locations the posting lists |
publishedOn | string | null | Publish date, YYYY-MM-DD (the source gives no clock time) |
createdOn | string | null | Date the posting was created in the system; can be much older than publishedOn |
daysSincePublished | number | null | Whole UTC days since publishedOn — what postedWithinDays filters on |
url | string | Public job page (apply.workable.com/j/<shortcode>) |
applyUrl | string | null | The posting's application page |
careerPageUrl | string | The company's career page |
descriptionHtml | string | null | Job description as the employer published it — only with includeDescription |
descriptionText | string | null | The same text with tags removed and entities decoded — only with includeDescription |
found | boolean | false on the marker row of a career page the platform does not serve |
error | string | null | Why a row is a marker row; null on job rows |
fetchedAt | string | When the career page was read, ISO 8601 UTC |
Rows of mode: "departments" and mode: "locations" carry account, companyName, facetType, name, locationCode (locations only), openJobs, filterUrl (the source's own filtered career-page link), url, found, error, fetchedAt:
{"account": "remote-recruitment","companyName": "Remote Recruitment","facetType": "location","name": "South Africa","locationCode": "ZA","openJobs": 84,"filterUrl": "https://apply.workable.com/api/v1/widget/accounts/694803?location=ZA","url": "https://apply.workable.com/remote-recruitment/","found": true,"error": null,"fetchedAt": "2026-09-26T20:47:53.626Z"}
Dataset views: Jobs (company, title, department, city, country, remote, employment type, publish date, days online, job page), Job details (function, industry, seniority, education, location text, internal code, apply link), Departments & locations (the dictionary modes). descriptionHtml and descriptionText are in no view on purpose — they would break a table; read them through the API or with fields.
Every run also writes a SUMMARY record to the default key-value store: per career page how many postings were on the page, how many matched, how many were written, which filter values matched nothing, and the run's status sentence.
Use it from code / agents
curl -X POST "https://api.apify.com/v2/acts/yadroo~workable-jobs/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"accounts":["devsinc-17","prox-works"],"titleKeywords":["engineer"],"maxItems":20,"fields":["companyName","title","city","countryCode","publishedOn","url"]}'
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('yadroo/workable-jobs').call({ accounts: ['wantable-careers'], includeDescription: true, maxItems: 5 });const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("yadroo/workable-jobs").call(run_input={"accounts": ["devsinc-17"], "postedWithinDays": 7, "maxItems": 25})items = client.dataset(run["defaultDatasetId"]).list_items().items
MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/workable-jobs tool with the same JSON input.
Agent and pipeline tips: use fields to keep rows narrow and prompts short; schedule the actor daily with postedWithinDays: 7 plus onlyNew: true so each run writes only postings you have not seen (the memory lives in the actor's own key-value store workable-jobs-state, keyed by career page); run mode: "departments" once per employer to learn the department names before you filter on them.
Pricing
Pay per event: $0.001 per run start + $0.0015 per dataset row. Every run is charged the start event, also a run that ends with a single found: false row. Marker rows count as rows and take their slot inside maxItems, so a run never charges for more rows than you asked for. Apify plan discounts apply to both events: −10 % on Bronze, −20 % on Silver, −30 % on Gold and above.
| Run | Rows | Cost at list price |
|---|---|---|
| One career page, 25 newest jobs | 25 | $0.001 + 25 × $0.0015 = $0.0385 |
| The default input: three career pages, 50 rows | 50 | $0.001 + 50 × $0.0015 = $0.0760 |
| Daily watchlist of 30 employers, 400 new postings | 400 | $0.001 + 400 × $0.0015 = $0.6010 |
| Departments of two career pages | 18 | $0.001 + 18 × $0.0015 = $0.0280 |
includeDescription costs nothing extra on the source side — the descriptions arrive in the same request — it only makes the rows about twenty times larger. Compute is negligible: 256 MB and a few seconds per career page, no browser.
Limits & FAQ
- Coverage is per career page. The actor reads the accounts you name; there is no keyword search across all employers and no company discovery, because the platform's cross-company job board disallows crawling its search paths in
robots.txt. Bring your own list of slugs. - Only published jobs. Drafts, internal and archived postings are not served publicly, and neither are they here.
- No salary. This payload carries no pay range or pay-transparency field.
educationLevelandexperienceLevelare the closest things to a requirement summary. - Publish dates have day resolution.
publishedOnis a date without a clock time, so the finest monitoring window ispostedWithinDays: 1; for a tighter loop schedule the actor withonlyNew: true.createdOnis when the posting was created in the system and can be years older than the publish date. - Employer-optional fields are often empty: department, function, industry, employment type, seniority, education, internal code. Filter on them to narrow a large board, and expect the blank ones to disappear from the result.
- One remote flag, no hybrid. A hybrid role appears as remote or on-site depending on how the employer filled the form.
- Dictionary counts are the source's own numbers and can differ from the number of job rows a career page returns (duplicate postings and locations an employer hides are counted differently). Treat
openJobsas the count the career page shows, not as a row count. - Large boards come in one response. No pagination parameter is documented and boards of up to 249 postings came back complete;
maxItemscuts the rows, never the request. A career page with thousands of postings is untested — setmaxItemsPerAccountwhen you watch a very large employer. - Unknown slug vs. empty career page. A slug the platform does not serve always produces one
found: falserow with an explanation, and the run still succeeds; a career page that exists but publishes nothing right now is reported in the log, the status message andSUMMARY, and gets a marker row only when the run would otherwise write nothing. A run fails only when the source answered none of its requests. - Rate limits and politeness. One request per career page per mode, sequential, with retry and backoff on 429 and 5xx. The endpoints are the ones the vendor documents for building a careers page,
robots.txtallows them, and the actor never touches candidate or application endpoints — no personal data is collected. The site's content signals allow search and AI input but not model training; we read the postings, we do not train on them. - If the platform ever puts these endpoints behind a wall, this actor will be marked paused in the store rather than trying to work around it.
Made by Yadroo. Sibling actors: greenhouse-jobs (the same job for Greenhouse boards), hh-kz-vacancies (vacancies in Kazakhstan and the CIS), github-repo-intel, domain-intel, sec-company-financials (company research around a hiring signal).