# Remote Jobs Aggregator: Job Feed from 3 Boards (`atesen-software/remote-jobs-aggregator`) Actor

Remote jobs API and job board aggregator: Remote OK, Jobicy and We Work Remotely in one clean remote job feed with salary, seniority and filters. Schedule it and get new jobs only. Source credit on every row.

- **URL**: https://apify.com/atesen-software/remote-jobs-aggregator.md
- **Developed by:** [Atesen Software](https://apify.com/atesen-software) (community)
- **Categories:** Jobs, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 job rows

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

### What does Remote Jobs Aggregator do?

Remote Jobs Aggregator is a **remote jobs API and job board aggregator**. It reads three public remote-job feeds in one run and returns a single clean **remote job feed** you can export as JSON, CSV or Excel, schedule, or call through the Apify API. Switch on **new jobs only** and a daily or weekly schedule returns just the jobs it has not delivered before.

| Board | What you get |
| --- | --- |
| [Remote OK](https://remoteok.com) | Latest remote jobs with USD salary ranges where published |
| [Jobicy](https://jobicy.com) | Remote jobs by industry, level and region, with salary ranges where published |
| [We Work Remotely](https://weworkremotely.com) | The public RSS feed: category, region, skills, contract type |

Every row uses the same fields: title, company, location, remote region, employment type, seniority, tags, salary minimum, maximum, currency and period, posting date, description, the apply link, the job page on the source board, and the credit line the board asks you to show.

No login, no browser, no proxy. Every board is read through its own public API or feed.

### Who is it for?

- **Job board and newsletter founders** who need a steady stream of remote listings to review and republish with the right credit.
- **Recruiting and sourcing teams** that watch which companies hire for a role, level or region.
- **Data analysts and researchers** who track remote hiring, seniority mix and published pay.
- **Automation builders** who feed a Slack channel, a spreadsheet, a database or an AI agent with fresh remote jobs.

### How to collect remote jobs

1. Open the **Input** tab.
2. Leave all boards selected, or pick some. Add **keywords** (any match), for example `python`, `devops`, `product designer`.
3. Optional filters: **tags**, **locations** (`europe`, `usa`, `latam`), **employment types**, **seniority**, **salary range and currency**, and how many days back to look.
4. Set **Max jobs to return** and press **Start**.
5. Download the dataset, or open the **Run summary** record to see how many jobs each board returned and whether any board failed.

#### Get only new jobs every day

1. Run once with **Only new jobs** switched on. The first run returns everything that matches your filters.
2. Create a schedule that starts the same input every day or week.
3. Each later run returns only jobs that were not delivered before. A run with no new jobs returns nothing and is not charged.

Give every saved search its own **Feed name** (for example `python-europe`) so two schedules never share their memory. The memory is a small key-value store named `remote-jobs-aggregator-state` in your own Apify account. It keeps short fingerprints (a job and its company-and-title key count as two) for up to 20,000 entries and 90 days, so roughly 10,000 jobs per feed; older entries drop out first. A job that is cut off by **Max jobs to return** is not marked as delivered, so the next run still returns it. Turn on **Forget previously delivered jobs** for one run to start a feed again from zero. If you delete the store or reuse a feed name for a different search, the memory starts again (or is shared), and jobs you already received can be returned and charged again.

#### Example input

```json
{
  "keywords": ["engineer", "developer"],
  "locations": ["europe", "usa"],
  "seniorities": ["senior", "lead"],
  "minAnnualSalary": 90000,
  "salaryCurrencies": ["USD", "EUR"],
  "postedWithinDays": 14,
  "onlyNewJobs": true,
  "stateName": "senior-engineers",
  "maxItems": 100
}
```

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `sources` | all three | Boards to read: `remoteok`, `jobicy`, `weworkremotely`. |
| `keywords` | none | Keep jobs where a word in the title, company, category or tags starts with any keyword (`engineer` also finds Engineering, `rust` does not find Trust). Keywords of three letters or fewer, such as `go` or `ai`, must match a whole word. |
| `excludeKeywords` | none | Drop jobs that contain any of these words. |
| `tags` | none | Keep jobs that carry one of these tags or categories. |
| `searchDescription` | off | Also match keywords against the full description. |
| `locations` | none | Keep jobs whose location or candidate restriction contains one of these words. |
| `includeWorldwide` | on | Jobs open to anyone also pass the location filter. |
| `remoteOnly` | on | Keep only jobs the board marks as remote. All boards offered today list remote jobs only, so this changes nothing yet. |
| `employmentTypes` | all | `full_time`, `part_time`, `contract`, `freelance`, `internship`, `other`. |
| `seniorities` | all | `intern`, `junior`, `mid`, `senior`, `lead`, `manager`, `executive`. |
| `onlyWithSalary` | off | Keep only jobs with a published pay range. |
| `minAnnualSalary`, `maxAnnualSalary` | none | Salary range filter. Hourly, daily, weekly and monthly pay is converted to a year. Jobs without a salary are dropped when either is set. |
| `salaryCurrencies` | none | Keep only pay published in these currencies, for example `USD`, `EUR`. Amounts are never converted between currencies. |
| `postedWithinDays` | 30 | Drop older jobs. `0` turns the filter off. |
| `descriptionFormat` | `text` | `text`, `html` or `none`. |
| `dedupe` | on | Return the same company and title once. |
| `sortBy` | `newest` | `newest` across all boards, or `balanced` so boards take turns. |
| `onlyNewJobs` | off | Return only jobs this feed has not delivered before. |
| `stateName` | `default` | Name of the feed whose delivered jobs are remembered. |
| `resetState` | off | Forget the delivered jobs of this feed name before the run. |
| `maxItems` | 100 | Maximum jobs returned (and charged). |
| `maxItemsPerSource` | 300 | How deep to read the paginated board (Jobicy) before filters apply. |

### Output

Each dataset row is one job. A shortened example:

```json
{
  "id": "jobicy:156402",
  "source": "jobicy",
  "sourceName": "Jobicy",
  "sourceUrl": "https://jobicy.com/jobs/156402-growth-product-manager-2",
  "applyUrl": "https://jobicy.com/jobs/156402-growth-product-manager-2",
  "title": "Growth Product Manager",
  "company": "YipitData",
  "location": "USA",
  "remoteRegion": "USA",
  "isRemote": true,
  "employmentType": "full_time",
  "seniority": "senior",
  "category": "Product & Operations",
  "tags": ["Product & Operations"],
  "salaryMin": 170000,
  "salaryMax": 170000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "postedAt": "2026-10-22T13:26:43.000Z",
  "expiresAt": null,
  "description": "About Us: YipitData is the leading market research and analytics firm...",
  "alsoListedOn": [],
  "attribution": "Source: Jobicy (https://jobicy.com). Credit Jobicy as the original source with a direct link and send applicants to the original job URL.",
  "scrapedAt": "2026-10-22T18:26:21.000Z"
}
```

`remoteRegion` says where the candidate must live as the board published it: `Worldwide` when the board says anywhere, a region or country list otherwise, and empty when the board says nothing.

The **Run summary** record (`SUMMARY` in the key-value store) lists, for every board, how many listings were read, how many matched your filters, whether the answer came from the cache and any error, plus the totals, the new-jobs counters and the reason the run stopped.

### How fresh is the data?

To keep to the boards' rules (Jobicy asks for no more than one automated sync per hour), the Actor saves each board's answer for 65 minutes in a small key-value store named `remote-jobs-source-cache` in your own Apify account. A run that starts inside that window reads the saved answer instead of calling the board again, so a result can be up to 65 minutes behind the board. The saved answers hold job fields only, and e-mail addresses and phone numbers are masked before anything is stored. The **Run summary** and the run's status message name the boards that came from the cache. A board that failed or answered only in part is never saved, and if the store cannot be read or written the Actor simply reads the boards. You can delete the store at any time. Rows read from the cache are charged like any other row. **Only new jobs** is not affected: a job is remembered by its id once it has been delivered, so a job that appears while the cache is still warm is delivered by the first run after the cache refreshes.

### How much does it cost?

The Actor uses **pay per event**: you pay a small fixed price for each job returned (see the pricing box on this page) and there is no start fee. Boards that fail or return nothing cost nothing, and a new-jobs run with nothing new costs nothing. Set **Max jobs to return**, or your own maximum charge per run, to cap spending. Memory use is low (256 MB is enough) and a typical run takes well under a minute.

### Using the data

- **API:** start runs and read datasets from any language through the [Apify API](https://docs.apify.com/api/v2).
- **Schedules:** run daily or hourly with **Only new jobs** switched on.
- **Integrations:** send results to Google Sheets, Slack, Airtable, Zapier, Make, n8n or a webhook from the Integrations tab.

### Source terms and credit

Every row carries `source`, `sourceUrl` and `attribution`. Show the credit line and link to `sourceUrl` wherever you display a job. In short, as the boards state it:

- **[Remote OK](https://remoteok.com):** link back to the Remote OK job page with a followable link (no `nofollow`) and name Remote OK as the source, otherwise Remote OK may suspend API access. Do not use the Remote OK logo without written permission.
- **[Jobicy](https://jobicy.com):** credit Jobicy as the original source with a direct link, and send applicants to the original job URL. Do not present listings as your own postings, and treat closed or expired jobs as unavailable. Jobicy asks for no more than one automated sync per hour: the Actor keeps each Jobicy answer for 65 minutes (see "How fresh is the data?"), so extra runs inside that window do not call Jobicy again. Still do not schedule this Actor more often than hourly.
- **[We Work Remotely](https://weworkremotely.com):** the feed is public for anyone; attribute the links back to We Work Remotely.

Himalayas, Remotive, Hacker News "Who is hiring" and Working Nomads are not included, because their terms forbid passing listings on to third-party aggregators, forbid automated collection, or give no permission for it. You are responsible for following each board's rules when you publish its jobs.

### Data protection

The Actor outputs company and job fields only. It does not collect recruiter names, e-mail addresses or phone numbers as data fields, and e-mail addresses and phone numbers that appear inside job texts are replaced with `[email removed]` and `[phone removed]` (common international, national and labelled formats; an unusual notation can slip through). Free text written by the employer can still mention a contact person by name; choose **No description** if you need metadata only.

### FAQ

**Does it need a login or proxy?** No. Every source is a public feed that works from normal cloud IPs.

**Why do I get fewer jobs than `maxItems`?** Your filters matched fewer jobs. Boards publish only their latest listings (about 100 for Remote OK and We Work Remotely, the last 7 days for Jobicy), so widen the keywords, raise `postedWithinDays`, or turn off `onlyWithSalary` and the salary range.

**How are salaries found?** From the board's structured fields where it has them (Remote OK, Jobicy). Boards without pay data return `null`. Amounts are as published; the salary filter converts the pay period to a year but never converts currencies.

**What if one board is down?** It is skipped, the others still return, the run ends successfully with a partial-result message, and the summary shows the error. If every selected board fails the run fails and nothing is charged.

**Can I request another board?** Yes, open an issue from the Actor page. Only boards whose terms allow reuse can be added.

Built by Atesen Software.

# Actor input Schema

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

Which job feeds to read. Leave all selected for the widest coverage. A board that is down is skipped and reported in the run summary; the others still return.

## `keywords` (type: `array`):

Keep jobs where a word in the title, company, category or tags starts with at least one of these keywords, for example python, product designer, devops (engineer also finds Engineering). Keywords of three letters or fewer, such as go or ai, must match a whole word. Leave empty to return every job.

## `excludeKeywords` (type: `array`):

Drop jobs whose title, company, category or tags contain any of these words, for example intern, unpaid.

## `tags` (type: `array`):

Keep jobs that carry at least one of these tags or categories, for example react, golang, customer support. Matches part of a tag.

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

When on, the keywords are also matched against the full job description text. Finds more matches.

## `locations` (type: `array`):

Keep jobs whose location or candidate restriction contains one of these words, for example europe, usa, germany, latam. Leave empty for all locations.

## `includeWorldwide` (type: `boolean`):

Jobs open to candidates anywhere (no restriction listed) also pass the location filter.

## `remoteOnly` (type: `boolean`):

Keep only jobs the board marks as remote. All boards offered today list remote jobs only, so this switch changes nothing until a board with on-site listings is added.

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

Keep only these types. Empty keeps all.

## `seniorities` (type: `array`):

Keep only these levels. The level comes from the board when given and from the job title otherwise. Empty keeps all.

## `onlyWithSalary` (type: `boolean`):

Keep only jobs that publish a pay range.

## `minAnnualSalary` (type: `integer`):

Keep jobs whose published pay range reaches at least this yearly amount. Hourly, daily, weekly and monthly pay is converted to a year (2,080 hours, 260 days, 52 weeks, 12 months). Jobs without a salary are dropped when this is set. Amounts are not converted between currencies; combine with Salary currencies.

## `maxAnnualSalary` (type: `integer`):

Keep jobs whose published pay range starts at or below this yearly amount. Jobs without a salary are dropped when this is set.

## `salaryCurrencies` (type: `array`):

Keep only jobs whose pay is published in one of these currencies, for example USD, EUR, GBP. Jobs without a salary are dropped when this is set.

## `postedWithinDays` (type: `integer`):

Drop jobs older than this many days. 0 disables the filter.

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

Plain text is easiest to use. HTML keeps the original formatting. None returns metadata only. E-mail addresses and phone numbers are removed in every format.

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

The same job is often posted on several boards. When on, it is returned once (the richest copy) and alsoListedOn lists the other boards.

## `sortBy` (type: `string`):

Newest first across all boards, or balanced: boards take turns so a large board does not fill the whole result.

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

Return only jobs this feed has not delivered before, so a daily or weekly schedule gets just the new ones. The memory is kept in a key-value store named remote-jobs-aggregator-state in your account (up to 20,000 fingerprints, roughly 10,000 jobs, for 90 days). A run with no new jobs returns nothing and charges nothing. The first run returns everything that matches.

## `stateName` (type: `string`):

Use one name per saved search, for example python-europe, so different schedules do not share their memory. Letters, digits, dash and underscore only.

## `resetState` (type: `boolean`):

Clears the memory of this feed name before the run, so every matching job counts as new again. Turn it off again after one run.

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

Newest jobs first. You are charged per returned job, so this also caps your cost.

## `maxItemsPerSource` (type: `integer`):

Upper bound on listings fetched from the paginated board (Jobicy) before filters apply. Raise it with narrow filters to dig deeper.

## Actor input object example

```json
{
  "sources": [
    "remoteok",
    "jobicy",
    "weworkremotely"
  ],
  "keywords": [
    "engineer"
  ],
  "excludeKeywords": [],
  "tags": [],
  "searchDescription": false,
  "locations": [],
  "includeWorldwide": true,
  "remoteOnly": true,
  "employmentTypes": [],
  "seniorities": [],
  "onlyWithSalary": false,
  "salaryCurrencies": [],
  "postedWithinDays": 30,
  "descriptionFormat": "text",
  "dedupe": true,
  "sortBy": "balanced",
  "onlyNewJobs": false,
  "stateName": "default",
  "resetState": false,
  "maxItems": 10,
  "maxItemsPerSource": 300
}
```

# Actor output Schema

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

One row per job after filters and de-duplication

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

Per-board counts, errors and applied filters

# 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",
        "weworkremotely"
    ],
    "keywords": [
        "engineer"
    ],
    "sortBy": "balanced",
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("atesen-software/remote-jobs-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 = {
    "sources": [
        "remoteok",
        "jobicy",
        "weworkremotely",
    ],
    "keywords": ["engineer"],
    "sortBy": "balanced",
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("atesen-software/remote-jobs-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 '{
  "sources": [
    "remoteok",
    "jobicy",
    "weworkremotely"
  ],
  "keywords": [
    "engineer"
  ],
  "sortBy": "balanced",
  "maxItems": 10
}' |
apify call atesen-software/remote-jobs-aggregator --silent --output-dataset

```

## MCP server setup

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