# Cwjobs UK $1💰 URL Search | Keyword Filtering | Rich Output (`abotapi/cwjobs-scraper`) Actor

From $1/1k. Scrape tech and IT jobs from CWJobs.co.uk into clean, structured data. Search by keyword, location, salary, work type, date, and sort order, or use job/listing URLs. Returns salary, GPS location, full description, dates, and rich employer details.

- **URL**: https://apify.com/abotapi/cwjobs-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Lead generation, Developer tools
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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?

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

## CWJobs Scraper - UK Tech Jobs, Salaries, Employers and Company Profiles

Scrape tech and IT job listings from CWJobs.co.uk into clean, structured rows. Search by keyword and location with filters for work type, salary, posting date and sort order, or paste listing and job URLs you already have. Every job comes back with the title, salary, location with GPS, full description, posting and expiry dates, and a rich employer profile (company size, founded year, industry, total jobs, and a profile video where available).

### Why this scraper

- 40+ fields per job, including employer profile data (company size, founded year, industry, jobs count, profile video) that other CWJobs scrapers do not return.
- Listing-first design walks about 25 jobs per request, so it is fast and light on connection usage.
- Structured salary parsing: minimum, maximum, currency and period pulled from the salary band.
- GPS coordinates, postal code and full address on every job that publishes them.
- Two modes: keyword and location search, or direct URL input (listing pages and single job pages).
- Server-side filters for work type, minimum salary, posting date and sort order, all verified to narrow results.
- Resilient connections: automatic retry on refused requests, connection rotation across regions, and a backup route, with a clear warning when a paid proxy is recommended.
- Optional detail fetching: turn it off for a faster, leaner run that still returns the core listing fields.
- Unlimited full-catalogue walks: leave Max listing pages at 0 to walk every page (bounded by Max jobs, the site's own last page, or a repeat-page guard, not an artificial page cap).
- Delta resume: point a new run at a previous run or dataset ID to collect only jobs not already gathered, and the run survives a platform migration or a Resurrect without duplicate rows or double charges.
- Incremental & dedup mode: schedule this actor daily/weekly against the same search and get back only what changed (NEW / UPDATED / REAPPEARED / EXPIRED), with suppressed unchanged jobs neither returned nor billed.

### Data you get

> Sample shape, values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| jobId | "00000001" |
| externalId | "00000000-0000-0000-0000-000000000000" |
| jobUrl | "https://www.cwjobs.co.uk/job/sample-job-title/sample-employer-job00000001" |
| title | "Sample Senior Software Engineer" |
| description | "Full job description HTML appears here when fetchDetails is on." |
| snippet | "Short preview of the job text from the listing card." |
| workType | "Permanent" |
| employmentType | "FULL\_TIME" |
| industry | "IT, IT-Software Development" |
| datePosted | "2026-01-01T00:00:00.000Z" |
| validThrough | "2026-02-01T00:00:00.000Z" |
| postedAgo | "3 days ago" |
| isPremium | true |
| isTopJob | false |
| isFreeListing | false |
| directApply | true |
| applyType | "internal" |
| jobLocationType | "TELECOMMUTE" |
| salaryText | "£60,000 - £70,000 per annum" |
| salaryMin | 60000 |
| salaryMax | 70000 |
| salaryCurrency | "GBP" |
| salaryPeriod | "annum" |
| locationText | "Sample Town, Sample Region, AA1, GB" |
| locationLocality | "Sample Town" |
| locationRegion | "Sample Region" |
| locationPostalCode | "AA1" |
| locationCountry | "GB" |
| latitude | 51.5000 |
| longitude | -0.1000 |
| employerName | "Sample Employer" |
| employerId | 0000000 |
| employerLogoUrl | "https://www.cwjobs.co.uk/CompanyLogos/00000000000000000000000000000000.png" |
| employerProfileUrl | "https://www.cwjobs.co.uk/jobs/sample-employer?cmpId=0000000" |
| companyEmployees | "50 - 199" |
| companyFounded | 1999 |
| companyIndustries | \["IT & Internet"] |
| companyJobsCount | 12 |
| employer | { full nested employer profile, including description and videoUrl } |
| scrapedAt | "2026-01-01T00:00:00.000Z" |

### How to use

Basic keyword and location search:

```json
{
  "mode": "search",
  "keywords": ["javascript"],
  "locations": ["london"],
  "maxPages": 3
}
```

Full-catalogue walk (no page cap, stop at 500 jobs):

```json
{
  "mode": "search",
  "keywords": ["javascript"],
  "locations": ["london"],
  "maxPages": 0,
  "maxListings": 500
}
```

Resume a previous run, collecting only new jobs since then:

```json
{
  "mode": "search",
  "keywords": ["javascript"],
  "locations": ["london"],
  "maxPages": 0,
  "maxListings": 0,
  "resumeFromRunId": "<a previous run ID or dataset ID>"
}
```

Incremental mode, daily monitoring of the same search (only changes are returned/billed after the first run):

```json
{
  "mode": "search",
  "keywords": ["javascript"],
  "locations": ["london"],
  "incrementalMode": true
}
```

Search with filters (permanent roles, at least £60k per year, posted in the last week, newest first):

```json
{
  "mode": "search",
  "keywords": ["data engineer", "devops"],
  "locations": ["london", "manchester"],
  "workType": "permanent",
  "minSalary": 60000,
  "salaryPeriod": "annual",
  "postedWithin": "7",
  "sortBy": "date",
  "maxPages": 5,
  "maxListings": 200
}
```

Lean run, no detail fetching (faster, fewer connections):

```json
{
  "mode": "search",
  "keywords": ["python"],
  "locations": ["remote"],
  "fetchDetails": false,
  "maxPages": 2
}
```

Direct URL input (listing pages and a single job page in one run):

```json
{
  "mode": "url",
  "urls": [
    "https://www.cwjobs.co.uk/jobs/javascript/in-london",
    "https://www.cwjobs.co.uk/jobs/devops/in-manchester",
    "https://www.cwjobs.co.uk/job/sample-job-title/sample-employer-job00000001"
  ],
  "maxPages": 2
}
```

### Resume vs. incremental mode

These are two different things:

- **`resumeFromRunId`** continues **one specific interrupted run**. Paste a previous run or dataset ID
  and this run returns only jobs not already in that dataset. Good for a large one-off crawl that got
  cut off, or for splitting a big pull across several runs.
- **`incrementalMode`** is for **recurring monitoring of the same search** (e.g. a daily schedule). The
  actor remembers the previous run of the same search itself, in a key-value store baseline, and
  classifies every job against it:

  - `NEW` - not seen before.
  - `UPDATED` - a real field changed (see `changedFields`); the job description/salary/location/dates/
    work type genuinely differ from last time.
  - `UNCHANGED` - nothing real changed. **Suppressed by default** (not returned, not billed) unless you
    turn on `emitUnchanged`.
  - `REAPPEARED` - was `EXPIRED` in a previous run, now seen again.
  - `EXPIRED` - was tracked before, not found this run. Only emitted when `emitExpired` is on **and** the
    run proved a complete scan of the tracked search (not capped by Max jobs / Max listing pages, not a
    Resume run, no page fetch failure) - an incomplete scan cannot tell "gone" apart from "not reached
    yet" and would wrongly tombstone jobs it simply didn't get to.

  State is keyed on `mode`, `keywords`, `locations`, `workType`, `minSalary`, `salaryPeriod`,
  `postedWithin`, `sortBy`, `urls` and `fetchDetails` - i.e. everything that changes *what* is searched.
  `maxPages`/`maxListings`/`maxNotifyListings` are deliberately excluded: those are run-size caps, not
  search filters, so raising a limit does not start a fresh (re-billed) baseline. Set `stateKey` to name
  the campaign explicitly, or to deliberately share state across differently-configured runs.

  **Cap semantics differ in incremental mode.** Outside incremental mode, `maxListings` caps jobs
  *returned*. In incremental mode it caps jobs **scanned** instead (`src/main.py`'s `_progress()`/
  `at_cap()`), because an EMITTED-counting cap would let a quiet run with everything suppressed simply
  page deeper to backfill the limit with fresh rows - suppression would "work" but a recurring run would
  never actually get cheaper.

  **What is excluded from change detection** (and why it is safe to exclude): `scrapedAt` (stamped fresh
  every run - not job data), `postedAgo` (a relative-age string like "3 days ago" that changes by the
  clock alone with zero change to the job - hashing it would mark every job UPDATED on every run),
  `isTopJob` and `isFreeListing` (ad-tier/promo flags that flip with the employer's ad spend, not the job
  itself). All four remain present in the output record - they are just not part of the fingerprint used
  to decide NEW/UPDATED/UNCHANGED. `jobUrl` and other link fields are compared with any query string
  stripped, as a defensive measure against a future tracking/rank parameter (CWJobs job links currently
  carry none). Everything else - `title`, `description`, `salaryText`/`salaryMin`/`salaryMax`,
  `locationText`, `workType`, `employmentType`, `datePosted`, `validThrough`, and the full employer
  profile - is real data and does trigger `UPDATED`.

### Input parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| mode | string | "search" | "search" builds listing pages from your keywords and locations; "url" takes URLs you already have. |
| keywords | array | \["javascript"] | Search mode. Job keywords or titles, one per line. Empty lists every job in the chosen locations. |
| locations | array | \["london"] | Search mode. Locations, one per line (e.g. "london", "remote"). Empty searches the whole UK. |
| workType | string | "any" | Filter by contract type: any, permanent, contract, temporary. |
| minSalary | integer | (none) | Only jobs paying at least this amount in the period below. |
| salaryPeriod | string | "annual" | Period for the minimum salary: annual, daily, hourly. |
| postedWithin | string | "any" | Only jobs posted within: any, 1, 3, 7 or 14 days. |
| sortBy | string | "relevance" | "relevance" (site default) or "date" (most recent first). |
| urls | array | (example) | URL mode. Listing URLs or single job URLs. |
| fetchDetails | boolean | true | Open each job page for the full description, GPS, dates, work type and employer profile. Turn off for a leaner run. |
| maxPages | integer | 0 | Listing pages to walk per search or URL (about 25 jobs per page). 0 means unlimited: the run stops at maxListings, the site's own last page, or a repeat-page guard, not an artificial page cap. |
| maxListings | integer | 20 | Maximum jobs across all searches. This is the main limit. 0 means unlimited (bounded by maxPages, if set). In incremental mode this counts jobs **scanned**, not returned. |
| resumeFromRunId | string | (none) | ID of a previous run of this actor (or a dataset ID). Jobs already in that dataset are skipped, so this run returns only new jobs (a delta). For recurring monitoring, use incrementalMode instead. |
| incrementalMode | boolean | false | Turn on for recurring monitoring of the same search. Classifies each job as NEW/UPDATED/UNCHANGED/REAPPEARED/EXPIRED against a remembered baseline. See "Resume vs. incremental mode" above. |
| stateKey | string | (none) | Optional name for the incremental-mode baseline. Leave empty to derive one automatically from the search/URL and filter/detail settings. |
| emitUnchanged | boolean | false | Incremental mode only. Also return (and bill) jobs with no real change, marked UNCHANGED. |
| emitExpired | boolean | false | Incremental mode only. Also return (and bill) jobs no longer found, marked EXPIRED - only once a run fully scans the tracked search. |
| proxy | object | Residential GB | Connection settings. Residential (United Kingdom) is recommended. |

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape, the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Output example

> Sample shape, values are illustrative placeholders, not from a live listing.

```json
{
  "rowType": "job",
  "listingUrl": "https://www.cwjobs.co.uk/jobs/javascript/in-london",
  "jobId": "00000001",
  "externalId": "00000000-0000-0000-0000-000000000000",
  "jobUrl": "https://www.cwjobs.co.uk/job/sample-job-title/sample-employer-job00000001",
  "title": "Sample Senior Software Engineer",
  "description": "Full job description HTML appears here when fetchDetails is on.",
  "workType": "Permanent",
  "industry": "IT, IT-Software Development",
  "datePosted": "2026-01-01T00:00:00.000Z",
  "validThrough": "2026-02-01T00:00:00.000Z",
  "directApply": true,
  "applyType": "internal",
  "salaryText": "£60,000 - £70,000 per annum",
  "salaryMin": 60000,
  "salaryMax": 70000,
  "salaryCurrency": "GBP",
  "salaryPeriod": "annum",
  "locationText": "Sample Town, Sample Region, AA1, GB",
  "locationPostalCode": "AA1",
  "latitude": 51.5000,
  "longitude": -0.1000,
  "employerName": "Sample Employer",
  "employerId": 0000000,
  "companyEmployees": "50 - 199",
  "companyFounded": 1999,
  "companyIndustries": ["IT & Internet"],
  "companyJobsCount": 12,
  "employer": {
    "name": "Sample Employer",
    "employees": "50 - 199",
    "founded": 1999,
    "industries": ["IT & Internet"],
    "jobsCount": 12,
    "description": "Company profile description appears here.",
    "videoUrl": "https://player.vimeo.com/video/000000000"
  },
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

When `incrementalMode` is on, every returned job also carries `changeType` ("NEW"/"UPDATED"/"UNCHANGED"/
"REAPPEARED"/"EXPIRED"), `changedFields` (array of field names that differ from the previous run, empty
unless `changeType` is "UPDATED"), `firstSeenAt` and `lastSeenAt` (ISO timestamps). These four fields are
absent on a normal (non-incremental) run.

### Plan requirement

CWJobs admits clean UK residential connections most reliably. Apify Residential with country United Kingdom is recommended and is the default. Datacenter connections also work, but a share of datacenter addresses are refused, so the actor retries automatically on a fresh connection; expect a few retries on datacenter. Residential proxy access is included on the Apify Starter plan and above. On the free plan, pick a residential proxy if your account includes it, otherwise the run may return few or no results.

### Verification note (2026-08-31)

Re-verified against the live site on 2026-08-31, and one defect found and fixed: the site had started answering gated pages with a solvable edge interstitial (a token plus an arithmetic proof-of-work) that the actor used to burn all retries on and then return zero items. The fetch loop now clears that interstitial in-session and re-fetches the same URL, and the run keeps its five-attempt cap. Verified live with the actor's own client: the all-UK listing search reported 11,350 jobs, the Python-in-London keyword narrowed it to 597, the permanent + 70k salary facet to 207, and a pasted job URL parsed the full detail record. Version bumped with the fix.

# Actor input Schema

## `mode` (type: `string`):

How to choose jobs. 'Search' builds listing pages from your keywords and locations. 'Direct URLs' takes CWJobs listing or job URLs you already have.

## `keywords` (type: `array`):

Search mode only. Job keywords or titles, one per line (e.g. 'javascript', 'devops', 'data engineer'). Leave empty to list every job in the chosen locations. Each keyword is combined with each location.

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

Search mode only. Locations, one per line (e.g. 'london', 'manchester', 'remote'). Leave empty to search the whole UK. Each location is combined with each keyword.

## `workType` (type: `string`):

Filter by contract type. 'Any' applies no filter.

## `minSalary` (type: `integer`):

Only jobs paying at least this amount (in the salary period below). Leave empty for no salary filter.

## `salaryPeriod` (type: `string`):

The period the minimum salary applies to.

## `postedWithin` (type: `string`):

Only jobs posted within this many days. 'Any time' applies no filter.

## `sortBy` (type: `string`):

Result ordering. 'Most recent' sorts by posting date (newest first); 'Relevance' is the site default.

## `urls` (type: `array`):

URL mode only. CWJobs listing URLs (e.g. https://www.cwjobs.co.uk/jobs/javascript/in-london) or direct job URLs (e.g. https://www.cwjobs.co.uk/job/...-job12345678). Multi-URL supported. Filter fields above are ignored in URL mode; pagination starts at the page in the URL and walks forward.

## `fetchDetails` (type: `boolean`):

Open each job page for the full description, GPS coordinates, posting and expiry dates, work type, and the rich employer profile (company size, founded year, industry, total jobs, video). Turn off for a faster, leaner run that returns only the fields visible on the listing page.

## `maxPages` (type: `integer`):

How many listing pages to walk per search or URL (about 25 jobs per page). Leave at 0 to walk the whole catalogue: the run stops at Max jobs, the site's own last page, or a repeat-page guard, not an artificial page cap.

## `maxListings` (type: `integer`):

Maximum number of jobs to scrape across all searches. This is the main limit. Set 0 for unlimited (bounded by Max listing pages, if set). In Incremental mode this counts jobs SCANNED, not jobs returned — otherwise a quiet recurring run with nothing changed would page deeper to backfill this many rows and cost would never drop.

## `resumeFromRunId` (type: `string`):

Optional. ID of a previous run of this actor (or a dataset ID). Jobs already in that dataset are skipped, so this run returns only NEW jobs (a delta). Combine both runs' datasets for the full set. Max jobs then counts only the new jobs. For recurring daily/weekly monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching jobs as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED jobs. Turn on "Emit unchanged" or "Emit expired" only when you also want those jobs returned (and billed). State is kept separately for each search/URL and filter/detail setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/URL and filter/detail settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return jobs that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return jobs that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max jobs / Max listing pages capped it, or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `proxy` (type: `object`):

CWJobs admits UK residential connections reliably. Apify Residential (United Kingdom) is recommended. Datacenter also works but a portion of datacenter IPs are refused, so the actor automatically retries on a fresh connection; expect a few retries. The actor rotates connections, falls back across countries, and uses a backup route if the primary keeps getting refused.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON, the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "keywords": [
    "javascript"
  ],
  "locations": [
    "london"
  ],
  "workType": "any",
  "salaryPeriod": "annual",
  "postedWithin": "any",
  "sortBy": "relevance",
  "urls": [
    "https://www.cwjobs.co.uk/jobs/javascript/in-london"
  ],
  "fetchDetails": true,
  "maxPages": 0,
  "maxListings": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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 = {
    "mode": "search",
    "keywords": [
        "javascript"
    ],
    "locations": [
        "london"
    ],
    "urls": [
        "https://www.cwjobs.co.uk/jobs/javascript/in-london"
    ],
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "GB"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/cwjobs-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 = {
    "mode": "search",
    "keywords": ["javascript"],
    "locations": ["london"],
    "urls": ["https://www.cwjobs.co.uk/jobs/javascript/in-london"],
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "GB",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/cwjobs-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 '{
  "mode": "search",
  "keywords": [
    "javascript"
  ],
  "locations": [
    "london"
  ],
  "urls": [
    "https://www.cwjobs.co.uk/jobs/javascript/in-london"
  ],
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}' |
apify call abotapi/cwjobs-scraper --silent --output-dataset

```

## MCP server setup

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