# Workable Jobs Scraper (`scrapyx/workable-jobs-scraper`) Actor

Jobs from any company's Workable job board (apply.workable.com/<account>) via the public API: title, departments, location(s), on-site/hybrid/remote, employment type, experience, industry, posted date, apply URL and full description. Search by keyword; filter by department, location, remote.

- **URL**: https://apify.com/scrapyx/workable-jobs-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Workable Jobs Scraper

Every open job on any company's **Workable** job board — Viva.com, Skroutz,
Blueground and the thousands of others hosted at
`apply.workable.com/<account>` — through Workable's public apply API:
title, departments, location(s) with country code, on-site / hybrid /
remote, employment type, experience level, function, industry, posted
date, apply URL and the full description (requirements and benefits
inline) as text and HTML.

HTTP only, no login, no key, no browser. The list is walked 10 jobs per
page by cursor; **one extra request per account** brings every description.

### What it is for

- **Company watchlists** — a list of accounts on a schedule, diff by
  `shortcode`.
- **Keyword search across companies** — `searchTerms` uses Workable's own
  search, one walk per account × term, de-duplicated.
- **Remote / department / city slices** — `remoteOnly` is server-side;
  `departments` and `locations` are local substring filters.

### Input

| field | what it does |
| --- | --- |
| `accounts` | Account slugs (`viva`) or URLs (`apply.workable.com/viva/`, a job URL under the account). A bare `apply.workable.com/j/<shortcode>` link carries no account and is refused. |
| `searchTerms` | Sent to Workable's search; empty = whole board. |
| `departments`, `locations`, `remoteOnly` | Filters (see #3 below for why two are local). |
| `includeDescription` | On by default; one widget request per account. |
| `maxItems`, `maxConcurrency`, `minRequestInterval`, `proxyConfiguration` | Limits. |

Each account × term gets a `BOARD_SUMMARY` with Workable's reported total,
the pages walked, how many rows carry a description, what the filters
dropped, and the board's own department and location vocabularies.

### Four things about this API worth knowing before you trust a run

#### 1. A bad or stale cursor restarts the walk from page one — with HTTP 200

`token=zzzz` answers the first page again, with the same `nextPage` an
untokened call returns. A walker that loops on `nextPage` alone never
ends. The Actor remembers every cursor and every page's shortcodes; a
repeat stops the walk with `stoppedReason: cursor_loop`.

#### 2. The legacy widget duplicates multi-location jobs and mislabels a field

The one-call description source (`www.workable.com/api/accounts/<account>
?details=true`) lists one row per location: Viva.com shows 44 rows for 40
jobs, same shortcode. Its `state` field is the region name ("Attica"), not
the job state. The Actor uses it only for descriptions and taxonomy, first
row per shortcode, and never reads its `state`; the v3 list is the record
of what exists.

#### 3. The server-side filters are typed, and a string is a 400

`department: ["Direct Sales"]` → `400 "department[0]" must be a number`;
`worktype: ["zzqq"]` → `400 does not match any of the allowed types`. Only
the free-text `query` and the boolean `remote` flag are ever sent;
`departments` / `locations` are matched locally against the board's own
words (`departmentsOnBoard` in the summary is the vocabulary).

#### 4. An account with no public jobs is `200 {"total":0}`; an unknown account is a bare 404

`hotjar`, `typeform` → total 0 (they exist on Workable). `zzqqnotanacct` →
`404 Not Found`. Reported as `empty_board` (or `no_matches` under a query)
vs `account_not_found`.

### Other things measured

- `published` is midnight UTC — date precision only. `createdAt` comes
  from the widget.
- `employmentType` (`full` / `part` / `contract` / `temporary`) is absent on
  some rows; the widget's label (`Full-time`) is kept beside it.
- A full last page still carries a `nextPage`; the page after it is empty.
  `pagesWalked` can be `ceil(total/10) + 1`.
- No throttling or anti-bot layer seen; a page is ~5 KB, the widget ~5 KB
  per job.

### Output

- **`JOB`** — `jobId`, `shortcode`, `title`, `url`, `applyUrl`,
  `departments`, `employmentType`, `employmentTypeLabel`, `workplace`,
  `isRemote`, `location`, `locationDetail`, `locations`, `language`,
  `requisitionCode`, `published`, `createdAt`, `experience`, `education`,
  `function`, `industry`, `description`, `descriptionHtml`, `account`,
  `resultPosition`.
- **`BOARD_SUMMARY`** — `account`, `boardUrl`, `query`,
  `totalJobsReported`, `jobsReturned`, `pagesWalked`, `rowsWithDescription`,
  `widgetRows`, `stoppedReason` (`exhausted`, `max_items`, `empty_board`,
  `no_matches`, `cursor_loop`, `max_pages`), `filteredOut`,
  `departmentsOnBoard`, `locationsOnBoard`.
- **`ERROR`** — `invalid_input`, `account_not_found`,
  `descriptions_unavailable` (widget failed; JOB rows still shipped),
  `payload_shape_changed`, `fetch_failed`, with detail.

### Known limits

- Salary is not a field on either endpoint; it appears only inside
  descriptions when the company writes it.
- `searchTerms` are Workable's search, not a local match; what it indexes
  is Workable's choice.
- Internal / unlisted postings are not served.

# Actor input Schema

## `accounts` (type: `array`):

Workable account slugs or board URLs: 'viva', 'skroutz', https://apply.workable.com/viva/ (a job URL under the account works too; a bare apply.workable.com/j/<shortcode> link carries no account and is refused). An unknown slug is reported as account\_not\_found; an account with no public jobs gets a summary with 0 jobs.

## `searchTerms` (type: `array`):

Keywords sent to Workable's own search, one walk per account x term (rows are de-duplicated across terms). Empty = the whole board.

## `departments` (type: `array`):

Keep jobs whose department names contain any of these (case-insensitive, applied locally). departmentsOnBoard in the summary row lists the vocabulary. Workable's server-side department filter takes numeric ids only; it is not used.

## `locations` (type: `array`):

Keep jobs whose city, region, country or country code contains any of these ('paris', 'greece', 'GR'). Applied locally.

## `remoteOnly` (type: `boolean`):

Ask Workable for remote jobs only (server-side flag) and keep only rows whose workplace is remote.

## `includeDescription` (type: `boolean`):

On by default: one extra request per account fetches Workable's legacy board widget, which carries every job's description (requirements and benefits inline) plus employment type label, experience, education, function and industry. Off = list fields only.

## `maxItems` (type: `integer`):

Overall cap on JOB rows across every account and term. Boards are walked 10 jobs per page; the cap stops the walk early.

## `maxConcurrency` (type: `integer`):

Requests in flight at once, across accounts.

## `minRequestInterval` (type: `integer`):

Politeness delay between request starts. No throttling seen; rarely needed.

## `proxyConfiguration` (type: `object`):

Optional. A public API published for embedding; no anti-bot layer. Enable Apify's free datacenter proxy only if a cloud run reports fetch\_failed.

## Actor input object example

```json
{
  "accounts": [
    "viva"
  ],
  "remoteOnly": false,
  "includeDescription": true,
  "maxItems": 1000,
  "maxConcurrency": 3,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "accounts": [
        "viva"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/workable-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "accounts": ["viva"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/workable-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "accounts": [
    "viva"
  ]
}' |
apify call scrapyx/workable-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/workable-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/wlAoDxpVcM0zvvmZV/builds/2uK4jJPt4aE6OkI90/openapi.json
