Workable Jobs Scraper — Company Job Boards
Pricing
from $1.45 / 1,000 posting saveds
Workable Jobs Scraper — Company Job Boards
Every open role on any company's Workable careers page as structured data: title, department, places with country, employment type, remote flag, published date, pay range where the text states one, text on request. One request per board. Incremental mode charges only for changes. No personal data.
Pricing
from $1.45 / 1,000 posting saveds
Rating
0.0
(0)
Developer
Adderley Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 hours ago
Last modified
Categories
Share
What does Workable Jobs Scraper do?
Workable Jobs Scraper reads every open role on any company's public Workable careers page and returns it as structured data you can load straight into a spreadsheet, a database or a model. If a company's jobs are at apply.workable.com/<name>/ — or in Workable's job widget on its own website — this Actor reads them, given the company's Workable account name.
Give it the account name (acme) or any link that names it, and for each job you get the title, the company's name, the department, every place the careers page shows with its country, the employment type, whether the company marks the job as remote, the day it was published, the pay range where the job's text states one, and a link to the job — and, if you ask for it, the full text of the job.
What makes it different:
- One request per board. The whole board, and every job's full text with it, comes back in a single call to Workable's public account API. No HTML parsing, no page-by-page crawling, nothing to break when a careers page is redesigned. A thousand jobs across fifty companies is fifty requests.
- One row per job. Workable's feed lists a job once for every place it is open in. This Actor returns it once, with every place in
location.raw, the way the company's careers page shows it. - Hidden places stay hidden. Workable lets a company hide a job's places from its careers page — every one of them, for a remote job. A hidden place is never returned and never matched by a filter.
- Pay ranges as numbers, where the job states one. Workable's public feed has no salary fields. Where a job's text states a range, it comes back as
min,max,currencyandperiod, with the sentence it came from insalary.raw. Where no range is stated, the fields arenull; nothing is estimated. - Incremental mode. Put the Actor on a schedule and each run returns only jobs that are new, changed, back again or gone. The Actor compares each job with the last run — title, company, places, department, employment type, remote flag, pay, published date, and every word of the text. Unchanged jobs are skipped and not charged.
- One stable schema. Every row has every field, every time. Unknown is
null, never a missing key, so nothing downstream breaks on a sparse job. The schema is versioned (job.v1) and every Adderley Data jobs Actor uses it, so a Workable board, a Recruitee board and a job board sit in the same table. - No personal data. Workable's feed carries the company's own blurb and an application link with each job; this Actor reads neither and has nowhere to put a person. Contact details inside job text are redacted by default.
- Bounded cost. You set a maximum number of results; the run stops there. It also stops at the spending limit you set on the run in Apify.
What Workable data can you extract?
| Field | What it holds |
|---|---|
id | Stable across runs: source:market:sourceJobId. Use it as your primary key. |
title | Job title as listed. |
company.name | The hiring company, where the listing names one. |
advertiser.name | The business that placed the listing — often a recruitment agency. Never a person. |
location.raw | Location text as listed. Several locations are joined with |. |
location.suburb | Suburb, when the listing states one. |
location.city | City or area, when the listing states one. |
location.region | State or region, e.g. VIC. |
location.postcode | Postcode, when the source provides it. |
location.country | ISO 3166-1 alpha-2 country code. |
workArrangement | on_site, hybrid, remote or unknown. |
employmentTypes | Normalised: full_time, part_time, contract, casual, temporary, internship, volunteer. |
salary.raw | The salary text exactly as shown, or null when the listing shows none. |
salary.min | Lower bound as a number, when the text contains one. |
salary.max | Upper bound as a number. Equal to min for a single figure. |
salary.currency | ISO 4217. Taken from the text, otherwise the market default. |
salary.period | hour, day, week, month or year; null when the text does not say. |
salary.includesSuper | true / false when the text says so ("plus super", "inc. super"); otherwise null. |
classifications | The source's category and subcategory pairs. |
teaser | The short summary shown on the results page. |
bulletPoints | Selling points shown on the results page. |
postedAt | When the listing was posted, ISO 8601 UTC. |
updatedAt | The source's own last-modified time, ISO 8601 UTC. Published by ATS and API sources; null where the site does not show one. |
expiresAt | Expiry, ISO 8601 UTC, where the source states one. |
isPromoted | true for paid placements. A listing shown both promoted and organic is returned once. |
url | Link to the listing. |
description | Null unless requested. text, optional sanitised html, and contactsRedacted. |
changeType | Incremental runs: NEW, UPDATED, REAPPEARED, EXPIRED (or UNCHANGED if you ask for those). Otherwise null. |
firstSeenAt | Incremental runs: when this monitor first saw the listing. |
contentHash | SHA-256 over the fields that define a change. Compare it to detect edits yourself. |
scrapedAt | When this row was produced, ISO 8601 UTC. |
source | Source key, e.g. seek. |
market | Market key, e.g. au, nz. |
sourceJobId | The source's own identifier for the listing. |
company.sourceCompanyId | The source's identifier for the company, when exposed. |
company.url | The company's page on the source site, when exposed. |
advertiser.sourceAdvertiserId | The source's identifier for the advertiser. |
schemaVersion | Always job.v1. Breaking changes ship as job.v2 in a new Actor version, never silently. |
How Workable's fields fill the schema:
company.nameis the company name Workable gives with the board.company.sourceCompanyIdis the board's Workable account name andcompany.urlits careers page,https://apply.workable.com/<name>/.urlis the job's own page on that careers page,https://apply.workable.com/<name>/j/<code>, andsourceJobIdis Workable's ten-character job code.location.rawis every place the job's careers page shows, each written city, region, country, joined with|.cityandregionare the first place's, andlocation.countryis Workable's own ISO country code for it — from the country's name where the code is missing, never from a city. A job whose places are all hidden has every location fieldnull.suburbandpostcodeare alwaysnull: Workable publishes neither.classificationsholds the job's department ascategory. Workable has no second level, sosubcategoryisnull.employmentTypescomes from Workable's employment type: Full-time givesfull_time, Part-timepart_time, Contractcontract, Temporarytemporary, Internshipinternship. Other, or no type, gives an empty list.workArrangementisremotewhen the company marks the job as remote. Workable's public feed does not say whether any other job is hybrid or on-site, so for those it isunknown.postedAtis the day the job was published on Workable, at 00:00 UTC: Workable gives a date, not a time.updatedAtandexpiresAtarenull: Workable's feed publishes neither.marketis alwaysglobal: a Workable board is the company's, not a country's.
How much does it cost to scrape Workable job boards?
You pay per job saved to your dataset — $1.75 per 1,000 jobs on Apify's Starter plan — plus $0.005 each time a run starts. There is no monthly rental.
| Apify plan | Price | Per listing |
|---|---|---|
| Free | $1.75 per 1,000 listings | $0.00175 |
| Starter (Bronze) | $1.75 per 1,000 listings | $0.00175 |
| Scale (Silver) | $1.60 per 1,000 listings | $0.00160 |
| Business (Gold) | $1.45 per 1,000 listings | $0.00145 |
Plus $0.005 per run start. Compute and proxy are included in these prices.
| What you run | Cost (USD, Starter plan) |
|---|---|
| 100 listings, one run | $0.18 |
| 1,000 listings, one run | $1.75 |
| 10,000 listings, one run | $17.50 |
| 50,000 listings, one run | $87.50 |
| A daily incremental monitor finding about 150 new or changed listings a day, for a month | $8.03 |
Descriptions cost nothing extra here: they arrive in the same request as the listing. Use incremental mode for anything you run more than once — after the first run you pay only for what changed.
How to scrape a Workable careers page
- Find the company's Workable account name. It is the part after
apply.workable.com/in its careers page address: inhttps://apply.workable.com/acme/it isacme. A job link on that page (apply.workable.com/acme/j/<code>) works too. - Open the Actor in Apify Console and go to the Input tab. Paste one name or link per line into Job boards. Up to 500 boards per run.
- Optionally filter: Title keywords, Locations, Departments, Published within (days).
- Set Maximum results. This is also your cost cap.
- Press Start. When the run finishes, open the Output tab and export as JSON, CSV, Excel, XML or HTML, or read the dataset through the Apify API.
A job that matches more than one board or filter is returned once.
Input
| Field | Type | Default | What it does |
|---|---|---|---|
boards | array | — | One entry per company: its Workable account name (in https://apply.workable.com/acme/ it is "acme"), or any link that names it — the careers page, one of its jobs, its application page, or the legacy acme.workable.com address. A short job link (apply.workable.com/j/…) does not name the account, so give the careers page link instead. A company that shows its Workable jobs on its own domain still has an apply.workable.com page, and that name is needed. Up to 500 boards per run; each is one request. |
keywords | array | — | Keep jobs whose title contains every word of any keyword, in any order — "engineer data" matches "Senior Data Engineer". Leave empty for all titles. |
locations | array | — | Keep jobs with a place on the careers page containing this text ("Athens", "Berlin"), open in this country ("Germany", "Australia"), or marked remote by the company ("Remote"). Every place a job's careers page shows counts; places the company hides are never matched. Leave empty for all locations. |
departments | array | — | Keep jobs whose department contains this text, e.g. "Engineering". Leave empty for all departments. |
postedWithinDays | integer | — | Keep jobs published on Workable in the last N days. Workable dates each job by the day, so a job published on the day N days ago counts. Leave empty for any time. |
maxResults | integer | 100 | The run stops once this many jobs are saved. You are charged per job saved, so this is also your cost cap. |
includeDescription | boolean | false | On: the full job text — description, requirements and benefits, as Workable publishes them — comes back with each row, read from the same request as the listing, so it costs no extra requests. Off: listing fields only. |
descriptionFormat | text, text_and_html | "text" | Plain text, or plain text plus sanitised HTML. |
redactContacts | boolean | true | On by default: email addresses and phone numbers inside description text are replaced with [redacted]. This Actor never outputs recruiter names or contact fields. |
incremental | boolean | false | Remember what earlier runs saw and save only jobs that are new, changed or gone. Unchanged jobs are skipped and not charged. Put the Actor on a schedule with this on. |
stateKey | string | — | Optional name for this monitor, e.g. "competitor-engineering". Runs with the same key share memory. Left empty, a key is derived from the boards and filters themselves. |
emitExpired | boolean | true | Incremental mode only. When a complete run no longer finds a job it saw before, save one row with changeType EXPIRED. |
emitUnchanged | boolean | false | Incremental mode only. Saves (and charges for) every job, labelled UNCHANGED where nothing moved. |
proxyConfiguration | object | {"useApifyProxy":true} | Apify Proxy, automatic group, is the default and is what this Actor is tested with. |
maxConcurrency | integer | 1 | Parallel requests. One by default: Workable limits how often one address may ask, and one board at a time stays under that limit. |
maxRequestsPerMinute | integer | 10 | An upper bound on request rate across the whole run. Ten by default, under the limit Workable applies to one address. If Workable still answers 429 (too many requests), the Actor waits as long as Workable asks, or 30 seconds, and asks again from the same address, at most twice. |
A typical input:
{"boards": ["futureplc"],"maxResults": 100}
The prefilled board, futureplc, is one company's public careers page on Workable, used here only as an example of a public Workable board. This Actor is not affiliated with that company.
Output
One row per job. This is a synthetic example in the exact shape the Actor returns:
{"schemaVersion": "job.v1","id": "workable:global:3F2A9C71B4","source": "workable","market": "global","sourceJobId": "3F2A9C71B4","url": "https://apply.workable.com/example-freight/j/3F2A9C71B4","title": "Data Analyst","company": {"name": "Example Freight Co","sourceCompanyId": "example-freight","url": "https://apply.workable.com/example-freight/"},"advertiser": {"name": "Example Freight Co","sourceAdvertiserId": "example-freight"},"location": {"raw": "Melbourne, Victoria, Australia | Sydney, New South Wales, Australia","suburb": null,"city": "Melbourne","region": "Victoria","postcode": null,"country": "AU"},"workArrangement": "remote","employmentTypes": ["full_time"],"salary": {"raw": "The salary for this role is A$95,000 to A$110,000 a year, plus super.","min": 95000,"max": 110000,"currency": "AUD","period": "year","includesSuper": false},"classifications": [{"category": "Data","subcategory": null}],"teaser": null,"bulletPoints": [],"postedAt": "2026-09-20T00:00:00.000Z","updatedAt": null,"expiresAt": null,"isPromoted": false,"description": null,"changeType": "NEW","firstSeenAt": "2026-09-21T19:30:12.000Z","contentHash": "df5f98b0f8a6676976a01466f8e2aa32c90f9592717f22c03d7c3455f6435de0","scrapedAt": "2026-09-21T19:30:12.000Z"}
The Output tab has two table views: Overview (the fields most people want, flattened) and Changes (for incremental runs).
Incremental mode: monitor new roles across companies
Turn on Incremental mode and run the same input on a schedule — hourly, daily, weekly. The Actor keeps a small record of what it has seen and every row tells you what happened:
changeType | Meaning |
|---|---|
NEW | First time this monitor has seen the job |
UPDATED | Seen before, and the title, company name, places, department, employment type, remote flag, pay range, published date or any word of the job's text has changed |
REAPPEARED | Was reported as expired and is back |
EXPIRED | Seen before and no longer on the board. One row, once |
UNCHANGED | Only if you turn on Also save unchanged jobs |
How it behaves, so there are no surprises:
- The first run returns everything as
NEW. From the second run you pay only for the difference. - A change is found by comparing the job itself. An edit to the text counts: a corrected typo is reported as
UPDATEDand charged like any other change. A change of formatting alone does not count, and neither do Workable's own bookkeeping fields — the job's internal requisition code and its creation date — or the order of the jobs on the board. - A place the company starts or stops showing on its careers page is a change, because it changes what the page says. A place it adds but hides is not.
EXPIREDis only ever reported by a complete run. If a run hits your result cap or your spending limit, or a board cannot be read, nothing is declared expired — a job on a board the run never read is not gone.- Each board is read in one response, so a run compares whole boards, never pages read seconds apart.
- Runs share memory when they share a State key. Leave it empty and the key is derived from the boards and filters themselves, so the same input always continues the same monitor. Name it (
competitor-engineering) if you want to change filters later without starting again. - A job not seen for 45 days is forgotten.
Descriptions and contact details
Full descriptions are off by default. Turn on Include full descriptions and each row carries description.text (and sanitised description.html if you choose that format): the job's description, requirements and benefits, in the one block Workable publishes them in. Because Workable returns the text in the same response as the listing, this costs no extra requests and no extra time.
Job text sometimes contains a recruiter's email address or phone number. With Redact contact details on — the default — those are replaced with [redacted] and description.contactsRedacted is true; so is a personal profile address (linkedin.com/in/…). Neither description.text nor the optional HTML carries the address behind a link: the HTML keeps each link's words and drops its address, and drops images. The Actor never returns recruiter names or contact details as fields, under any setting: it never reads the company's own blurb or the application link in Workable's feed, and the schema has nowhere to put a person. If your use case is contacting individuals, this is the wrong tool.
What people use it for
- Competitor and market hiring signals. Which companies are opening which roles, in which departments and countries, and how often — a daily monitor across a list of boards is one scheduled run.
- Remote-work datasets. Jobs companies mark as remote, with the places they are open in where the company shows them.
- Job aggregators and alert products. A clean feed of new jobs from a curated list of employers, deduplicated and labelled by change.
- Sales and partnership research at company level. Growth signals from hiring, without collecting anything about the individuals involved.
- Research and teaching. A clean, repeatable dataset with a documented schema.
Using the API
Run it from code with the Apify client, using your own API token:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('adderleydata/workable-jobs-scraper').call({"boards":["futureplc"],"maxResults":100});const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items.length, items[0]?.location);
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("adderleydata/workable-jobs-scraper").call(run_input={"boards":["futureplc"],"maxResults":100})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["title"], item["location"]["raw"], item["postedAt"])
Schedules, webhooks and the Make, Zapier, n8n and Google Sheets integrations all work the way they do for any Apify Actor. The Actor runs with limited permissions and is priced per event, so AI agents can call it through Apify's MCP server as well.
Is it legal to scrape Workable careers pages?
The Actor reads Workable's public account API — the feed Workable documents for companies that list their jobs on their own website, with no login and no key — at a modest request rate, and returns facts about job postings. It does not log in, does not solve CAPTCHAs, does not submit applications and does not collect personal information. A place a company hides from its careers page is not returned.
What you do with the data is your responsibility. Each company's job text is its own copyright — analyse it, do not republish it. If your project touches personal information, privacy law applies to you wherever you are. This is general information, not legal advice.
Questions
Where do I find a company's Workable account name? Open the company's careers page on Workable. If the address is apply.workable.com/acme/, the name is acme; paste the whole link or just the name. The older acme.workable.com address works too.
I have a short job link. Workable's short job links (apply.workable.com/j/<code>) do not name the company's account, so the Actor cannot tell which board to read from one and asks for the careers page link instead. The same job's link on the careers page (apply.workable.com/<name>/j/<code>) works.
The company's jobs are on its own domain. Many companies show their Workable jobs in a widget on an address such as careers.example.com. That address does not say which Workable account is behind it, so the Actor asks for the account name instead of guessing, and never sends a request to a domain Workable does not run. The company's careers page on apply.workable.com gives the name.
A board I gave came back as "not found". Workable answers "Not Found" when no account has that name. The run log names the board, and the other boards in the run are unaffected. A missing board is asked for once, not retried, and a run in which every board is missing finishes with an empty dataset and a message naming each one.
Why is a job's location empty? The company hid every place the job is open in from its careers page, which Workable allows for a remote job. The Actor returns only what the careers page shows; workArrangement still says remote.
Why is workArrangement unknown for a job that is not remote? Workable's public feed says whether a job is remote and nothing more. A job that is not remote may be hybrid or on-site; the Actor does not guess.
Why is salary empty for most jobs? Workable's public feed has no salary fields, so a range comes only from the job's text, and many jobs state none.
Does it need a Workable login or API key? No. The account API is public.
Why is the default one request at a time? Workable limits how often one address may ask for boards. By default the Actor asks for one board at a time, at most ten a minute, which stays under that limit: fifty companies take about five minutes. If Workable answers 429 (too many requests), the Actor waits as long as Workable asks, or 30 seconds when it does not say, and asks again from the same address, at most twice. A board still refused after that fails with a message naming it, the other boards are kept, and the run summary counts the refusals under http.tooManyRequests. The Actor never switches to another address to get round the limit.
Why is employmentTypes sometimes empty? It is empty when the job states no employment type or states "Other" — nothing is guessed.
Can I get recruiter emails or phone numbers? No, by design.
How current is the data? It is read from Workable while your run is in progress. postedAt is the day the job was published; scrapedAt records when the row was produced.
The field I need is not there. Open an issue on the Issues tab. Fields are added to the schema without breaking existing ones.
Support
Use the Issues tab on this page. We read it every day. Include the run ID and what you expected to see.
Other Adderley Data Actors
Every Actor in a vertical returns the same fields, so adding a source needs no new code on your side.
- Ashby Jobs Scraper — Company Job Boards — same
job.v1fields - BambooHR Jobs Scraper — Company Job Boards — same
job.v1fields - Breezy HR Jobs Scraper — Company Job Boards — same
job.v1fields - Career Site Jobs Scraper — Greenhouse, Lever, Workday — same
job.v1fields - Dayforce Jobs Scraper — Company Job Boards — same
job.v1fields - Greenhouse Jobs Scraper — Company Job Boards — same
job.v1fields - JazzHR Jobs Scraper — Company Job Boards — same
job.v1fields - JobAdder Jobs Scraper — Agency and Employer Job Boards — same
job.v1fields - Lever Jobs Scraper — Company Job Boards — same
job.v1fields - Personio Jobs Scraper — Company Job Boards — same
job.v1fields - Pinpoint Jobs Scraper — Company Job Boards — same
job.v1fields - Recruitee Jobs Scraper — Company Job Boards — same
job.v1fields - RemoteOK Jobs Scraper — Remote Job Feed — same
job.v1fields - Rippling Jobs Scraper — Company Job Boards — same
job.v1fields - Workday Jobs Scraper — Company Job Boards — same
job.v1fields
About
Made by Adderley Data, Melbourne — https://adderleydata.com. Not affiliated with, endorsed by or sponsored by Workable. Workable is a trade mark of its owner and is used here only to describe what this Actor reads.