Scrape WeWorkRemotely job listings with the FULL job description, not a listing-page snippet: company, salary, region, job type, category and detected tech stack. Pure HTTP - no browser, no proxy, no login. Filter by category, tech stack or minimum salary, and export to JSON, CSV or Excel.
Adopts the shared src/kit/, generated from apify-actor-kit
— do not edit in place; check-sync.sh fails on any hand-edit.
Until now a run that returned nothing printed ✓ Scraping complete! and exited
SUCCEEDED. A buyer paying per result could not tell a too-narrow filter from a
broken actor from a change in WeWorkRemotely's RSS. Each is now a distinct, named,
failing outcome carrying a per-filter breakdown, with failOnZeroResults
(default true) to opt back into quiet green runs.
Fixed — one rate-limited feed no longer discards every feed already fetched
fetchFeed() threw on a non-OK response and the feed loop had no catch, so a
single HTTP 403 lost every row from every feed already read. WeWorkRemotely
does rate-limit rapid sequential requests: measured 2026-08-27, requesting all nine
category feeds back to back returned 403 on two of them.
Feeds are now fetched with a 1 s politeness gap, failures are counted rather than
fatal, and blocked reaches the diagnosis — so "every feed was refused" is a
different, actionable answer from "no postings found".
Measured effect on that exact run: 0 rows → 245 rows, with
feedsBlocked: 2 recorded in RUN_STATS.
Fixed — maxJobs advertised a ceiling it cannot reach
The schema offered up to 1,000. WWR's feeds are single un-paginated pages:
measured 2026-08-27, the default all-jobs feed carried 89 postings and all nine
category feeds together ~250. Maximum corrected to 300, description states
the real limit. Selling a cap that cannot be delivered is a refund request.
Changed
One batched Actor.pushData() per run instead of one call per row.
Note: the 2.0.0 rewrite (browser → RSS, 2026-08-14) predates this changelog's
coverage and is recorded in the TQM vault's Scraper Commercialisation Plan §2.
Unreleased — 2026-08-13
Documentation only. No behavior changes; build 0.0.4 is unmodified.
Verified — the HTTP hypothesis is CONFIRMED
IMPROVEMENT-PLAN.md item 4 asked whether detail pages render without JavaScript. They do.
Plain curl — no browser, no proxy — returns every field main.ts extracts, on the listing page and
on 5 of 5 detail pages: title, company, the full 648-word description, company profile URL,
region, job type, and 51 job links from section.jobs.
CheerioCrawler can therefore replace PuppeteerCrawler with no data loss, moving this actor from
$4.19 to HTTP-class cost ($0.05–$0.26 per 1,000) — −323% margin to ~95%. This was the gate on
the actor's entire commercial case; it is now open. The migration itself is not yet done.
Added
Full documentation suite: docs/ARCHITECTURE.md, docs/DEVELOPMENT.md, docs/WWR-DOM.md,
docs/IMPROVEMENT-PLAN.md, and CLAUDE.md.
.actor/dataset_schema.json — the output surface was previously undeclared.
README rewritten as a buyer-facing listing with a known-limitations table.
Verified against live WeWorkRemotely
Platform run on 2026-08-13: 8 rows, $0.0335, 241 s — the run hit an imposed timeout rather than
finishing naturally. Roughly 30 s and $0.004 per job.
This is the highest-quality actor of the job-board set. Unlike its siblings it extracts real
company names, real full job descriptions and real company profile URLs. Two reasons:
It uses WeWorkRemotely's semantic BEM class names rather than Tailwind utility classes
(as in the YC actor) or guessed [class*="…"] substrings (as in the Wellfound actor).
It validates before emitting — three gates on title, company and description length — so pages
that fail to parse are skipped rather than emitted as hollow rows. No other actor in the portfolio
does this.
Findings recorded (not fixed)
jobId is the company slug and collides. It is derived by splitting the URL's final segment on
the first hyphen, which yields the company, not the job. Measured: 8 rows, 6 unique jobIds —
three separate Toptal postings all reported jobId: "toptal". Consumers must key on jobUrl.
maxJobs does not bound the run. The listing loop checks processedCount >= maxJobs before
queueing, but processedCount only increments in the detail handler, which hasn't run — so it is
0 throughout and every discovered URL is queued. Identical to the sibling Wellfound bug; masked
here because the run hit a timeout first. At ~30 s per job this is a real cost exposure.
A zero-row run exits SUCCEEDED. Worse, because failed rows are silently skipped, a broken
selector shows up as a low row count rather than an error.
category and postedAt read from the same sidebar list without qualifying which item — observed
category values mix real categories ("All Other Remote") with employment types ("Full-Time").
jobType is the reliable field.
postedAt is relative prose ("9 hours ago"), not a timestamp.
currency is hard-coded "USD"; the salary regex runs over the description, so it can match an
unrelated dollar figure; max is read by computed array index.
description is hard-truncated at 2,000 characters — most rows hit the cap exactly.
Validation skips are logged but never counted or surfaced.
skills duplicates techStack.
All are itemized and prioritized in docs/IMPROVEMENT-PLAN.md, which flags one test to run early:
the detail pages appear server-rendered, so CheerioCrawler may be able to replace Puppeteer
entirely — potentially a ~30× cost reduction with no data loss.
0.0.4 and earlier
No changelog was kept. Build 0.0.4 is the deployed build as of 2026-08-13; the last source commit
was 2025-11-16.
2026-09-04 — companyUrl was advertised with an example and never populated
Found by a pre-launch audit comparing every listing claim against the code across the portfolio.
The dataset schema described companyUrl as "WeWorkRemotely company profile URL" and gave the
example https://weworkremotely.com/company/collibra. It is declared on the row interface
(src/main.ts:50) and never assigned anywhere — the field is always absent.
This actor reads WWR's RSS feeds, which carry the full job description but no company profile link.
The description now says so, and the example is null.
The sibling remoteok-scraper has the identical field and already documented it correctly
("Not currently populated — always absent. Declared for forward compatibility."). The correct
wording existed twenty lines away in a neighbouring repo; only this one was missed. A field that
looks populated is never audited — that is what makes this class of defect survive.
Fourth instance of the pattern in the portfolio, after upwork's proposalsMin, hackernews's
title and indiehackers' tags.