# Workable Jobs Scraper | $1.99 / 1k | (`glasswing/workable-jobs-scraper`) Actor

Scrape live job ads from Workable boards. Search by keyword, location or remote status, or pull one company board. Title, company, domain, city, workplace type, department, apply link and full ad text.

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

## Pricing

from $1.99 / 1,000 job results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### What does Workable Jobs Scraper do?

Get a live feed of job adverts straight from the employers who posted them: title, company, company domain, city, country, remote or hybrid, department, dates, apply link and the **full advert text**. It is built for job boards, recruiters and sales teams who need first-party hiring data rather than a second-hand aggregator copy.

Point it at a keyword search, one company's careers page or a single posting and **scrape Workable jobs** into JSON, CSV or Excel, or read them from the Apify API. It is the practical way to turn the public [Workable job board](https://jobs.workable.com) into a **remote jobs API** or a **company hiring data feed** without an API key, a headless browser or residential proxies.

Workable hosts the careers pages of tens of thousands of small and mid-sized employers, and its public board carries **170,000+ live openings** at any moment. Every advert comes straight out of the employer's own applicant-tracking system, so there are no aggregator duplicates and no stale reposts. The default 20-job run finished in **3.1 seconds** on the Apify platform.

It does **not** log in, solve captchas, circumvent access controls or collect private user data. It reads only pages that anyone can open in a browser.

### Use cases for this Workable scraper

- **Job board or aggregator seeding listings.** Keep a niche board - remote engineering, healthcare, one country - stocked with fresh, complete adverts including `description`, `department` and `applyUrl`, instead of thin stubs that send users elsewhere.
- **Sales team finding companies that are hiring.** A new opening is a buying signal. Filter on `department` and `title`, then enrich your CRM with `company`, `companyDomain` and `country` to reach the buyer while the budget is still open.
- **Recruitment agency building a client pipeline.** Track which employers post engineering or sales roles week after week, and approach the ones whose team is visibly scaling - `createdAt` and `publishedAt` show exactly when each search started.
- **Market researcher tracking hiring trends.** Build time series of postings by `department`, `workplace` and `country`, or measure how quickly a single company grows a team by re-running its board on a schedule.
- **Talent sourcing and career tools.** Power a job-matching feature, a remote-work newsletter or a "who is hiring for X" digest, using `isRemote` and `location` to route each advert to the right audience.
- **Competitive and portfolio monitoring.** Watch a defined list of companies by slug and get an alert the moment one of them opens a role in a new city, a new department or a new language.

### What data you get from every Workable job advert

One row per job advert. Every column below exists in the dataset schema; the **Jobs overview** table view shows `title`, `company`, `location`, `workplace`, `employmentType`, `department`, `publishedAt`, `jobUrl`, `status`, `error` and `scrapedAt`, and the **Full job records** view shows all of them.

**Row status and provenance**

| Field | Type | Description |
|---|---|---|
| `url` | string | Source URL the row was extracted from. On `not_found` / `error` rows this is the page that failed. |
| `status` | string | `ok`, `not_found` or `error` (see the table below). |
| `error` | string | Reason when `status` is not `ok`. Absent on `ok` rows. |
| `scrapedAt` | string | ISO 8601 time of extraction. |

**Job identity and links**

| Field | Type | Description |
|---|---|---|
| `jobId` | string | Stable UUID of the posting. |
| `shortcode` | string | Short public job code used in the job URL. |
| `title` | string | Job title as published by the employer. |
| `jobUrl` | string | Public job page on the Workable board. |
| `applyUrl` | string | Where the application starts: the employer's external link when there is one, otherwise the job page. |

**Company**

| Field | Type | Description |
|---|---|---|
| `company` | string | Hiring company name. |
| `companyDomain` | string | Bare company domain, e.g. `eurodyn.com`. Empty when the employer published no website. |
| `companyUrl` | string | Company page listing all of that employer's open roles. |
| `accountSlug` | string | Workable account slug (`apply.workable.com/<slug>`), set when the run started from a company slug or careers-site URL, or when the employer lists that page as its website. |

**Location and workplace**

| Field | Type | Description |
|---|---|---|
| `location` | string | Location as displayed, e.g. `Athens, Attica, Greece`. |
| `city` | string | City part of the location. |
| `region` | string | State, province or region part of the location. |
| `country` | string | Country name. |
| `isRemote` | boolean | True when the workplace type is remote. |
| `workplace` | string | `remote`, `hybrid` or `on_site`. |

**Role details**

| Field | Type | Description |
|---|---|---|
| `employmentType` | string | `Full-time`, `Part-time`, `Contract`, ... Empty when the employer left it blank. |
| `department` | string | Department or team, e.g. `Engineering`. |
| `language` | string | ISO language code of the advert, e.g. `en`. |

**Dates**

| Field | Type | Description |
|---|---|---|
| `createdAt` | string | ISO 8601 time the posting was created. |
| `publishedAt` | string | ISO 8601 time the posting was last (re)published. |

**Advert text**

| Field | Type | Description |
|---|---|---|
| `description` | string | The full advert as plain text: description, requirements and benefits. Needs `includeDescription`. |
| `descriptionHtml` | string | The same advert with the employer's original HTML markup. Needs `includeDescription`. |

A missing optional field is honest: it means the employer did not publish that value, never a guess.

#### Result status (tri-state output)

| `status` | Meaning | Billed? |
|---|---|---|
| `ok` | The advert was found and extracted. | Yes |
| `not_found` | The board answered, but there is no such job or company, or the search has zero hits. | No |
| `error` | The board could not be read after retries (network error or a layout change). `error` says why. | No |

### How to scrape Workable job listings

1. Open the Actor in Apify Console and click **Try for free**.
2. Fill in **Workable board URLs or company slugs**, or type a **Search query** instead. Every one of these works:
   - `https://jobs.workable.com/search?query=python%20developer` - keyword search across all companies
   - `https://jobs.workable.com/company/<id>/jobs-at-acme` - one company's board
   - `https://jobs.workable.com/view/<code>/<slug>` - a single job posting
   - `https://apply.workable.com/acme` - a company careers site (resolved to its board automatically)
   - `acme` - just the company slug
3. Optionally set **Location**, **Remote jobs only** and **Maximum results** (start with the default 20 to see the output).
4. Click **Start**. The default run finishes in **under 10 seconds**.
5. Open the **Output** tab or **Export** the dataset as JSON, CSV, Excel, XML or HTML.

To automate it, use the **API** tab (Node.js, Python, curl examples) or add a **Schedule**.

### Input

Every field is optional - the prefilled values already produce a complete run. See the **Input** tab for the full schema.

| Field | Type | Default | When to change it |
|---|---|---|---|
| `startUrls` | array of strings | `["https://jobs.workable.com/search?query=python%20developer"]` | Set the exact boards you want: search URLs, company board URLs, single job URLs, `apply.workable.com/<slug>` URLs or bare company slugs, one per line. Use this whenever you already know the page or the companies to watch. |
| `query` | string | empty | Use it instead of `startUrls` to search every company on the board by keyword, e.g. `react developer` or `nurse`. Ignored when `startUrls` is set. |
| `location` | string | empty (worldwide) | Add a place filter - `Germany`, `Berlin, Germany` - to keep a run small and relevant instead of filtering afterwards. Applies to keyword searches, not to a single company board. |
| `remoteOnly` | boolean | `false` | Turn it on to keep only postings whose `workplace` is `remote`; hybrid and on-site postings are skipped. |
| `maxItems` | integer | `20` | Raise it for production pulls, lower it while testing. The board returns 20 jobs per request, so a multiple of 20 wastes nothing. This is your main spending cap. |
| `includeDescription` | boolean | `true` | Turn it off when you only need the hiring signal (company, title, location): rows get roughly 10x smaller and export faster. The price per row does not change. |
| `proxyConfiguration` | object | Apify Proxy off | Leave it off - the board answers plain HTTP from datacenter IPs. Enable the Apify Proxy datacenter group only if you start seeing `error` rows. |

Example input (exactly the prefilled default):

```json
{
    "startUrls": ["https://jobs.workable.com/search?query=python%20developer"],
    "maxItems": 20,
    "includeDescription": true
}
```

Searching by keyword instead of URL:

```json
{
    "query": "customer support",
    "location": "Germany",
    "remoteOnly": true,
    "maxItems": 100
}
```

### Output

You can download the dataset as JSON, CSV, Excel, XML or HTML. This is a real row from a run of the default input (advert text shortened here; the dataset carries all 3,248 characters):

```json
{
    "url": "https://jobs.workable.com/view/87gW9bnZMGhiuouxYfmutJ/remote-python-developer-in-athens-at-european-dynamics",
    "status": "ok",
    "scrapedAt": "2026-09-08T11:46:41.856Z",
    "jobId": "399040a9-8c84-4a3d-ab9d-02ab036d6cf8",
    "shortcode": "87gW9bnZMGhiuouxYfmutJ",
    "title": "Python Developer",
    "company": "EUROPEAN DYNAMICS",
    "companyDomain": "eurodyn.com",
    "companyUrl": "https://jobs.workable.com/company/fEngW8oPQVhXrenX1cLyBg/jobs-at-european-dynamics",
    "location": "Athens, Attica, Greece",
    "city": "Athens",
    "region": "Attica",
    "country": "Greece",
    "isRemote": true,
    "workplace": "remote",
    "employmentType": "Full-time",
    "department": "PRA",
    "createdAt": "2026-09-01T10:15:25.844Z",
    "publishedAt": "2026-09-01T10:15:25.844Z",
    "jobUrl": "https://jobs.workable.com/view/87gW9bnZMGhiuouxYfmutJ/remote-python-developer-in-athens-at-european-dynamics",
    "applyUrl": "https://jobs.workable.com/view/87gW9bnZMGhiuouxYfmutJ/remote-python-developer-in-athens-at-european-dynamics",
    "language": "en",
    "description": "Are you a Python Developer looking for an exciting opportunity to join a dynamic team? Join us in Athens or work remotely, and become part of our expanding development teams working on innovative projects...",
    "descriptionHtml": "<p>Are you a <strong>Python Developer</strong> looking for an exciting opportunity to join a dynamic team? ...</p>"
}
```

Rows that are not `ok` look like this and are never billed:

```json
[
    {
        "url": "https://jobs.workable.com/search?query=python&remote=true",
        "status": "not_found",
        "error": "No results on this page",
        "scrapedAt": "2026-09-08T11:46:42.101Z"
    },
    {
        "url": "https://jobs.workable.com/company/does-not-exist",
        "status": "not_found",
        "error": "HTTP 404: page does not exist",
        "scrapedAt": "2026-09-08T11:46:42.140Z"
    }
]
```

### How much does it cost to scrape Workable jobs?

This Actor uses **pay-per-event** pricing:

| Event | Price | Charged when |
|---|---|---|
| Actor start | $0.005 per run | Once when the run starts. The platform charges one event per GB of memory and the Actor runs on 1 GB, so this is one event. |
| Job result | $0.00199 per job | One job advert with `status: ok` was saved to the dataset. |

**1,000 job adverts cost about $2.00** ($0.005 to start plus 1,000 x $0.00199); 100 jobs cost about $0.20. You are never charged for a row with `status` `not_found` or `error`, and never for a duplicate: the board can reshuffle its ranking between two paginated requests, so the Actor drops any posting whose `jobId` it has already emitted in the same run. The free Apify plan's $5 of monthly credit covers about 2,500 jobs in a single run.

You can cap spending per run with **Maximum results** and with the run's **Max total charge** option; the Actor stops gracefully when either limit is reached and keeps everything it has already saved.

### How it compares

This Actor is built for breadth and freshness on one source. Because every advert comes from the employer's own applicant-tracking system, you get the complete original text, a real `companyDomain` and no duplicate reposts - and because it is pure HTTP, a full advert costs a fraction of a request and a run finishes in seconds.

It is not for you if you need salary data (the public board does not publish it as a structured field), if you need coverage of large enterprises that use a different hiring system, if you want deep pagination through a single company's board, or if you need a vendor-backed feed with a stability guarantee. It reads one public source, and reports honestly when that source changes.

### Integrations, exports and API access

Results export to **CSV, JSON, Excel**, XML and HTML from the **Output** tab or the dataset API. From there, Apify integrations and webhooks push the same rows into **Google Sheets, Airtable or a CRM**, plus Slack, Amazon S3 or your own endpoint - directly, or through Zapier, Make and n8n. Add a **Schedule** and today's new adverts land in your sheet every morning.

The Actor can also be called from the **Apify API** (Node.js, Python and curl snippets are on the **API** tab), from the **Apify MCP server**, or by any AI agent that can call a tool: the inputs are plain strings and booleans, and every row carries `status` and `error`, so an agent can tell an empty search apart from a failure without parsing prose.

### Tips

- Keep `maxItems` a multiple of 20 - the board hands out 20 jobs per request, so nothing is wasted.
- Pass several start URLs in one run instead of many single-URL runs; you pay the start fee once.
- Combine `query` with `location` and `remoteOnly` to keep runs small and relevant instead of filtering afterwards.
- Set `includeDescription` to `false` when you only need the hiring signal (company, title, location); rows get roughly 10x smaller and export faster. The price per row is unchanged.
- Schedule a daily run with the same search and diff the `jobId` column to get "new jobs today".

### Limitations

- **A single company board returns its first 20 openings.** The public company page server-renders one page of results; larger employers publish more. To reach the rest, use a keyword search (which paginates through thousands of results) or the company's own careers site.
- **No salary field.** Workable's public board does not expose structured salary data. When an employer mentions pay, it is inside `description`.
- **`employmentType` and `department` are often empty** - they are optional for the employer, and many leave them blank.
- **Only published, public jobs.** Internal, draft, archived and password-protected postings are invisible to this Actor, and it does not log in to reach them.
- **Board-hosted adverts only.** Employers who redirect applicants to another system get an external `applyUrl`, but the advert text still comes from the Workable board.
- Results reflect the public page at the time of the run. If the board changes its layout, rows come back as `error` rather than silently wrong; report it in the **Issues** tab and the Actor is updated quickly.

### FAQ

**Do I need a Workable API key or an account?** No. The Actor reads the same public pages a visitor sees; there is no login and no key. All you need is an Apify account.

**How do I get more results?** Raise `maxItems` and search by keyword rather than by company: a keyword search paginates through thousands of hits, while one company board serves only its first 20 openings. Widen the `query`, drop `location` or `remoteOnly`, or pass several start URLs in a single run.

**How often does the data change?** Continuously. Employers publish, edit and close roles through the day, and `publishedAt` moves whenever a posting is republished. Schedule a daily run and diff the `jobId` column if you want a clean "new today" list.

**What happens if the board changes?** Rows come back with `status: error` and a reason instead of silently wrong data, so your pipeline can alert on it. Report it in the **Issues** tab with the run ID; you are not billed for `error` rows in the meantime.

**Does it return recruiter e-mails or phone numbers?** No. See the notice below: no personal-contact column exists in the output, and any e-mail address an employer typed into their own advert is removed from the text before the row is saved.

**Can I use this Actor from an AI agent or MCP client?** Yes. Inputs are plain strings, every row is self-describing (`status` plus `error`), and results can be fetched via the dataset API or the Apify MCP server.

**Why did I get fewer rows than `maxItems`?** The search had fewer hits, the company board has fewer openings, or the run hit your **Max total charge** limit. Check the run log for "Stopping gracefully".

**Can I scrape several companies at once?** Yes. Put one slug or careers-site URL per line in **Workable board URLs or company slugs**; they are all crawled in the same run and you pay the start fee once.

### Legal and data-protection notice

This Actor extracts only data that Workable publishes publicly on its job board, and only business data about **jobs and hiring companies**: job titles, adverts, locations, departments, workplace types, dates, company names and company websites.

It does **not** emit recruiter or hiring-manager names, e-mail addresses or phone numbers. The board's public payload contains no such fields, the Actor copies only the columns documented above, and as an extra safeguard any e-mail address or `mailto:` / `tel:` link that an employer typed into their own advert text is replaced with `[email removed]` / `[contact removed]` before the row is saved. The Actor does not log in, does not solve captchas and does not circumvent access controls.

Job adverts can nevertheless mention individuals. Personal data is protected by the GDPR in the European Union and by other regulations around the world; you should not process personal data without a legitimate reason. If you are unsure whether your reason is legitimate, consult your lawyers. You are responsible for complying with Workable's terms of service and with applicable law when you use the extracted data.

This Actor is an independent tool. It is not affiliated with, endorsed by or sponsored by Workable or its owners. All trademarks belong to their respective owners.

# Actor input Schema

## `startUrls` (type: `array`):

One entry per line. Accepted: a job-board search URL (https://jobs.workable.com/search?query=python), a company board (https://jobs.workable.com/company/<id>), a single job page (https://jobs.workable.com/view/<code>/<slug>), a company careers site (https://apply.workable.com/acme) or just the bare company slug (acme). Company slugs are resolved to that company's public board automatically.

## `query` (type: `string`):

Free-text keyword search across every company hosted on Workable (job titles, descriptions and company names), e.g. `react developer` or `nurse`. Used only when Start URLs is empty.

## `location` (type: `string`):

Place to search in, as typed on the board: a country (`Germany`), a city with country (`Berlin, Germany`) or a region. Leave empty for worldwide.

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

Return only postings whose workplace type is `remote`. Hybrid and on-site postings are skipped.

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

Stop after this many rows have been saved. Each saved row with status `ok` is one billable result. The board returns 20 jobs per request, so a value that is a multiple of 20 wastes nothing.

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

Add the full advert to every row as `description` (plain text) and `descriptionHtml` (original markup). Turn off for a lighter dataset; the price per row does not change.

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

Optional and off by default. The board answers plain HTTP requests from Apify's datacenter IPs, so no proxy is needed; enable the datacenter group only if you see `error` rows. Residential proxies are not required.

## Actor input object example

```json
{
  "startUrls": [
    "https://jobs.workable.com/search?query=python%20developer"
  ],
  "remoteOnly": false,
  "maxItems": 20,
  "includeDescription": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "startUrls": [
        "https://jobs.workable.com/search?query=python%20developer"
    ],
    "maxItems": 20,
    "includeDescription": true,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("glasswing/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 = {
    "startUrls": ["https://jobs.workable.com/search?query=python%20developer"],
    "maxItems": 20,
    "includeDescription": True,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("glasswing/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 '{
  "startUrls": [
    "https://jobs.workable.com/search?query=python%20developer"
  ],
  "maxItems": 20,
  "includeDescription": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call glasswing/workable-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,glasswing/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/Z878O8CWy3txp5c4h/builds/bxnkQ527Tn4RbRKwk/openapi.json
