Cwjobs UK $1π° URL Search | Keyword Filtering | Rich Output
Pricing
from $1.00 / 1,000 results
Cwjobs UK $1π° URL Search | Keyword Filtering | Rich Output
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.
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
7 days 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 |
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 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}
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), and the ad-tier flagsisTopJob/isFreeListing. 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). |
urls | array | (none) | 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. |
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. |
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
The default proxy setting (Apify Residential, United Kingdom) works out of the box and is recommended for this site. A datacenter connection also works, but a share of datacenter addresses are refused, so the actor automatically retries on a fresh connection when that happens; expect a few more retries on datacenter than on residential. Pick your proxy group under Connection.
FAQ
How much does it cost?
You pay per job returned, plus an additional detail-enrichment charge only for jobs whose full detail page was actually fetched and returned; a job suppressed by incremental mode is never charged for that extra fetch even if its detail page was opened to check for changes. The Pricing tab shows the current rates. 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:
| πΌ Jobsite UK From $1/1K. Collect UK job listings from Jobsite.co.uk at scale. Search by keyword... | πΌ IrishJobs.ie Scraper Scrape job listings from IrishJobs.ie by keyword, location, job type, salary, recency, or... |
| π¨ Caterer.com Scraper Scrape UK hospitality jobs from Caterer.com, including chef, hotel, restaurant, bar, and... | πΌ Workday Jobs Scraper Scrape job postings from any Workday-hosted careers site on Myworkdayjobs.com. Paste... |
| πΌ Dice.com Scraper Scrape tech job listings from Dice.com by keyword, filters, or URL. Extract job titles... | πΌ RemoteOK Jobs Scraper Scrape RemoteOK jobs by keyword or job URL. Extract title, company, location, remote... |
π Browse all abotapi scrapers
π¬ Support & custom scrapers
- π 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 abotapi@proton.me or message Telegram @abotapi.
- β Enjoying it? A quick review on the actor page helps other users find it.