Workable Jobs by Company: ATS Career Page Postings API avatar

Workable Jobs by Company: ATS Career Page Postings API

Pricing

from $1.05 / 1,000 job row returneds

Go to Apify Store
Workable Jobs by Company: ATS Career Page Postings API

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

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

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 (two in the dictionary modes).

Need it tailored to your workflow? Missing a field, a filter, another source or a different output format? Open an issue and describe what you need — we read every request and adapt the actor.

Use cases

  • Sourcing watchlist: keep the openings of 20–200 target employers in one dataset, refreshed on a schedule; onlyNew writes only the postings that appeared since the last run.
  • Hiring signal for B2B sales: postedWithinDays: 30 over 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"] plus excludeTitleKeywords: ["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: true adds 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

FieldTypeDefaultAllowed values / notes
accountsstring[]requiredCareer 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>) gets its own found: false row with a message while the other career pages are read. See Finding the slug.
modestringjobsjobs, departments, locations — see Modes
titleKeywordsstring[]emptyKeep a job when its title contains any of these (case-insensitive)
excludeTitleKeywordsstring[]emptyDrop a job when its title contains any of these; applied after titleKeywords
departmentsstring[]emptyKeep jobs whose department contains any of these. Free text per company — mode: "departments" prints the exact list
locationContainsstring[]emptyText match over city, region and country of every location of a job
countryCodesstring[]emptyExact two-letter ISO 3166-1 codes, e.g. ["US", "GB", "PK"], matched against every location of a job; mode: "locations" prints the codes a page uses
workplacestringanyany, remote, onsite — from the employer's own remote flag
employmentTypesstring[]empty (all)Employment types; setting it drops postings where the employer left the field blank
experienceLevelsstring[]empty (all)Experience levels; same rule about blank values
postedWithinDaysintegerempty (no limit)1–3650. Published in the last N days; the source's publish date has day resolution
onlyNewbooleanfalseWrite only postings that were not delivered by an earlier run (jobs mode only); a task keeps its own memory — see Watching for new postings
includeDescriptionbooleanfalseAdd descriptionHtml and descriptionText; same request, no extra source call
sortBystringpublishedDescpublishedDesc, publishedAsc, titleAsc, departmentAsc, pageOrder — see Sort orders
maxItemsinteger501–5000 rows in total, across all career pages
maxItemsPerAccountintegerempty (no cap)1–2000 rows per career page, applied before maxItems
fieldsstring[]allKeep only these output fields, in this order; account and found are always kept. Checked before the run: another letter case or a near miss is corrected, a name that is not a column of the mode is left out (both noted in the log and SUMMARY.warnings), a list with no valid name fails the run

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, so it is never guessed at: like a URL on another host, it comes back as one row with found: false, the text you entered in account and the reason in error, and the other career pages of the run are read as usual. A slug the platform does not serve also comes back as one found: false row with an explanation, and the run still succeeds. Only a run in which no entry names a career page at all fails, with the reason for each entry — or one in which Workable answered none of the career pages (then nothing is written or charged).

Modes

modeOne row perFields you get
jobspublished jobthe full job row below; all filters apply
departmentsdepartment of the career pagename, openJobs, filterUrl, facetType: "department"
locationslocation of the career pagename, 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: Apify refuses any other value — another spelling or letter case included — before the run starts, so write the values exactly as listed. 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

sortByOrder
publishedDescnewest publish date first (default)
publishedAscoldest publish date first
titleAscjob title A–Z
departmentAscdepartment A–Z, then title
pageOrderexactly 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 and locationContains look at every location of a posting (locationContains is a text match over city, region and country); when one of them is set, the location that matched becomes the row's first location, so city and countryCode show where the posting matched and the other places move to extraLocations. 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"
}
FieldTypeMeaning
accountstringCareer page slug that produced the row (always filled)
companyNamestringCompany name as the career page publishes it (always filled)
titlestringJob title (always filled)
shortcodestringThe platform's code for the posting, also the last part of url (always filled)
requisitionCodestring | nullThe employer's internal code — optional, often empty
departmentstring | nullFree-text team name; empty on career pages that use none
functionstring | nullJob function, e.g. Art/Creative, Consulting — employer-optional
industrystring | nullIndustry of the employer as chosen for the posting — employer-optional
experienceLevelstring | nullSeniority from the dictionary — employer-optional
educationLevelstring | nullRequired education, e.g. Bachelor's Degree — employer-optional
employmentTypestring | nullEmployment type — employer-optional
isRemotebooleanThe employer's remote flag (always present; one flag, no hybrid value)
citystring | nullCity of the first location (the matching one when countryCodes or locationContains is set); empty for country-wide postings
regionstring | nullState, province or region of the first location
countrystring | nullCountry name of the first location
countryCodestring | nullISO 3166-1 alpha-2 code of the first location
locationTextstring | nullCity, Region, Country of the first location, skipping what is empty
extraLocationsstring[]The same text for every further location of the posting (empty for single-location jobs)
locationCountnumberHow many locations the posting lists — a posting open in several places is one row, not one row per place
publishedOnstring | nullPublish date, YYYY-MM-DD (the source gives no clock time)
createdOnstring | nullDate the posting was created in the system; can be much older than publishedOn
daysSincePublishednumber | nullWhole UTC days since publishedOn — what postedWithinDays filters on
urlstring | nullPublic job page (apply.workable.com/j/<shortcode>); on a marker row the career page, or null for an entry that names none
applyUrlstring | nullThe posting's application page
careerPageUrlstring | nullThe company's career page; null on the marker row of an entry that names none (a job link)
descriptionHtmlstring | nullJob description as the employer published it — only with includeDescription
descriptionTextstring | nullThe same text with tags removed and entities decoded — only with includeDescription
foundbooleanfalse on the marker row of a career page the platform does not serve, or of an accounts entry that names no career page
errorstring | nullWhy a row is a marker row; null on job rows
fetchedAtstringWhen 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, and found / error for marker rows), Job details (function, industry, seniority, education, location text, internal code, apply link), Departments & locations (the dictionary modes, with found / error too). The dataset schema describes every field with a title, a description and an example, for agents that chain this actor. 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 (linked from the run's output as Run summary): per career page how many postings were on the page, how many matched, how many were written, which filter values matched nothing, the career pages the run did not get to (accountsNotRead) and why it stopped (stoppedBy: your spending limit, the run timeout or an abort), the source's answers by kind (throttle: attempts, retries, 429, 403, 5xx, timeouts), the onlyNew store it used, 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 ApifyClient
client = 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 (see Watching for new postings); run mode: "departments" once per employer to learn the department names before you filter on them.

Watching for new postings

With onlyNew: true a run writes only the postings no earlier run delivered — career page, job code and publish date are remembered in a named key-value store in your own account, one record per career page (seen-<slug>):

  • workable-jobs-state-<task id> when the run comes from a task, so two schedules with different company lists or filters cannot blind each other. A task's first run starts from what the shared store below already knows, so switching to this per-task memory delivers nothing twice.
  • workable-jobs-state for runs started directly (API, Console without a task).

Only rows a run really wrote are remembered: postings cut off by maxItems, by your spending limit or by the run timeout come back on the next run. A re-published posting (new publish date) counts as new. A found: false row of a career page is remembered apart from its jobs — it is written once, later runs name the career page in the status message only — while an entry that is not a career page at all gets its row on every run until you fix it. Runs that start at the same second read the same memory and can return the same posting twice, so schedule a watchlist as one run after another. Delete the record (or the store) to start over.

Pricing

Pay per event: $0.001 per run start + $0.0015 per dataset row. Bronze −10 %, Silver −20 %, Gold and above −30 % on the row price; the start event is the same on every plan; platform usage is included. Every run is charged the start event, also a run that ends with a single found: false row. A posting open in several locations is one row and is charged once. Marker rows count as rows and take their slot inside maxItems, so a run never charges for more rows than you asked for. A run in which Workable answered none of the career pages fails and writes no row, so it costs the start event only. Your Maximum cost per run is respected: the run stops at it and says so ("Stopped at your spending limit: N rows delivered").

RunRowsCost at list price
One career page, 25 newest jobs25$0.001 + 25 × $0.0015 = $0.0385
The default input: three career pages, 50 rows50$0.001 + 50 × $0.0015 = $0.0760
Daily watchlist of 30 employers, 400 new postings400$0.001 + 400 × $0.0015 = $0.6010
Departments of two career pages18$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. educationLevel and experienceLevel are the closest things to a requirement summary.
  • Publish dates have day resolution. publishedOn is a date without a clock time, so the finest monitoring window is postedWithinDays: 1; for a tighter loop schedule the actor with onlyNew: true. createdOn is 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.
  • Multi-location postings are one row. The source lists a posting that is open in several places once per place (one career page sent 280 entries for 94 postings); the actor merges them by the posting's code into one row with every place in locationText + extraLocations and the number in locationCount, so you get — and pay for — each posting once.
  • Dictionary counts are the source's own numbers and can differ from the number of job rows a career page returns (a multi-location posting is one row but counts in every location; locations an employer hides are counted differently). Treat openJobs as 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; maxItems cuts the rows, never the request. A career page with thousands of postings is untested — set maxItemsPerAccount when you watch a very large employer.
  • Unknown slug vs. empty career page. A slug the platform does not serve — and an accounts entry that is not a career page at all, such as a single-job link — always produces one found: false row with an explanation (with onlyNew, a slug's row once — see Watching for new postings), and the run still succeeds; a career page that exists but publishes nothing right now is reported in the log, the status message and SUMMARY, and gets a marker row only when the run would otherwise write nothing. A run fails only when no entry names a career page or the source answered none of its requests.
  • Spending limit, timeout, outages. The run stops at your Maximum cost per run and before its timeout, keeps the rows it has read and ends with a status that says so; career pages it did not get to are listed in SUMMARY.accountsNotRead. When Workable does not answer three career pages in a row (timeouts or 5xx after the retries), the rest of the list is not tried in that run; when it answers none, the run fails without writing or charging a row.
  • Rate limits and politeness. One request per career page in jobs mode and two in the dictionary modes (the list plus the company name), sequential, with retry and backoff on 429 and 5xx; every throttle-like answer is counted in SUMMARY.throttle. The endpoints are the ones the vendor documents for building a careers page, robots.txt allows 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).