# Workable Jobs Scraper - apply.workable.com Careers, No API Key

Scrape published job postings from any apply.workable.com careers page: title, department, location, remote flag, dates, links, optional full description. New-postings-only mode. No API key.

Canonical product page: https://mayd-it.com/data-tools/actors/workable-jobs-scraper/
Public Actor: https://apify.com/maydit/workable-jobs-scraper

## Example input

```json
{
  "companies": [],
  "preset": "verified-sample",
  "includeDetails": false,
  "remoteOnly": false,
  "maxResultsPerCompany": 10,
  "maxResults": 10,
  "onlyNewSinceLastRun": false,
  "maxRunSeconds": 240
}
```

## Full guide

![Workable jobs, clearly structured — illustrative workflow](https://api.apify.com/v2/key-value-stores/YGxD9JRpduom8Q5Zd/records/workable-jobs-scraper-246477860a55.png?signature=14HwWoNebOVhnNWiKQ4r4)

## Workable jobs, clearly structured

For recruiters and job-board operators, collect public Workable careers postings in a consistent table without a Workable API key.

**Coverage:** Employer careers pages on Workable, rather than Workable's cross-company job-search site. One result per distinct job shortcode.

[Open this Actor in Console](https://console.apify.com/actors/b5ilHf9KxjyCefPRD/input) · [First run](#try-this-actor) · [Results](#result-preview) · [Cost](#usage-and-pricing) · [Full guide](#full-guide)

## Try this Actor

1. Use the Blueground careers-page example already shown below. Keep description enrichment off and lower the total posting cap before expanding to more companies.
2. Open the dataset for job rows, then inspect `SUMMARY.boardHealth` to distinguish empty boards, rate limits and companies not reached.

**Read the results correctly:** Remote is the employer's own flag; hybrid, on-site and structured salary are not inferred. New-postings mode records only successfully saved IDs.

## Result preview

The dataset view highlights these columns:

- **Company** · `company`
- **Job title** · `title`
- **Department** · `department`
- **Published employment type** · `employmentType`
- **First advertised location** · `location`
- **All advertised locations** · `locations`

Use the original source links and the field definitions below when interpreting the results. CSV works well for flat columns; JSON preserves nested values.

## Usage and pricing

Base result rate on the FREE tier: **$1 per 1,000 results**. The [Pricing tab](https://apify.com/maydit/workable-jobs-scraper/pricing) shows your plan's result rate and any Actor Start charge. Set a small result limit for the first run; the requested limit is not a guarantee of that many results.

---

## Full guide

Scrape published job postings from any **Workable careers page** (`apply.workable.com/<slug>`) and get them back as flat CSV, JSON or Excel rows. Give it an account slug (`blueground`) or the careers-page URL (`https://apply.workable.com/blueground/`) and this Workable scraper returns every posting the employer has published there: job title, department, employment type, city / region / country, the employer's remote flag, published date, posting and apply links, and - when you ask for it - the **full job description converted from Workable's HTML into clean plain text**.

Built for recruiters, sourcers, job-board operators and anyone tracking hiring at a list of companies. It reads the public JSON endpoint behind Workable's embeddable jobs widget, so there is **no API key, no login and no Workable account** involved. Pass one company or a few dozen.

This Actor covers **Workable careers pages only**. It does not read `jobs.workable.com` (Workable's cross-company job search, which its robots.txt disallows), and a company that does not host its careers page on Workable cannot be scraped here.

### What you get

One row per distinct job shortcode. Workable sometimes repeats a posting once per location; these variants are merged before result limits and billing. `locations` retains every advertised location, while `city`, `region`, `country` and `location` describe the first source variant. Fill rates below were measured on 678 source rows across three real runs on 2026-09-25; a blank means the employer left that field empty in Workable.

| Field | What it is | Filled |
| --- | --- | --- |
| `company` | Company name as Workable publishes it (for example `Skroutz S.A`). Falls back to the slug if the account has no name. | 100% |
| `slug` | The Workable account the row came from, so multi-company runs stay separable. | 100% |
| `jobId` | Workable's shortcode for the posting (for example `186545F8C1`) - the id in the posting URL. Stable; use it to dedupe across runs. | 100% |
| `requisitionCode` | The employer's own requisition code, if they filled one in. | 54% |
| `title` | Job title. | 100% |
| `department` | Department the employer filed the role under. | 98% |
| `employmentType` | `Full-time`, `Part-time`, `Contract` and so on, as set by the employer. | 76% |
| `function` | Job function the employer picked from Workable's list (for example `Engineering`). | 50% |
| `industry` | Industry the employer picked (for example `Computer Software`). | 96% |
| `experienceLevel` | `Entry level`, `Associate`, `Mid-Senior level`... as set by the employer. Workable's literal `Unspecified` is returned as blank. | 57% |
| `educationLevel` | `Bachelor's Degree`, `Master's Degree`, `Vocational`... as set by the employer. `Unspecified` is returned as blank. | 44% |
| `city`, `region`, `country` | The posting's location as three separate fields. `country` was filled on every row measured; `city` and `region` are blank on postings the employer published as country-only (often remote roles). | 83% / 83% / 100% |
| `countryCode` | ISO 3166-1 alpha-2 code for the country (`GR`, `CY`, `GB`). | 100% |
| `location` | The three location parts joined, for example `Athens, Attica, Greece`. | 100% |
| `locations`, `locationCount` | All distinct advertised locations and their count, merged across repeated source rows sharing one shortcode. | Source-dependent |
| `remote` | `true`/`false`: the employer's own remote ("telecommuting") flag in Workable. | 100% |
| `workplaceType` | `Remote` when `remote` is true, otherwise blank. Workable's widget exposes no hybrid or on-site flag, so this Actor does not invent one - see the limits section. | 42% (by design) |
| `publishedAt` | Date the posting was published, as a calendar date (`2026-09-14`). Workable publishes a date only, not a time, so none is invented. Rows are sorted newest-published first across all companies. | 100% |
| `createdAt` | Date the posting was created in Workable, same format. | 100% |
| `url` | Public link to the posting (`https://apply.workable.com/j/<jobId>`). | 100% |
| `applyUrl` | Link straight to the application form. | 100% |
| `descriptionText` | The whole job description as clean plain text: tags removed, entities decoded, list items kept as `- ` bullets, capped at 20,000 characters. Only when `includeDetails` is on; blank otherwise. | 100% with `includeDetails` |
| `scrapedAt` | ISO 8601 UTC timestamp of the run. | 100% |

A `SUMMARY` record is written to the run's key-value store with a **board-health report** for every company requested (`ok` with jobs, `empty`, `not-found`, `rate-limited`, `error`, `not-reached`, each with the company name and how many postings were published and saved), which companies failed and why, the total postings published, and department and country breakdowns (top 25 of each with counts).

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `companies` | string list | empty | Account slugs or careers-page URLs, one per company. Accepts `blueground`, `https://apply.workable.com/blueground/`, `https://apply.workable.com/blueground/j/186545F8C1/` and `https://blueground.workable.com/`. A bare job link (`https://apply.workable.com/j/<code>`) has no account in it and is skipped with a warning. |
| `preset` | select | `verified-sample` | Used only when `companies` is empty. `verified-sample` is 18 Workable accounts that each published at least 3 jobs when checked on 2026-09-25 (Blueground, Skroutz, Epignosis, Novibet, Viva.com, Spotawheel, Welcome Pickups, Printec, Orfium, European Dynamics, Grecotel, payabl., Costa Navarino, Uni Systems, Eightcap, EXUS, Hellas Direct, Stoiximan). `none` means only the companies you list; if that list is also empty the sample is used anyway so the run returns something. |
| `includeDetails` | boolean | `false` | Fetch with `details=true` and keep `descriptionText`. Every other field is unaffected. |
| `titleContains` | string | empty | Keep only postings whose title contains this text (case-insensitive, partial). |
| `remoteOnly` | boolean | `false` | Keep only postings the employer flagged as remote. |
| `maxResultsPerCompany` | integer | `100` | Row cap per company, newest-published first. |
| `maxResults` | integer | `300` | Row cap for the whole run, applied after sorting newest-published first across companies. |
| `onlyNewSinceLastRun` | boolean | `false` | Save only postings this Actor has not seen before. See the FAQ for exactly how the first run behaves. |
| `maxRunSeconds` | integer | `240` | Wall-clock budget. When it is reached the run stops cleanly, keeps everything already collected, and names the companies it did not reach. |

### Example output

A real row from a run with `includeDetails` on (description truncated here for readability):

```json
{
  "company": "EUROPEAN DYNAMICS",
  "slug": "european-dynamics",
  "jobId": "47DDC6EF15",
  "requisitionCode": "DSE/09/26",
  "title": "DevSecOps Engineer",
  "department": "PRB",
  "employmentType": null,
  "function": null,
  "industry": "Computer Software",
  "experienceLevel": null,
  "educationLevel": null,
  "city": "Athens",
  "region": "Attica",
  "country": "Greece",
  "countryCode": "GR",
  "location": "Athens, Attica, Greece",
  "remote": true,
  "workplaceType": "Remote",
  "publishedAt": "2026-09-14",
  "createdAt": "2026-09-14",
  "url": "https://apply.workable.com/j/47DDC6EF15",
  "applyUrl": "https://apply.workable.com/j/47DDC6EF15/apply",
  "descriptionText": "We are looking for a DevSecOps Engineer to help us build, automate, secure, and operate reliable software delivery platforms ...",
  "scrapedAt": "2026-09-25T17:34:59.865Z"
}
```

### FAQ

**Where do I find a company's slug?**
Open the company's careers page. If it is hosted on Workable the address looks like `https://apply.workable.com/<slug>/` - the slug is that path segment (`blueground`, `european-dynamics`, `uni-systems`). You can paste the whole URL, or a link to one of its job postings, into `companies` and the Actor extracts the slug. A slug with no careers page returns HTTP 404 and is reported for that company only; the rest of the run continues. The slug `workable` is not an account (Workable's own openings are at `careers`).

**The company exists but I got no rows.**
That is a real answer, not a failure: plenty of Workable accounts resolve but currently publish zero jobs (in the accounts checked while building this Actor, more than half were in that state). The run finishes successfully, logs a warning, and the company shows up in `SUMMARY.boardHealth` as `empty`. Check the page in a browser at `https://apply.workable.com/<slug>/` if you expected postings.

**What exactly does "only postings new since the last run" do?**
With the option on, the Actor records successfully saved job IDs in the named key-value store `workable-jobs-scraper-state`, keyed by board (and platform where applicable). The first run has no baseline and returns matching postings up to your result and charge limits. Later runs return IDs not already saved. Prior seen IDs are retained; newly discovered jobs excluded by filters, result caps, or the charge limit are not marked seen and remain eligible for a later run. A run with nothing new succeeds with an empty dataset and a warning. The baseline is written only while this option is enabled. Runs watching the same board share its baseline, so use one schedule per board and avoid concurrent runs against that baseline.

**Is the description really plain text?**
Yes. Workable returns descriptions as HTML. This Actor strips the tags, decodes the entities and keeps list items as `- ` bullets. On the 335-row run with `includeDetails` on that produced the example above, 0 rows contained leftover HTML tags or entities; descriptions averaged about 4,700 characters.

**Why is there no hybrid / on-site value, and no salary?**
Because the widget endpoint does not publish them. The only workplace signal it carries is the employer's boolean remote flag, so `remote` is that flag and `workplaceType` is `Remote` or blank - nothing is inferred from the text. There is no structured salary either; pay ranges appear inside `descriptionText` when the employer wrote them into the posting. No made-up columns.

**How fast is it?**
Workable returns an entire careers page in a single request, with no pagination. Measured locally on 2026-09-25, end to end including writing the rows: the default run (18 companies, 584 postings published, 300 saved) took about 8 seconds; four companies with 43 postings took under 3 seconds; five companies with 335 postings and full descriptions took under 5 seconds. Platform runs add the time to write results over Apify Storage. After the first 25 companies in a run the Actor deliberately slows to one request every 2.5 seconds - see the limits section for why.

### Data source, access and limits

- **Source:** the JSON endpoint behind Workable's embeddable jobs widget, `https://apply.workable.com/api/v1/widget/accounts/{slug}?details=false|true`. It is **public but undocumented** - it needs no key, login, account or cookies, but Workable does not describe it in its developer documentation, so its shape could change without notice. Only postings the employer has published on their careers page are returned.
- **Crawling is allowed; training is not.** `https://apply.workable.com/robots.txt` (read 2026-09-25) is `User-agent: *` / `Disallow:` (nothing disallowed) with `Content-Signal: search=yes, ai-input=yes, ai-train=no`. So: use the data for recruiting, job aggregation, market research and monitoring; **do not use it to train AI models**. `jobs.workable.com`, Workable's cross-company search, is disallowed to crawlers and this Actor never touches it.
- **Rate limit.** Workable's edge (Cloudflare) rate-limits this endpoint, and it is keyed to *how many different pages* one client asks for, not to raw request count: on 2026-09-25, 150 requests for the same careers page in five seconds went through untouched, while 50 requests for 50 different (unknown) slugs in two seconds got HTTP 429 (an HTML error page). The first three blocks that day carried `retry-after: 0` and cleared within twenty seconds to five minutes. After a fourth burst inside the same hour the 429 came back with `retry-after: 85836` (about 24 hours); over the following half hour that block was enforced inconsistently - some requests from the same client went through while others were refused with the same expiry - so treat a long `retry-after` as real. A run is one request per company, so a list of a few dozen companies is fine (30 companies in one run did not trip it); past 25 the Actor paces itself to one request every 2.5 seconds. A 429 with no `retry-after` is retried four times over 45 seconds; a short `retry-after` (up to 45 seconds) is waited out once and the request tried once more; a longer `retry-after` is treated as the block it is - the Actor does not retry it. Either way the run stops there, keeps what it has, writes the board-health report, and lists the companies it did not reach in `SUMMARY.boardHealth` as `not-reached` (the refused one as `rate-limited`, with `retryAfterSec`) so you can re-run just those later. Spreading scheduled runs apart helps, and so does not passing long lists of guessed slugs - unknown slugs count too. Apify runs share outbound IPs with other users, so a block caused by someone else's traffic is possible; it shows up as `rate-limited` in the board-health report, never as "no jobs".
- **`remote` is the employer's flag, not an inference.** A role that is remote only in its description text reads `false`.
- **Everything else is passed through as published.** Titles, departments, functions, industries, levels, locations and dates come from the employer's own Workable data, including its gaps - the fill rates above are what employers actually fill in.
- **No candidate or application data is extracted.** Every row describes a *job posting*, not a person: there is no recruiter name, email or phone field, and none is inferred. `descriptionText` is the employer's own published description reproduced as text. Nothing is collected from candidates or applications.
- **Failures are explicit.** Companies that 404 or error are named in the log and in `SUMMARY.failedSources`, and the run still saves everything from the companies that worked. If *every* company that was fetched fails, the run fails with a message naming the likely cause instead of returning an empty dataset that looks like "no jobs". The `SUMMARY` record, with the board-health report, is written before the run fails, so you can still see which companies were refused and which were never reached.
- **Billing:** this Actor is billed per result on Apify - the current rate is on the Pricing tab of this Actor's page. There is nothing else to buy; the data source itself needs no key.
- **Not affiliated with Workable.** Workable is a trademark of Workable Software Limited; this Actor is an independent tool that reads publicly available careers-page data.


#### Saved rows and run charge limits

The Actor writes results one at a time and stops when Apify reports the run charge limit. `SUMMARY.stoppedOnChargeLimit` distinguishes that stop from an empty source. Saved-row counts include only rows accepted by the dataset. For change detection, prior seen IDs are retained and newly discovered IDs enter the baseline only after their row is saved; rows excluded by a result or charge cap remain eligible for a later run. Avoid concurrent monitor runs sharing the same named baseline store.

## Help with your run

Open the **Issues** tab on this Actor with the run URL, the input field that caused trouble and what you expected to receive. Remove API keys and private information before posting.

[Explore more Mayd-It Actors](https://apify.com/maydit)

## Understand your run

Open **Run report** in the run’s Output tab for saved-record counts, reported source issues and limits, and suggested next steps. Use **CSV export** to take the saved results into a spreadsheet. The existing JSON results and detailed machine-readable summary remain available.

The report reflects this run’s recorded coverage. A successful run or a saved result does not establish that every possible source record was collected.
