CWJobs UK Scraper - Tech Jobs, Salaries & Employers
Pricing
from $1.00 / 1,000 results
CWJobs UK Scraper - Tech Jobs, Salaries & Employers
Scrape CWJobs.co.uk tech and IT job listings by keyword, location, salary, work type, date or URL. Extract job titles, salary ranges, GPS coordinates, full descriptions, posting dates and employer details. Supports advanced filters, sorting and structured data for job market research.
Pricing
from $1.00 / 1,000 results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
0
Monthly active users
15 hours ago
Last modified
Categories
Share
CWJobs Scraper: UK Tech Jobs, Salaries and Employer Profiles
CWJobs Scraper turns CWJobs.co.uk into structured job data. Search by keyword and location with filters for work type, minimum salary, posting date and sort order, or paste listing and job URLs you already have. Every job comes back with the title, salary band, GPS location, full description, posting and expiry dates, and a rich employer profile (company size, founded year, industry, total jobs at that employer, and a hosted profile video where published). Export the results to JSON, CSV or Excel, or pull them straight into your app through the API.
Why This Scraper?
- 40+ named fields per job, including a rich employer profile (company size, founded year, industry, total jobs at that employer, and a hosted profile video where published).
- Structured salary parsing. Minimum, maximum, currency and period are broken out from the salary text band, not left as free text.
- Full location data. GPS coordinates, postal code and locality on every job that publishes them.
- Two ways in. Keyword and location search with server-side filters (work type, minimum salary, posting recency, sort order), or paste listing and job URLs you already have.
- Unlimited catalogue walks. Leave Max listing pages at 0 to walk an entire search, bounded by Max jobs, the site's own last page, or a stop once a page adds nothing new, never an artificial page cap.
- Delta resume and incremental mode. Continue an interrupted run without duplicate rows or double charges, or schedule the same search and get back only NEW, UPDATED and REAPPEARED jobs, with unchanged jobs suppressed and never billed.
Use Cases
- Tech recruiters and staffing agencies: monitor new IT and software vacancies across UK cities and pull employer profiles for outreach lists.
- Compensation researchers: build salary bands for tech roles by keyword, location and work type from the parsed salary fields.
- Job boards and aggregators: mirror CWJobs listings into your own search product on a recurring schedule.
- Market and competitor trackers: watch an employer's hiring pace (jobs count) and job description changes over time with incremental mode.
Data You Get
Sample shape: values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
rowType | "job" |
jobId | "10000001" |
jobUrl | "https://www.cwjobs.co.uk/job/sample-job-title/sample-employer-job10000001" |
title | "Sample Senior Software Engineer" |
description | "Full job description text appears here when detail fetching is on." |
workType | "Permanent" |
employmentType | "FULL_TIME" |
industry | "IT, IT-Software Development" |
datePosted / validThrough | "2026-01-01T00:00:00.000Z" / "2026-02-01T00:00:00.000Z" |
directApply / applyType | true / "internal" |
salaryText | "£55,000 - £65,000 per annum" |
salaryMin / salaryMax | 55000 / 65000 |
salaryCurrency / salaryPeriod | "GBP" / "annum" |
locationText | "Sample Town, Sample County, AA1, GB" |
latitude / longitude | 51.5 / -0.1 |
employerName | "Sample Employer Ltd" |
companyEmployees / companyFounded | "50 - 199" / 2005 |
companyJobsCount | 8 |
employer | full nested employer profile object |
The dataset has a Detail view (full job and employer fields) and a Changes view (incremental changeType, changedFields, firstSeenAt, lastSeenAt). The output fields are declared in the dataset schema, so every row has a stable, typed shape.
Every job also carries externalId (a global job identifier), snippet and postedAgo (short preview text and relative recency, sourced from the listing card), isPremium / isTopJob / isFreeListing (ad-tier flags), jobLocationType and applicantLocationRequirements (remote/on-site signals). The site publishes no per-job work-from-home marker, so to get remote jobs use the workFromHome input filter instead, benefits (the employer's benefits list from the detail page, null when none is published), the full location breakdown (locationLocality, locationRegion, locationPostalCode, locationCountry), flat convenience copies of the employer block (employerId, employerLogoUrl, employerProfileUrl, companyIndustries), and scrapedAt. In incremental mode every returned job also carries changeType, changedFields, firstSeenAt and lastSeenAt.
How to Use
- Pick a mode:
search(keyword and location) orurl(paste listing or job URLs you already have). - In search mode, add keywords and locations, and set the filters you need (work type, minimum salary, posting recency, sort order).
- Leave Fetch full job details on (default) for the full record, or turn it off for a leaner run with only the fields visible on the listing page.
- Set Max jobs (and optionally Max listing pages) to control run size and cost, then click Start.
Basic keyword and location search:
{"mode": "search","keywords": ["javascript"],"locations": ["london"],"maxListings": 20}
Filtered search (permanent roles, at least £60k per year, posted in the last week, newest first):
{"mode": "search","keywords": ["javascript"],"locations": ["london"],"workType": "permanent","minSalary": 60000,"salaryPeriod": "annual","postedWithin": "7","sortBy": "date","maxListings": 100}
Direct URL input (two listing searches in one run):
{"mode": "url","urls": ["https://www.cwjobs.co.uk/jobs/javascript/in-london","https://www.cwjobs.co.uk/jobs/devops/in-manchester"],"maxListings": 50}
Incremental mode, daily monitoring of the same search (only changes are returned/billed after the first run):
{"mode": "search","keywords": ["javascript"],"locations": ["london"],"incrementalMode": true}
If you choose URL mode without editing the urls field, the run scrapes the eight pre-filled example URLs, one for each supported shape (keyword and location, keyword only, location only, work-type category, paginated, filtered, facet/sort, and a single job page). Replace them with your own URLs. Your query parameters (radius, postedwithin, sort, action) are kept on every page, and a page=N in the URL is the starting page.
A pasted job URL (a page under /job/, not a listing under /jobs/) returns one record for that job. Because it has no listing card behind it, its snippet, postedAgo and isPremium fields always come back empty (null or false) even with detail fetching on, and turning detail fetching off for a pasted job URL returns almost nothing, since there is no listing card to fall back on either. Paste a listing URL, or use search mode, to get every field on a job.
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/cwjobs-scraper").call(run_input={"mode": "search", "keywords": ["javascript"], "locations": ["london"]})for job in client.dataset(run["defaultDatasetId"]).iterate_items():print(job["title"], job["employerName"], job["salaryText"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/cwjobs-scraper').call({ mode: 'search', keywords: ['javascript'], locations: ['london'] });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Resume vs. incremental mode
These are two different things:
-
resumeFromRunIdcontinues 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. -
incrementalModeis for recurring monitoring of the same search (for example, a daily schedule). The actor remembers the previous run of the same search itself, in a stored baseline, and classifies every job against it:NEW- not seen before.UPDATED- a real field changed (seechangedFields); the description, salary, location, dates or work type genuinely differ from last time.UNCHANGED- nothing real changed. Suppressed by default (not returned, not billed) unless you turn onemitUnchanged.REAPPEARED- was markedEXPIREDin a previous run, now seen again.EXPIRED- was tracked before, not found this run. Only produced whenemitExpiredis on and the run proved a complete scan of the tracked search (not capped by Max jobs or 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 did not get to.REAPPEAREDcan only fire for a job that was tombstoned this way in an earlier run.
State is keyed on the mode, keywords, locations, filters and the detail-fetching setting, everything that changes what is searched. Max jobs / Max listing pages are deliberately excluded, so raising a limit does not start a fresh (re-billed) baseline. Set
stateKeyto name the campaign explicitly, or to deliberately share state across differently-configured runs.Cap semantics differ in incremental mode. Outside incremental mode, Max jobs caps jobs returned. In incremental mode it caps jobs scanned instead, because an emitted-count 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.
A handful of fields never affect change detection even though they stay in the output:
scrapedAt(stamped fresh every run),postedAgo(a relative-age string that changes by the clock alone), the ad-tier flagsisTopJob/isFreeListing, andworkType(taken from the listing URL that found the job, so it can differ between runs without the job changing). Fields added in a later version of this actor (benefits) are never treated as a change on jobs tracked before they existed. Everything else, including the full employer profile, is real data and does triggerUPDATED.
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.
- Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto 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.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search builds listing pages from your keywords and locations; url takes CWJobs listing or job URLs you already have. |
keywords | array | (none) | Search mode. Job keywords or titles, one per line. Leave empty to list every job in the chosen locations. Each keyword is combined with each location. |
locations | array | (none) | Search mode. Locations, one per line (e.g. "london", "manchester", "remote"). Leave empty to search the whole UK. Each location is combined with each keyword. |
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. Leave empty for no salary filter. |
salaryPeriod | string | annual | The period the minimum salary applies to: annual, daily, hourly. |
postedWithin | string | any | Only jobs posted within this many days: any, 1, 3, 7 or 14. |
sortBy | string | relevance | Result ordering: relevance (site default) or date (most recent first). |
workFromHome | boolean | false | Search mode. Keep only jobs the site lists with work-from-home options (the site's own filter). In URL mode add wfh=1 to the URL yourself. |
urls | array | one example of each supported listing shape | URL mode. CWJobs listing URLs or direct job URLs. Filter fields above are ignored in URL mode; pagination starts at the page in the URL and walks forward. |
fetchDetails | boolean | true | Open each job page for the full description, GPS coordinates, posting and expiry dates, work type, and the rich employer profile. Turn off for a leaner run that returns only the fields visible on the listing page. |
descriptionFormat | string | html | Return the job description as HTML (default, as published), plain text, or Markdown. Output only; change detection is unaffected. |
descriptionMaxLength | integer | 0 | Truncate each description to this many characters after formatting. 0 keeps it all. |
compact | boolean | false | Drop the nested employer profile object and the listing snippet from each row. Billing is unchanged: each pushed row is still one result. |
maxPages | integer | 0 | Listing pages to walk per search or URL (about 25 jobs per page). 0 means unlimited: the run stops at Max jobs, 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 Max listing pages, 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 Incremental mode 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 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. |
ignoreFieldsForChanges | array | [] | Incremental mode only. Field names that never make a job UPDATED, on top of the built-in ones (scrapedAt, postedAgo, isTopJob, isFreeListing, workType). Dotted names reach nested fields. The fields stay in the output; changing this list does not re-bill your tracked jobs. |
proxy | object | Residential, GB | Connection settings. Residential (United Kingdom) is recommended and is the default. |
mcpConnectors | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
notionParentPageUrl | string | (none) | Notion connector only: page under which records are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
Output Example
Sample shape: values are illustrative placeholders, not from a live listing.
{"rowType": "job","listingUrl": "https://www.cwjobs.co.uk/jobs/javascript/in-london","jobId": "10000001","externalId": "00000000-0000-0000-0000-000000000000","jobUrl": "https://www.cwjobs.co.uk/job/sample-job-title/sample-employer-job10000001","title": "Sample Senior Software Engineer","description": "Full job description text appears here when detail fetching is on.","snippet": "Short preview text taken 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": "£55,000 - £65,000 per annum","salaryMin": 55000,"salaryMax": 65000,"salaryCurrency": "GBP","salaryPeriod": "annum","locationText": "Sample Town, Sample County, AA1, GB","locationLocality": "Sample Town","locationRegion": "Sample County","locationPostalCode": "AA1","locationCountry": "GB","latitude": 51.5,"longitude": -0.1,"employerName": "Sample Employer Ltd","employerId": 5551234,"employerLogoUrl": "https://www.cwjobs.co.uk/CompanyLogos/00000000000000000000000000000000.png","employerProfileUrl": "https://www.cwjobs.co.uk/jobs/sample-employer-ltd?cmpId=5551234","companyEmployees": "50 - 199","companyFounded": 2005,"companyIndustries": ["IT & Internet"],"companyJobsCount": 8,"employer": {"name": "Sample Employer Ltd","employees": "50 - 199","founded": 2005,"industries": ["IT & Internet"],"jobsCount": 8,"description": "Company profile description appears here.","videoUrl": "https://video.example.com/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 refuses datacenter connections outright (the site's edge answers "Access Denied"), so a residential proxy is required. The default setting (Apify Residential, United Kingdom) is the one that works and needs a paid Apify plan, because the free plan has no Apify proxy. The actor never switches your chosen proxy group on its own: the proxy group is your choice and your cost. If nothing can be loaded, the run stops with an explicit message instead of returning an empty dataset. When the site refuses the selected group, the message is "No results: the site refused the selected proxy group (access denied). Select Apify Proxy group Residential, country United Kingdom, in the Connection setting, then run again." When the proxy itself refuses the connection, it is "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid." When the requests fail for any other reason (for example a site outage), it is "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."
FAQ
How much does it cost?
Billing has three parts, all listed on the Pricing tab: a start charge per run (apify-actor-start), one apify-default-dataset-item charge for each job pushed to the dataset, and one detail-enrichment charge for each job whose full detail page was fetched and returned. Jobs suppressed by incremental mode (unchanged, or duplicates) are never charged, even if their detail page was opened to check for changes. With Fetch full job details off, only the start and per-job charges apply. Output options (compact, descriptionFormat, descriptionMaxLength) do not change billing. Use Max jobs to cap the cost of any run.
Is it legal to scrape CWJobs?
This actor collects only publicly available job listing data. You are responsible for how you use it: follow CWJobs' terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Job descriptions and employer profile text may be subject to third-party rights.
Can I get only new or changed jobs on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode for the same search. Each run then returns only new, updated and reappeared jobs; jobs with no real change are suppressed and not billed unless you turn on Emit unchanged.
What is the difference between Resume and Incremental mode?
Resume (resumeFromRunId) continues one specific interrupted run and returns only the jobs not already in that run's dataset. Incremental mode is for running the same search again and again (for example daily) and getting back only what changed each time; the actor remembers the previous run itself, no ID required. See "Resume vs. incremental mode" above.
Why did my run return an empty dataset instead of an error?
If every connection attempt to the site is refused across every search, or a search genuinely matches nothing, this run finishes with an empty dataset and a status message explaining why, rather than an error. The run does fail outright in two narrower cases instead: a resumeFromRunId that cannot be resolved, or turning on Incremental mode together with resumeFromRunId when a baseline for that search already exists. Widen your filters, check the run's status message and log, or review your connection settings, then run it again.
Why are snippet, postedAgo or isPremium empty on a job I pasted directly?
Those three fields come only from a listing search-results card. A job reached by pasting its own job page URL has no listing card behind it, so they always come back empty (null or false) even with detail fetching on. Paste the listing page instead, or use search mode, to get every field on that job.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
🔗 Want more jobs data?
Pair this actor with these related scrapers from the same team:
| 💼 IrishJobs.ie Scraper Scrape job listings from IrishJobs.ie by keyword, location, job type, salary, recency, or... | 💼 Jobsite UK Scraper Scrape Jobsite.co.uk job listings at scale by keyword, location, filters or URL. Extract... |
| 💼 CareerBuilder Scraper Scrape CareerBuilder.com job listings by search or URL. Returns title, company profile... | 💼 Workday Jobs Scraper Scrape job postings from any Workday-hosted careers site on Myworkdayjobs.com. Paste... |
| 💼 RemoteOK Jobs Scraper Scrape RemoteOK jobs by keyword or job URL. Extract title, company, location, remote... | 💼 SEEK Jobs Scraper Scrape SEEK.com.au and SEEK.co.nz jobs by keyword, location, or filters. Extract full... |
💬 Support
- 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- 🛠️ Need another site, extra fields or a private build? Email contact@abotapi.com.
- ⭐ Enjoying it? A quick review on the actor page helps other users find it.