Scrape the whole RemoteOK board into structured rows: job title, company, salary range, tech stack, location and a link to the original posting. Reads RemoteOK's public JSON feed - no browser, no proxy, no login - so a full run takes seconds. Filter by tech stack, category, region or minimum salary.
[2.2.1] - 2026-09-26 - the Console prefill was an empty intersection, so a buyer's first run failed
The Store form prefilled two filters at once, categories: ["dev"] and regions: ["United States"].
Apify applies prefills when the buyer clicks Start, so that pair, which nobody chose, was the input
of every untouched first run. Filters AND together and those two do not overlap, so the run returned
nothing, and with failOnZeroResults defaulting to true it returned nothing as a FAILED run.
Over the preceding 30 days the Store actor logged 20 runs: 5 SUCCEEDED, 15 FAILED.
Measured against the live actor, one filter at a time:
input
result
categories: ["dev"]
SUCCEEDED, 23 items
regions: ["United States"]
SUCCEEDED, 6 items
both, the Console prefill
FAILED, 0 items
no filters
SUCCEEDED, 99 items
The failing run's own numbers say why: filteredOut {"categories":53,"categories_uncategorised":23,"regions":23}.
Of 99 postings, 53 are a category other than dev, 23 carry no category at all (RemoteOK leaves about
a quarter of the board unlabelled and the filter rejects those, by design since 2026-09-10), and the
23 dev postings that remain are all outside the United States string.
Fix: the regions prefill is gone.categories: ["dev"] stays, because a prefill that
demonstrates a filter is worth having and that one returns rows on its own.
Two alternatives were rejected:
Default failOnZeroResults to false. That hides the symptom and costs the actor a real feature.
A zero-row run that reports success is indistinguishable from a broken actor, which is the whole
reason the flag defaults to true, and it was reporting this bug accurately. The internal pipeline
sets it false for this actor because zero really is a normal daily outcome there, but that is a
caller's choice, not a sane default for a pay-per-result Store listing.
Stop categories rejecting unlabelled postings. That is the fail-closed fix from 2026-09-10 and
reversing it would silently re-invert the filter. Unrelated to this bug: either prefill works on
its own.
Verified on the live Store actor: the old prefill pair still FAILED (run NyCJ09B49Vnz4zllm, 0
items), the new prefill SUCCEEDED (run RpUGkygOymq6IT8sj, 23 items).
Docs updated with the same numbers: the schema now says why regions has no prefill, the Store
README explains that filters AND together and what the form arrives with, and the root README's
claim that categories "leaks" was corrected, it has failed closed since 2026-09-10.
2026-09-10 — categories failed OPEN and inverted itself; regions advertised values that match nothing
Found by running every filter at a generic, buyer-shaped value against the live board.
🔴 categories returned exactly the rows it could not classify
if(categories.length >0&& category){...}// before
The && category guard meant a posting with no detected category bypassed the filter
entirely. Combined with a Store README that advertised ["devops", "backend"] — neither of which
is in KNOWN_CATEGORIES — the filter inverted:
input
result
categories: ["devops","backend"] (the README's own example)
A buyer asking for devops jobs was handed the postings the actor knew nothing about, under a status
message reading "That is every posting on the board matching your filters".
Now fails closed. A row with no category cannot satisfy a category filter, so it is rejected and
reported as its own reason, categories_uncategorised — so an empty run distinguishes "your value
was wrong" from "the board is unlabelled". 14 of 99 postings carry no category.
And the documented values were wrong everywhere. The only labels that exist are dev, design,
sales, marketing, support, admin. Measured live: dev 29, marketing 27, design 27,
sales 2. Corrected in INPUT_SCHEMA.json, the README field table, the input sample, and a new
table of real counts.
🔴 regions was documented as a taxonomy; it is a substring match
The schema offered "e.g., USA, Europe, Asia" and the README "e.g. `["Europe"]". Measured on the
same 99-posting board:
value
postings
Remote
34
United States
8
US
7
USA (the schema's example)
1
Europe (the README's example)
0 — the run FAILS
Asia · Germany · Anywhere · Worldwide
0
Behaviour unchanged — it matches against the employer's free-form location text, which is all it can
do. The documentation now says so, with real counts and a working prefill.
⚠️ An example value in a listing is a promise that it works. Both of this Actor's array filters
shipped with examples that returned 1 row and 0 rows from a 99-row board.
Not a bug, recorded so nobody re-files it: 8 of 99 postings carry mojibake in location
("Heart\u00e2\u0080\u0099s Content", garbled Arabic). Checked against https://remoteok.com/api
directly — the upstream API serves it already mangled, identically. The Actor passes through what
it is given.
Unreleased
2026-09-08 — a short result set now says whose limit it was
Measured against the live Store listing: asking for the advertised maximum of 100 returned
100 (100%), and the count reproduced exactly across separate runs — so this is the board's real
inventory, not a flaky scrape. The status message said only Returned 100 RemoteOK postings., which a
buyer who asked for 100 cannot tell apart from a broken Actor.
finishRun() now receives requested: maxJobs and appends the reason:
Returned 100 RemoteOK postings. That is every posting on the board matching your
filters — asking for100 will not return more today.
RUN_STATS also carries requested and boardExhausted. Logic and tests live in
apify-actor-kit (inventory.ts); src/kit/ here is generated.
[2.1.0] - 2026-08-27
Added — a zero-row run now says why, and fails
Adopts the shared src/kit/, generated from apify-actor-kit — do not edit in place; check-sync.sh fails a build on any hand-edit.
This is the first consumer.
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 RemoteOK format change. Now:
outcome
run
Filters removed everything
FAILS — Read 99 postings but every one was removed by your filters (techStackFilter=99). Loosen the filters and re-run.
API returned records, none usable
FAILS — a RemoteOK format change, reported as one
Feed genuinely empty
FAILS — and says how to tell the two apart
failOnZeroResults (default true) lets a scheduled user opt back into
quiet green runs.
RUN_STATS now carries a per-filter breakdown (filteredOut), and is
written before the failure — a failed run is exactly when someone reads it.
Verified locally against the live feed, no platform spend:
99 fetched → 99 parsed → filteredOut {techStackFilter: 98} → 1 pushed, exit 0;
and with a filter matching nothing, exit 1 with
RUN_STATS.zeroRowReason = filtered_out
on disk.
Fixed — maxJobs advertised a ceiling it cannot reach
The input schema offered up to 1,000. RemoteOK's public feed is a single
un-paginated page of ~100 live postings, so anything above that was unreachable
by construction. Maximum corrected to 100 and the description now 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.
Unreleased — 2026-08-13
Documentation only. No behavior changes; build 0.0.5 is unmodified.
Added
Full documentation suite: docs/ARCHITECTURE.md, docs/DEVELOPMENT.md, docs/REMOTEOK-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 an explicit known-limitations table.
Verified against live RemoteOK
Two platform runs on 2026-08-13:
Input
Rows
Cost
Duration
maxJobs: 10
10
$0.0042
34 s
maxJobs: 100
49
$0.0025
17 s
~$0.05 per 1,000 rows — the cheapest actor in the portfolio. 49 rows is source exhaustion: the
RemoteOK homepage is the only page read and it carries ~49 listings.
Findings recorded (not fixed)
A zero-row run exits SUCCEEDED — total scrape failure is indistinguishable from success.
maxJobs advertises a maximum of 1,000; ~49 is achievable.
description is not a description — it is the row's embedded JSON-LD blob captured via
textContent and truncated at 1,000 chars, mid-string.
verified (hard-coded false) and currency (hard-coded "USD") are fabricated.
postedAt, companyUrl, applyUrl are declared but never populated.
skills duplicates techStack.
The jobId fallback embeds Date.now(), so it differs every run.
Salary regex caps at three digits — $1500 would parse as $150,000.
The categories filter is bypassed for jobs with no detected category.
All are itemized and prioritized in docs/IMPROVEMENT-PLAN.md.
0.0.5 and earlier
No changelog was kept. Build 0.0.5 is the deployed build as of 2026-08-13; the last source commit
was 2025-11-16.