# Remote Job Aggregator — RemoteOK, WeWorkRemotely + 8 Boards (`flash_scraper/remote-job-aggregator`) Actor

Remote job aggregator sweeping 10 keyless boards in one run - RemoteOK, WeWorkRemotely, Working Nomads, Remotive, Jobicy, Himalayas, DevITjobs (US + UK), The Muse and HN Who is Hiring - deduplicated across boards, salaries annualized. The same role on two boards ships once.

- **URL**: https://apify.com/flash\_scraper/remote-job-aggregator.md
- **Developed by:** [Flash Scrape](https://apify.com/flash_scraper) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Remote Job Aggregator — RemoteOK, WeWorkRemotely + 8 Boards

A **remote job aggregator** that sweeps ten keyless remote job boards in one run — **RemoteOK**, **WeWorkRemotely**, **Working Nomads**, **DevITjobs** (US + UK, remote rows only), **The Muse** (remote filter), **Remotive**, **Jobicy**, **Himalayas** and the Hacker News **"Who is hiring?"** thread — and returns one clean, deduplicated dataset.

Every posting is normalized to the same schema (title, company, location, salary where the board publishes one, posting date, tags, description snippet, direct link), and **the same role listed on two boards ships once**: rows are merged across boards by canonical job URL *and* by `(title, company)`. That cross-board deduplication is what a single-board scraper can't give you — you pay per unique job, not per feed it happened to appear in.

Three things no other multi-board remote actor offers:

- **`onlyNewJobs` monitoring** — run it on a schedule and every run delivers (and bills) **only postings you haven't seen before**. A run where nothing is new delivers 0 rows, says so plainly, and costs nothing. See [Turn it into a job alert](#turn-it-into-a-job-alert-onlynewjobs).
- **Annualized salaries** — hourly/weekly/monthly figures are converted to yearly (×2080 / ×52 / ×12) so `salary_min`/`salary_max` are comparable across boards, with a sanity guard against absurd conversions.
- **Descriptions** — a 500-character `description_snippet` on every row that has one (88% of rows on the 2026-08-15 default run), and the full HTML-stripped text via `includeDescription`.

No API keys, no proxies, no login. All ten sources are public, keyless endpoints.

### What you get

One row per remote job:

```json
{
  "title": "Director, GTM Strategy & Analytics",
  "company": "Temporal Technologies",
  "location": "Anywhere in the World",
  "remote": true,
  "salary_min": 260000,
  "salary_max": 326000,
  "salary_currency": "USD",
  "salary_interval": "yearly",
  "salary_source": "description",
  "url": "https://weworkremotely.com/remote-jobs/temporal-technologies-director-gtm-strategy-analytics",
  "source_board": "weworkremotely",
  "posted_at": "2026-08-15",
  "tags": ["Product"],
  "description_snippet": "Headquarters: United States - Remote Opportunity About Us Temporal is an open source programming model that can simplify code…",
  "description": null,
  "scraped_at": "2026-08-15T20:15:17Z"
}
```

(A real row from a live test run on 2026-08-15. That run delivered 100 deduplicated jobs from 9 boards with 6 cross-board duplicates merged — **35% of rows carrying a salary, 88% a description snippet and 100% a posting date**, all measured on that run, not estimated.)

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `searchTerms` | array of strings | `[]` (everything) | Keywords matched against job **titles**, case-insensitive. A job matches if **any** term matches (OR). A multi-word term needs every word in the title — `"python developer"` requires both, so prefer short terms like `"python"` for recall. Empty returns every remote job the boards offer. Capped at **10 terms per run** — extras beyond the first 10 are ignored and the run's status message notes it. |
| `matchDescriptions` | boolean | `false` | Widen matching from titles to **tags + the first 500 characters of the description** (exactly what ships in `tags` and `description_snippet`, so you can always see why a row matched). Measured 2026-08-15 with `["python"]`: 28 rows title-only vs **194 rows** widened — ~7× the recall, at a precision cost: a marketing role whose blurb mentions "our Python stack" now matches too. |
| `boards` | multi-select | all ten | Which boards to sweep: `remoteok`, `weworkremotely`, `working_nomads`, `devitjobs_us`, `devitjobs_uk`, `muse`, `remotive`, `jobicy`, `himalayas`, `hn_hiring`. |
| `maxItems` | integer (1–1000) | `100` | Maximum number of deduplicated jobs to return. Results are interleaved round-robin across boards, so a small cap still samples every board instead of just the biggest feed. |
| `includeDescription` | boolean | `false` | `true` fills the `description` column with the job's **full HTML-stripped text** (avg ~4,300 chars measured). Off by default to keep rows light — the 500-char `description_snippet` is always on. |
| `onlyNewJobs` | boolean | `false` | Deliver **only postings this exact search has never delivered before**. See [Turn it into a job alert](#turn-it-into-a-job-alert-onlynewjobs). |

Running with completely empty input `{}` works and returns up to 100 jobs from all boards.

### Output columns

| Column | Notes |
|---|---|
| `title`, `company`, `location` | `location` is exactly what the board provides — often a country restriction like "USA Only" or "Anywhere in the World". |
| `remote` | Always `true` — non-remote rows (DevITjobs office jobs, onsite HN posts) are filtered out before delivery. |
| `salary_min`, `salary_max`, `salary_currency`, `salary_interval` | Only when the board publishes a figure — **35% of rows on the 2026-08-15 default run** (DevITjobs ~100%, Himalayas/Remotive/WWR/HN partial, The Muse and Working Nomads none). Hourly/weekly/monthly figures are **annualized** (×2080 / ×52 / ×12) so `salary_interval` normally reads `yearly` and figures are comparable across boards; a magnitude guard suppresses conversions that could only be wrong (an "hourly" $50,000 stays raw rather than shipping as $104M). `salary_source` says whether the figure was a structured field (`direct_data`) or parsed out of the posting text (`description`). Currency is left `null` when the board doesn't state it — no guessing. |
| `url` | Direct link to the posting (for Remote OK rows this is the job's Remote OK page, as their API terms require). |
| `source_board` | Which board this (first) copy of the job came from. |
| `posted_at` | Posting date, `YYYY-MM-DD`, when the board provides one. |
| `tags` | Skills/technologies/categories as the board tags them. |
| `description_snippet` | Always on: the first 500 characters of the HTML-stripped description — filled on **88% of rows** (measured; DevITjobs publishes no description text). |
| `description` | Full HTML-stripped description text when `includeDescription: true`, else `null`. |

### Run it from your code

**JavaScript (apify-client):**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('flash_scraper/remote-job-aggregator').call({
    searchTerms: ['python', 'data engineer'],
    maxItems: 200,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(`${items.length} remote jobs`, items[0]);
```

**Python (apify-client):**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("flash_scraper/remote-job-aggregator").call(run_input={
    "searchTerms": ["python", "data engineer"],
    "maxItems": 200,
})

items = client.dataset(run["defaultDatasetId"]).list_items().items
print(len(items), "remote jobs")
```

**curl (run synchronously and get the dataset back):**

```bash
curl -sX POST \
  "https://api.apify.com/v2/acts/flash_scraper~remote-job-aggregator/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"searchTerms": ["python"], "maxItems": 100}'
```

### Turn it into a job alert (onlyNewJobs)

Set `onlyNewJobs: true` and the actor becomes a monitor: every run delivers — and bills — **only postings this exact search (same `searchTerms` + `boards`) has never delivered before**.

How it behaves, run by run:

1. **First run = baseline.** Delivers everything currently matching and the status message says the baseline was saved.
2. **Every later run** delivers only new postings. If nothing is new, the run finishes SUCCEEDED with `Nothing new since your last run (<time> UTC): … You were not charged.` — zero rows, zero cost. That message is the monitor working, not an error.
3. Memory lives in a named key-value store **in your own Apify account** (`remote-job-monitor`), one record per search signature; entries are pruned after 90 days, so a job reposted after a long silence counts as new again.

Set up the alert in 2 minutes (Apify Console):

1. Open this actor → **Input**: set your `searchTerms`, tick **Only new jobs**, and set `maxItems` generously (e.g. 500–1000) — with `onlyNewJobs` on, the sweep reads each board to full feed depth so nothing hides behind the cap, and you still only pay for what's delivered.
2. Run it once manually — that's your baseline.
3. **Schedules → Create new → pick this actor** → cron like `0 8 * * *` (daily 08:00). The schedule reuses your saved input.
4. Add a notification: **Integrations** on the actor → webhook on `Run succeeded` to Slack/email/n8n — or read the dataset from the run; on a nothing-new day the dataset is simply empty and you paid nothing.

Two honest notes: if a baseline run hits `maxItems`, the overflow postings are still "never delivered" and will arrive on the next run(s) until the backlog drains — deliver-once semantics, never deliver-never. And changing `searchTerms` or `boards` starts a fresh signature with its own baseline.

### Schedule it (n8n, Make, Zapier)

A remote job aggregator earns its keep on a schedule — a daily sweep feeding a Slack channel, an Airtable base, or a job-alert newsletter:

- **n8n**: use the **Apify node** (or an HTTP Request node hitting the `run-sync-get-dataset-items` endpoint above) on a Cron trigger, then map the JSON rows to Slack/Notion/Google Sheets nodes.
- **Make (Integromat)**: the **Apify app** has a "Run an Actor" module — pair it with a scheduled scenario and an Iterator over the dataset items.
- **Zapier**: the **Apify integration** offers "Actor Run Finished" as a trigger — schedule runs natively in the Apify Console (Schedules → new schedule → pick this Actor) and let Zapier fan the fresh dataset out.
- **Apify Console alone** also works: Schedules can run this Actor daily and email you or push to a webhook when a run finishes.

Tip for monitoring pipelines: turn on `onlyNewJobs` (section above) so the scheduled runs deliver only the delta — no downstream dedup needed and quiet days cost nothing.

### Honest limits

- **Feed-limited depth.** These are public feeds, not archives: RemoteOK serves its ~100 newest rows, WeWorkRemotely ~100 per feed, Working Nomads ~50 curated rows, Jobicy 50 per call. A run samples the current face of each board; it cannot paginate years back.
- **Keyword matching is title-only by default** (deliberately — that's the billing gate; you're never charged for a fuzzy server-side match that isn't really your keyword). A Python job titled "Backend Engineer" won't match `"python"` unless you set `matchDescriptions: true`, which trades precision for ~7× recall (measured) — the term then only needs to appear in the tags or the first 500 chars of the description.
- **Salary coverage varies by board** — 35% of rows overall on the measured default run. DevITjobs publishes structured annual salaries on ~100% of its rows; Himalayas, Remotive, WWR and HN partially; RemoteOK ~3%; The Muse and Working Nomads none. Figures parsed out of free text (`salary_source: "description"`) are conservative best-effort, and annualization only multiplies figures whose interval the board or text actually states.
- **DevITjobs contributes remote rows only** (~8% of its inventory, measured) — it's a city-based board, and shipping its office jobs with `remote: true` would be a lie. Its `.uk` feed is also geo-sensitive and can be unavailable from some networks; failures are reported per board, never silently.
- **HN "Who is hiring?" rows are parsed from community-formatted posts** (`Company | Role | Location | Salary`). Parsing degrades gracefully, but expect the occasional imperfect company/title split. Only posts that explicitly say "remote" are included.
- **Boards change.** Undocumented internal APIs (DevITjobs in particular) can change shape without notice. A board that fails is named in the run's status message and log; its failure never poisons the other nine.
- **Attribution:** job rows sourced from **Remote OK** link back to the job's Remote OK page in the `url` column, as their API terms require.
- **Zero matches cost nothing.** A run that delivers no rows tells you why (boards empty vs. your filters) and does not charge you.

### Related actors

- [LinkedIn Jobs Scraper](https://apify.com/flash_scraper/linkedin-jobs-scraper) — keyword + location search over LinkedIn's public job listings.

> Need LinkedIn, Indeed and Glassdoor too - merged and deduplicated across 12 boards with only-new-jobs monitoring? -> [Multi Job Board Scraper](https://apify.com/flash_scraper/multi-jobboard-scraper)

# Actor input Schema

## `searchTerms` (type: `array`):

Keywords matched against job TITLES, case-insensitive. A job matches if ANY term matches (OR). Multi-word terms need every word in the title ('python developer' requires both). Empty = every job. Capped at 10 terms per run - extras beyond the first 10 are ignored, and the run's status message says so.

## `matchDescriptions` (type: `boolean`):

OFF (default): searchTerms match job TITLES only — precise, every hit visibly carries your keyword. ON: terms also match the job's tags and the first 500 characters of its description, where boards provide them — more recall, less precision ('python' will also match a marketing role whose blurb mentions the Python team). DevITjobs publishes no descriptions, so there this widens matching to its technology tags only.

## `boards` (type: `array`):

Which boards to sweep. Default: all of them. Results are deduplicated across boards, so selecting more boards never means paying twice for the same role.

## `maxItems` (type: `integer`):

Maximum number of deduplicated jobs to return across all boards.

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

ON: every row's `description` column carries the job's full HTML-stripped description text (rows get heavy). OFF (default): `description` is null and only the always-on 500-character `description_snippet` ships. Boards that publish no description (DevITjobs) deliver null either way.

## `onlyNewJobs` (type: `boolean`):

Deliver only postings this exact search (same searchTerms + boards) has never delivered before. The first run is the baseline — it delivers everything and says so; every later run returns only new postings. A run where nothing is new delivers 0 rows, reports 'Nothing new since <last run>' and bills nothing. Memory lives in a named key-value store ('remote-job-monitor') in YOUR Apify account, one record per search, entries pruned after 90 days.

## Actor input object example

```json
{
  "searchTerms": [
    "python developer"
  ],
  "matchDescriptions": false,
  "boards": [
    "remoteok",
    "weworkremotely",
    "working_nomads",
    "devitjobs_us",
    "devitjobs_uk",
    "muse",
    "remotive",
    "jobicy",
    "himalayas",
    "hn_hiring"
  ],
  "maxItems": 50,
  "includeDescription": false,
  "onlyNewJobs": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "searchTerms": [
        "python developer"
    ],
    "maxItems": 50,
    "onlyNewJobs": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("flash_scraper/remote-job-aggregator").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 = {
    "searchTerms": ["python developer"],
    "maxItems": 50,
    "onlyNewJobs": False,
}

# Run the Actor and wait for it to finish
run = client.actor("flash_scraper/remote-job-aggregator").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 '{
  "searchTerms": [
    "python developer"
  ],
  "maxItems": 50,
  "onlyNewJobs": false
}' |
apify call flash_scraper/remote-job-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,flash_scraper/remote-job-aggregator"
        }
    }
}

```

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/HjxcrzAAsZjDh4qPK/builds/ywgSQsOM2O62lcm0o/openapi.json
