# Remote Jobs Scraper - Remote OK, We Work Remotely & 5 More (`cedarfetch/remote-jobs-scraper`) Actor

Remote job postings from the public APIs and RSS feeds of Remote OK, We Work Remotely, Remotive, Jobicy, 4dayweek.io, Remote First Jobs and Arbeitnow. One schema, cross-posts merged, salary and location restrictions parsed, source attribution on every row, and new / removed jobs between runs.

- **URL**: https://apify.com/cedarfetch/remote-jobs-scraper.md
- **Developed by:** [Cedarfetch Labs](https://apify.com/cedarfetch) (community)
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Remote Jobs Scraper - Remote OK, We Work Remotely & 5 More

Collect remote job postings from seven remote-job boards in one run and get them back as one clean, deduplicated dataset: Remote OK, We Work Remotely, Remotive, Jobicy, 4dayweek.io, Remote First Jobs and Arbeitnow. Every board is read through its **official public API or RSS feed**, and every board is normalised to **one schema**: title, company, category, employment type, where you can work from (parsed into countries and regions), salary where published (with a USD-per-year estimate), description as text, HTML or Markdown, dates and links.

The same job is often posted on several boards. The Actor merges those copies into one row and lists the other boards in `alsoOn`. Turn on **change tracking** and schedule it daily: each row then says whether the job is `new`, `updated`, `unchanged` or `removed` since the last run, and "Output only changes" returns just the new and removed postings.

No browser, no proxies, no login. A run over every board takes about five minutes (4dayweek.io alone is ~100 polite requests; leave it out or set `maxJobsPerSource` for a run under a minute); a run over two boards takes a few seconds.

### Who it is for

- **Job-board and newsletter builders**: a daily feed of new remote jobs with categories, location restrictions and salaries already parsed, and the attribution each board asks for on every row.
- **Recruiters and sourcers**: who is hiring remotely for which roles, and which roles closed.
- **Job seekers automating alerts**: "new remote data jobs open to Europe, at least $80k" every morning.
- **Analysts**: remote salary ranges, category mix and regional openness across boards.
- **AI agents and pipelines**: one stable JSON shape for seven boards, callable through the Apify API.

### Sources

| Board | What it provides | Coverage per run | Requests per run | Attribution and reuse terms (read 2026-11-11) | Polling limit |
|---|---|---|---|---|---|
| [Remote OK](https://remoteok.com) | Title, company, logo, tags, location text, USD salary range, HTML description | the ~100 most recent jobs | 1 | Link back to the job on Remote OK with a followed link (no `rel="nofollow"`) and mention Remote OK as the source. Do not use the Remote OK logo. | none published |
| [We Work Remotely](https://weworkremotely.com) | Title, company, company URL, logo, category, type, region and allowed-country list, skills, HTML description, expiry | every listed job (all 11 category feeds) | 11 | Anyone can use the public RSS feeds; attribute the job links back to We Work Remotely. | none published (feed TTL 60 min) |
| [Remotive](https://remotive.com) | Title, company, logo, category, tags, job type, location text, salary text, HTML description | all public jobs, published with a 24-hour delay | 1 | Link back to the job on Remotive and mention Remotive as the source. Do not submit Remotive jobs to third-party job sites (Google Jobs, LinkedIn Jobs, Jooble, Neuvoo). Do not use the listings to collect sign-ups or e-mail addresses. | about 4 a day; enforced: 1 call per account per 6 hours |
| [Jobicy](https://jobicy.com) | Title, company, logo, industry, job type, seniority, location, salary with currency and period, HTML description | jobs from the last 7 days (3-hour delay) | 1 per 100 jobs | Commercial use and redistribution allowed. Credit Jobicy as the source, keep apply links on the Jobicy job page, do not present listings as your own. | no more than once an hour; enforced: 1 pass per account per hour |
| [4dayweek.io](https://4dayweek.io) | Title, company, website, logo, category, seniority, contract type, countries, salary, schedule (4-day week, async hours...), skills and stack, plain-text description, expiry | every live remote job (new listings can take 48 hours) | 1 per 100 jobs (about 100) | Free API designed for scripts, dashboards and aggregators; link back to 4dayweek.io and credit it in public products. | 60 requests a minute; enforced: 1 request per 1.1 s |
| [Remote First Jobs](https://remotefirstjobs.com) | Title, company, logo, category, seniority, countries, salary amounts, HTML description | the newest 500 jobs (24-hour delay) | 5 | Credit Remote First Jobs and link to each job. Do not submit the jobs to LinkedIn Jobs, Google Jobs, Jooble or similar platforms. | 5 pages of 100 |
| [Arbeitnow](https://www.arbeitnow.com) | Title, company, tags, job types, location, HTML description (only jobs Arbeitnow flags as remote) | remote-flagged jobs among the 2,400 newest postings (Europe, mostly Germany) | 8 | Free public API, do not abuse; provide a link back to Arbeitnow.com. | none published |

Every row carries its board's terms in one sentence (`sourceAttribution`), and the run summary keeps the notice each board returned live in that run (`sourceNotice`). **If you republish the jobs, follow the terms of the board each job came from**: link to `jobUrl`, name the board, and do not forward Remotive or Remote First Jobs listings to Google Jobs, LinkedIn Jobs or Jooble.

Polling limits are enforced per Apify account: if you run the Actor again within a board's window, that board's answer from your previous run is reused (`dataOrigin: "cache"`, noted in the summary) instead of calling the board again. All other boards are read live.

Not included, on purpose: **Himalayas** (its API terms forbid passing the jobs to third parties), **Working Nomads** (its terms forbid reselling or commercially exploiting the service, and the feed publishes no reuse terms), **The Muse** (the public API answers 404), **Real Work From Anywhere** and **NoDesk** (RSS feeds offered for personal reading, no reuse terms), and boards without a public unauthenticated feed or behind bot protection (Jobspresso answered 403, JustRemote and Remote.co offer no usable feed, LinkedIn and Indeed).

### Input

Everything is optional. With no input you get every listed remote job from all seven boards, deduplicated, newest first, with a plain-text description.

#### Example: a daily "new remote jobs" feed (changes only)

```json
{
    "trackChanges": true,
    "changesOnly": true,
    "changeTypes": ["new"],
    "descriptionFormat": "markdown"
}
```

#### Example: engineering jobs open to people in Europe, paying at least $90k

```json
{
    "categories": ["engineering", "devops", "data"],
    "locationsAllowed": ["europe"],
    "minSalaryUsd": 90000,
    "postedAfter": "14 days",
    "includeDescription": false
}
```

#### Example: two boards, keyword search in titles and descriptions

```json
{
    "sources": ["weworkremotely", "remotive"],
    "titleKeywords": ["react", "typescript"],
    "searchDescription": true,
    "excludeTitleKeywords": ["intern"],
    "maxJobs": 100
}
```

#### All options

| Field | Default | What it does |
|---|---|---|
| `sources` | all seven | Boards to read: `remoteok`, `weworkremotely`, `remotive`, `jobicy`, `fourdayweek`, `remotefirstjobs`, `arbeitnow`. |
| `maxJobs` | 0 (all) | Stop after this many delivered jobs (newest first, after filters and dedupe). |
| `maxJobsPerSource` | 0 (all) | Stop reading a board after this many matching jobs (saves requests; such a board reports no removals in that run). |
| `titleKeywords` | none | Keep titles containing any (or all) of these words. Alias: `keywords`. |
| `titleKeywordsMode` | `any` | `any` or `all`. |
| `excludeTitleKeywords` | none | Drop titles containing any of these words. Alias: `excludeKeywords`. |
| `searchDescription` | false | Apply both keyword lists to the description too. |
| `categories` | none | `engineering`, `design`, `product`, `marketing`, `sales`, `support`, `data`, `devops`, `finance`, `hr`, `writing`, `other`. |
| `locationsAllowed` | none | Where you can work from: `worldwide`, a region, or a country (see below). |
| `includeUnknownLocations` | false | With a location filter, also keep jobs whose restriction could not be read. |
| `employmentTypes` | none | `full_time`, `part_time`, `contract`, `temporary`, `internship`, `apprenticeship`, `volunteer`, `other`. |
| `minSalaryUsd` | 0 (off) | Minimum estimated USD per year (top of the published range). |
| `includeJobsWithoutSalary` | false | With a minimum salary, also keep jobs that publish no pay. |
| `postedAfter` | none | ISO date (`2026-11-01`) or relative (`24 hours`, `7 days`, `2 weeks`). |
| `dedupe` | true | Merge cross-posted copies into one row (`alsoOn`). |
| `includeDescription` | true | Include the description. |
| `descriptionFormat` | `text` | `text`, `html`, `markdown` or `all`. |
| `trackChanges` | false | Label rows new / updated / unchanged / removed against the previous run with the same boards and filters. |
| `changesOnly` | false | With tracking on, deliver only the change types in `changeTypes`. |
| `changeTypes` | `new`, `updated`, `removed` | Which changes `changesOnly` delivers. |
| `snapshotStoreName` | `remote-jobs-scraper-snapshots` | Named key-value store for snapshots; use different names for independent feeds. |
| `requestTimeoutSecs` | 60 | Timeout per request. |
| `maxRetries` | 4 | Retries for 429, 5xx and network errors (jittered backoff, Retry-After honoured). |

**Keywords** are case- and accent-insensitive and match whole words: `engineer` matches "Senior Engineer" but not "Engineering"; `engineer*` matches both; phrases such as `data scientist` work.

### Output

One row per job in the default dataset, newest first. Download as JSON, CSV, Excel or XML, or read it through the API. Views: **Jobs**, **Changes**, **Job details**, **Boards that could not be read**.

Sample row (shortened):

```json
{
    "recordType": "job",
    "source": "jobicy",
    "sourceJobId": "161259",
    "jobId": "jobicy:161259",
    "alsoOn": [
        {
            "source": "remotefirstjobs",
            "sourceJobId": "associate-general-counsel-880884",
            "jobUrl": "https://remotefirstjobs.com/companies/pair-team/jobs/associate-general-counsel-880884",
            "postedAt": "2026-11-08T18:33:31.000Z",
            "sourceAttribution": "Job from Remote First Jobs (https://remotefirstjobs.com). Remote First Jobs API terms: credit Remote First Jobs and link to this jobUrl; do not submit the job to third-party sites such as LinkedIn Jobs, Google Jobs or Jooble."
        }
    ],
    "title": "Associate General Counsel",
    "companyName": "Pair Team",
    "companyUrl": null,
    "companyLogo": "https://jobicy.com/data/server-nyc0409/galaxy/mercury/2025/06/105c0d0c-221.png",
    "category": "finance",
    "categoryRaw": "Legal & Compliance",
    "tags": [],
    "seniority": "Director",
    "employmentType": "full_time",
    "employmentTypeRaw": "Full-Time",
    "isRemote": true,
    "workplaceType": "remote",
    "location": "USA",
    "locations": [
        "USA"
    ],
    "regions": [
        "north_america",
        "americas"
    ],
    "countries": [
        "US"
    ],
    "country": "US",
    "isWorldwide": false,
    "compensation": {
        "min": 215000,
        "max": 240000,
        "currency": "USD",
        "interval": "year",
        "text": null
    },
    "salaryUsdAnnualMin": 215000,
    "salaryUsdAnnualMax": 240000,
    "descriptionText": "About Pair Team\n\nPair Team is a public benefit company reimagining care for the safety net. As an AI-enabled medical group for Medicaid and Medicare, we deliver...",
    "descriptionHtml": null,
    "descriptionMarkdown": null,
    "postedAt": "2026-11-11T10:53:05.000Z",
    "expiresAt": null,
    "jobUrl": "https://jobicy.com/jobs/161259-associate-general-counsel-3",
    "applyUrl": "https://jobicy.com/jobs/161259-associate-general-counsel-3",
    "sourceAttribution": "Job from Jobicy (https://jobicy.com). Jobicy API terms: credit Jobicy as the source, keep apply links pointing to the Jobicy job page (this jobUrl), do not present the listing as your own.",
    "changeType": null,
    "changedFields": null,
    "previous": null,
    "firstSeenAt": null,
    "removedAt": null,
    "removedReason": null,
    "dataOrigin": "api",
    "scrapedAt": "2026-11-11T21:36:19.003Z"
}
```

#### Fields

| Field | Meaning |
|---|---|
| `source`, `sourceJobId`, `jobId` | Board, the board's id, and `"<source>:<sourceJobId>"`. |
| `alsoOn` | Other boards listing the same job: `source`, `sourceJobId`, `jobUrl`, `postedAt`, `sourceAttribution`. |
| `title`, `companyName`, `companyUrl`, `companyLogo` | As published (company URL from We Work Remotely and 4dayweek.io). |
| `category`, `categoryRaw` | Shared category, and the board's own label. |
| `tags`, `seniority` | Board tags or skills; seniority label where published. |
| `employmentType`, `employmentTypeRaw` | Normalised type and the board's label(s). |
| `isRemote`, `workplaceType` | Always `true` / `"remote"` (every source is a remote board or is filtered to remote jobs). |
| `location`, `locations` | The restriction text as published. |
| `regions`, `countries`, `country`, `isWorldwide` | Parsed restriction (see below). |
| `compensation` | `{ min, max, currency, interval, text }` as published. |
| `salaryUsdAnnualMin`, `salaryUsdAnnualMax` | Estimated USD per year. |
| `descriptionText`, `descriptionHtml`, `descriptionMarkdown` | Per `descriptionFormat`; the others are null. |
| `postedAt`, `expiresAt` | ISO 8601. |
| `jobUrl`, `applyUrl` | The job page on the board (all seven boards route applications through their own job page). |
| `sourceAttribution` | The board's attribution and reuse terms. |
| `changeType`, `changedFields`, `previous`, `firstSeenAt`, `removedAt`, `removedReason` | Change tracking. |
| `dataOrigin` | `api` (read in this run), `cache` (reused to respect a polling limit), `snapshot` (removed row). |
| `scrapedAt` | When the row was produced. |

Field names are the same as in the **ATS Jobs Scraper** (Greenhouse, Lever, Ashby...) wherever the meaning is the same (`companyName`, `title`, `location`, `locations`, `country`, `isRemote`, `workplaceType`, `employmentType`, `employmentTypeRaw`, `compensation`, `descriptionText`, `descriptionHtml`, `postedAt`, `jobUrl`, `applyUrl`, `jobId` and the change fields), so the two datasets can be stacked.

The run summary (`OUTPUT` in the default key-value store) lists for every board: status (`ok`, `partial`, `no_jobs`, `blocked`, `rate_limited`, `not_found`, `unreachable`, `error`), jobs listed, matching the filters, left after dedupe, delivered, removed, requests, seconds, whether the cached answer was used, and the board's live terms notice.

### How location restrictions are read

Boards write "where you may work from" as free text: "Worldwide", "USA Only", "Europe, USA, Canada", "EMEA", a flag-prefixed list of countries, "Remote - US". The Actor parses it like this:

- `countries`: ISO codes of every country named, including common aliases (USA, UK, UAE, "United Kingdom of Great Britain and Northern Ireland", "Korea (Republic of)", "Türkiye"). US state and Canadian province names count as US / CA. Bare two-letter codes other than US, UK and EU are not read, because "IN", "CA" and "DE" are also US state codes.
- `regions`: regions named in the text (`north_america`, `latin_america`, `americas`, `europe`, `eu`, `middle_east`, `africa`, `emea`, `asia`, `apac`, `oceania`) plus the regions of the named countries.
- `isWorldwide`: `true` for Worldwide / Anywhere / Global; `false` when a country or region was recognised; `null` when nothing was (empty, "Remote", or only a city such as "Berlin").
- We Work Remotely: the allowed-country list wins over the region label when both are given; the `state` field is ignored (it appears next to "Anywhere in the World").
- Remote OK and Arbeitnow often give an office city or nothing; those jobs have `isWorldwide: null`.

`locationsAllowed` keeps a job when it is open worldwide, or when its countries or regions overlap the places you list (a German resident matches "EMEA" and "Europe" jobs; a "Germany, Europe" job matches a French resident). Jobs with an unknown restriction are dropped unless `includeUnknownLocations` is on. `["worldwide"]` alone keeps only jobs open worldwide.

### Categories

Each board's category is mapped to one shared list: engineering, design, product (product and project management), marketing, sales, support (customer support and success), data (data, analytics, ML/AI), devops (DevOps, SRE, sysadmin, IT), finance (finance, accounting, legal), hr, writing, other. Vague board labels ("All others", "Sales and Marketing") and boards without categories (Remote OK, Arbeitnow) are classified from the job title. The board's label stays in `categoryRaw`.

### Salary

Salary is only filled when the board publishes one: structured ranges (Remote OK in USD per year, Jobicy with currency and period, 4dayweek.io, Remote First Jobs) or free text (Remotive: "$50-$75 /hour", "OTE $25k - $35k", "$3.5k–$5k/month"). When no period is written, "year" is assumed only for amounts of 10,000 or more. `salaryUsdAnnualMin/Max` convert with fixed ECB reference rates of 2026-11-11 and hour x 2080, day x 260, week x 52, month x 12; they stay null when the currency or period is unknown (Remote First Jobs publishes amounts without a currency, so only US-only jobs get USD). Placeholder values (0, 110, 114,000,000) are dropped.

### Deduplication

Two records are merged only when all of these hold: the same company (ignoring case, punctuation and legal suffixes such as Inc, LLC, GmbH), the same title (ignoring punctuation, "(m/f/d)"-style markers and "(Remote)" markers, but not seniority words or region suffixes such as "- UK"), different boards, no board listing two jobs under that company and title, location restrictions that do not contradict, posting dates within 30 days, and matching external apply hosts when both have one. The richest copy is kept (description, salary, logo, company URL, parsed location, employment type), and the others are listed in `alsoOn`. On the boards' live data on 2026-11-11, a full run read 13,192 postings and merged 174 cross-posted copies (1.3%) into 13,018 jobs; most merges paired 4dayweek.io or Jobicy with We Work Remotely or Remote First Jobs. Keys that one board uses for several openings (for example one posting per country) were left unmerged.

### Change tracking and the daily feed recipe

1. Run once with `trackChanges: true` and the boards and filters you want. Every row is `new`; a snapshot is saved in a named key-value store in your account.
2. Schedule the same input daily (Apify Console, Schedules) with `changesOnly: true`. Each run returns only `new`, `updated` and `removed` jobs. Use `changeTypes: ["new"]` for a pure "new jobs" feed.
3. Connect the dataset to your newsletter, Slack, Google Sheets or webhook through Apify integrations.

What the change types mean:

- `new`: not seen before in this scope (same boards and filters).
- `updated`: title, company, board category, employment type, location text, salary or description changed; `changedFields` and `previous` say what changed.
- `removed`: no longer listed. Boards drop jobs when they are filled or expire, and some feeds only expose a recent window: `removedReason` is `no_longer_listed` (We Work Remotely, Remotive, 4dayweek.io read in full) or `left_feed_window` (Remote OK's latest ~100, Jobicy's last 7 days, Remote First Jobs' newest 500 - the job may still be open elsewhere).
- A job is only reported removed when every board that listed it was read completely in this run without errors. A failed, capped or partial read never claims a job disappeared; it is reported on the next complete run. Arbeitnow is always read partially, so it never reports removals.
- A cross-posted job keeps its history when one board drops it and another still lists it.

### Use from an AI agent or via the API

Ready-to-run Python, Node.js and cURL examples: [github.com/cedarfetch/remote-jobs-scraper-examples](https://github.com/cedarfetch/remote-jobs-scraper-examples).

Call the Actor synchronously and get the jobs back in one HTTP request:

```bash
curl -X POST "https://api.apify.com/v2/acts/<username>~remote-jobs-scraper/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"sources":["remoteok","weworkremotely","remotive"],"categories":["data"],"locationsAllowed":["US"],"maxJobs":50,"includeDescription":false}'
```

The Actor is also available to MCP-capable assistants through the Apify MCP server. Small, filtered inputs (`maxJobs`, `includeDescription: false`) keep agent context short; `jobUrl` is the link to show users.

### Pricing

Pay per event: you are charged for each **job row** delivered (removed-job rows included). Board status rows are free. The run stops cleanly when it reaches the maximum total charge you set; everything delivered so far stays in the dataset. With "Output only changes", a daily run is charged only for the new, updated and removed jobs.

### Limits and honest caveats

- These are the boards' public feeds, not their full websites: Remote OK exposes its latest ~100 jobs, Jobicy the last 7 days, Remote First Jobs the newest 500, Arbeitnow is read up to 8 pages; Remotive and Remote First Jobs publish with a 24-hour delay, Jobicy with 3 hours, 4dayweek.io up to 48 hours.
- Remotive's public feed is small (13 jobs on 2026-11-11).
- Location text is free text; anything the parser cannot read stays in `location` with `isWorldwide: null`.
- Categories for Remote OK and Arbeitnow, and for vague board labels, come from the title; a few will be wrong.
- Salary USD estimates use fixed exchange rates and full-time hours; treat them as filters, not offers.
- Arbeitnow is a Europe-focused board, many of its postings are in German, and its location is often a city.
- If a board blocks or rate-limits requests, it is reported in the summary and as a free status row, and the other boards are still delivered. The run fails only when every selected board failed (with each board's reason).
- The Actor reads job postings only. It does not collect anything about applicants or other people.

### FAQ

**Why not LinkedIn, Indeed or Himalayas?** LinkedIn and Indeed have no public job feed and sit behind bot protection, which this Actor does not bypass. Himalayas has a public API, but its terms forbid passing its jobs to third parties.

**Why do some jobs show `dataOrigin: "cache"`?** Remotive asks API users to call at most about 4 times a day and Jobicy at most once an hour. Runs in your account within those windows reuse your previous answer for those two boards.

**Can I get the board's apply link to the employer?** All seven boards route applications through their own job page in their public feeds (Jobicy requires it). `applyUrl` is that page.

**How do I keep two independent daily feeds?** Use a different `snapshotStoreName` for each, or different boards/filters (each combination has its own snapshot).

**Does it work with the ATS Jobs Scraper?** Yes. Shared fields have the same names and shapes, so you can stack both datasets: company career pages from the ATS scraper, remote boards from this one.

**What does a failed board look like?** A free row with `recordType: "source_status"`, the board, its status (for example `blocked`) and a plain explanation, plus the same in `OUTPUT`.

### More Actors from Cedarfetch Labs

Built the same way: public data only, typed output, per-source error reporting, change tracking for scheduled runs.

- [Shopify Store Scraper](https://apify.com/cedarfetch/shopify-store-scraper): full product catalogs from any Shopify store, with price and stock change tracking.
- [ATS Jobs Scraper](https://apify.com/cedarfetch/ats-jobs-scraper): open jobs from Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio boards, with new and removed postings flagged.
- [Greenhouse Jobs Scraper](https://apify.com/cedarfetch/greenhouse-jobs-scraper): only need Greenhouse boards? The same engine with a simpler input.
- [Lever Jobs Scraper](https://apify.com/cedarfetch/lever-jobs-scraper): open postings from any Lever-hosted careers page, with new and removed jobs flagged.
- [Ashby Jobs Scraper](https://apify.com/cedarfetch/ashby-jobs-scraper): open jobs from any Ashby job board, with new and removed jobs flagged.
- [Substack Scraper](https://apify.com/cedarfetch/substack-scraper): public posts from any Substack newsletter as Markdown, with a new-posts feed.
- [Google Trending Searches](https://apify.com/cedarfetch/google-trending-searches): what is trending on Google right now, per country, with alerts on new trends.

# Changelog

This Actor's version history is a separate document: https://apify.com/cedarfetch/remote-jobs-scraper/changelog.md

# Actor input Schema

## `sources` (type: `array`):

Which boards to read. Default: all seven. Each board's terms (attribution, polling limits) are in the README and on every row as sourceAttribution.

## `maxJobs` (type: `integer`):

Stop after delivering this many jobs in total (newest first, after filters and dedupe). 0 means no limit. Removed-job rows from change tracking are not counted here.

## `maxJobsPerSource` (type: `integer`):

Stop reading a board after this many matching jobs. 0 means no limit. Saves requests on large boards (4dayweek.io has ~100 pages); a board stopped early never reports removed jobs in that run.

## `titleKeywords` (type: `array`):

Keep jobs whose title contains these words (case- and accent-insensitive, whole words). Add \* for a prefix: "engineer\*" also matches "Engineering". Phrases work: "data scientist". Turn on "Also search descriptions" to match the description too.

## `titleKeywordsMode` (type: `string`):

"any": at least one keyword. "all": every keyword.

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains any of these words (same matching rules as Keywords).

## `searchDescription` (type: `boolean`):

Apply Keywords and Exclude keywords to the title AND the description. Off: titles only. Careful with exclusions: a word like "senior" appears in many descriptions.

## `categories` (type: `array`):

Keep jobs in these categories. Every board's own categories are mapped to this one taxonomy (from the job title where the board has none); the board's label stays in categoryRaw.

## `locationsAllowed` (type: `array`):

Keep jobs open to people in these places. Use "worldwide" (only jobs open worldwide), regions (americas, north_america, latin_america, europe, eu, emea, middle_east, africa, asia, apac, oceania) or countries ("US", "Germany", "GB"). Jobs open worldwide always match a country or region. See the README for how each board's free-text restriction is read.

## `includeUnknownLocations` (type: `boolean`):

With a location filter, also keep jobs whose restriction could not be read (empty, "Remote", or only a city such as "Berlin"). Common on Remote OK and Arbeitnow.

## `employmentTypes` (type: `array`):

Keep only these normalised employment types. Jobs whose board publishes no type are dropped when this is set.

## `minSalaryUsd` (type: `integer`):

Keep jobs whose published pay reaches this amount, compared on the estimated USD-per-year top of the range (fixed exchange rates, hourly x 2080, monthly x 12). Only published salaries are compared; jobs without one are dropped unless the next option is on. 0 = off.

## `includeJobsWithoutSalary` (type: `boolean`):

With a minimum salary set, also keep jobs that publish no pay (or whose currency or period is unknown).

## `postedAfter` (type: `string`):

ISO date (2026-11-01) or a relative value ("24 hours", "7 days", "2 weeks"). Jobs without a posting date are dropped when this is set.

## `dedupe` (type: `boolean`):

The same job is often posted on several boards. On: one row per job (the richest copy) with the other boards' copies listed in alsoOn. Matching is conservative: same company and same title (ignoring punctuation, legal suffixes and "(Remote)"-style markers), different boards, compatible locations, posted within 30 days.

## `includeDescription` (type: `boolean`):

Add the job description in the format chosen below. Off: smaller rows.

## `descriptionFormat` (type: `string`):

text = descriptionText (plain text); html = descriptionHtml (as published); markdown = descriptionMarkdown (converted from the HTML); all = all three. 4dayweek.io only publishes plain text, so its html is empty.

## `trackChanges` (type: `boolean`):

Compare with the previous run that used the same boards and filters, and label every row new / updated / unchanged / removed. A compact snapshot is kept in a named key-value store in your account. "removed" means no longer listed (filled, expired, or left a board's recent-jobs window); it is only reported when every board that listed the job was read completely.

## `changesOnly` (type: `boolean`):

With change tracking on, deliver only the change types selected below (default: new, updated, removed). Unchanged jobs are not delivered or charged.

## `changeTypes` (type: `array`):

Which changes "Output only changes" delivers.

## `snapshotStoreName` (type: `string`):

Named key-value store that keeps the change-tracking snapshots. Use a different name to keep independent feeds apart.

## `requestTimeoutSecs` (type: `integer`):

Timeout for one request to a board.

## `maxRetries` (type: `integer`):

Retries for 429, 5xx and network errors, with jittered exponential backoff and Retry-After honoured. 401/403 and bot challenges are never retried.

## Actor input object example

```json
{
  "sources": [
    "remoteok",
    "jobicy"
  ],
  "maxJobs": 500,
  "maxJobsPerSource": 200,
  "titleKeywords": [
    "react",
    "product manager",
    "engineer*"
  ],
  "titleKeywordsMode": "any",
  "excludeTitleKeywords": [
    "intern",
    "unpaid",
    "commission only"
  ],
  "searchDescription": false,
  "locationsAllowed": [
    "US",
    "canada",
    "worldwide"
  ],
  "includeUnknownLocations": false,
  "minSalaryUsd": 80000,
  "includeJobsWithoutSalary": false,
  "postedAfter": "7 days",
  "dedupe": true,
  "includeDescription": true,
  "descriptionFormat": "text",
  "trackChanges": false,
  "changesOnly": false,
  "changeTypes": [
    "new",
    "updated",
    "removed"
  ],
  "snapshotStoreName": "remote-jobs-scraper-snapshots",
  "requestTimeoutSecs": 60,
  "maxRetries": 4
}
```

# Actor output Schema

## `jobs` (type: `string`):

All job rows (including removed rows with change tracking), plus free source_status rows for boards that could not be read.

## `changes` (type: `string`):

The same rows shown with the change-tracking columns.

## `summary` (type: `string`):

Per-source status (ok, partial, blocked, rate_limited, not_found, unreachable, error), jobs fetched, after filters, after dedupe, delivered, requests, duration, cache use, each board's live terms notice, and change counts.

# 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 = {
    "sources": [
        "remoteok",
        "jobicy"
    ],
    "maxJobs": 20,
    "maxJobsPerSource": 20,
    "titleKeywords": [
        "engineer*",
        "developer",
        "manager"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cedarfetch/remote-jobs-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 = {
    "sources": [
        "remoteok",
        "jobicy",
    ],
    "maxJobs": 20,
    "maxJobsPerSource": 20,
    "titleKeywords": [
        "engineer*",
        "developer",
        "manager",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("cedarfetch/remote-jobs-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 '{
  "sources": [
    "remoteok",
    "jobicy"
  ],
  "maxJobs": 20,
  "maxJobsPerSource": 20,
  "titleKeywords": [
    "engineer*",
    "developer",
    "manager"
  ]
}' |
apify call cedarfetch/remote-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cedarfetch/remote-jobs-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/5kPLYvoS6aTAGgiPe/builds/ruX9ZT0US1AunH6EA/openapi.json
