# Remote Jobs Scraper - Himalayas, Remote OK, Jobicy, Arbeitnow (`neverempty/remote-jobs-feed-monitor`) Actor

Remote jobs from four public job feeds (Himalayas, Remote OK, Jobicy, Arbeitnow) in one format: title, company, location, salary, tags, date and source links. The same job on several sites is returned once. Monitor the feeds and get only new jobs.

- **URL**: https://apify.com/neverempty/remote-jobs-feed-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 job returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 - Himalayas, Remote OK, Jobicy, Arbeitnow

Remote job postings from **four public job feeds** - Himalayas, Remote OK, Jobicy and Arbeitnow - returned in **one format**, one row per job: title, company, location, employment type, salary, tags, categories, posting date, description, the job page on the source site and the apply link.

- **The same job on several sites is one row.** Jobs with the same company and title on different sites are merged; the row lists every site it was found on and is charged once.
- **Monitoring mode** returns only the jobs that were not there at earlier runs.
- No login, no cookies, no proxy. Only the feeds each site publishes for this purpose are read.

### Source credit: what you must do with this data

Each site lets its feed be used on the condition that it is credited and linked. **If you publish or redistribute these rows, you are responsible for meeting each site's conditions.** Every row carries what you need: `source` (the site name), `sourceUrl` (the job's page on that site - link back to it) and `applyUrl` (the apply address the feed provides - send applicants there). The conditions, as written in the feeds on 2026-10-05:

- **Remote OK** - "Please link back (with follow, and without nofollow!) to the URL on Remote OK and mention Remote OK as a source \[...] Please don't use the Remote OK logo without written permission".
- **Jobicy** - "Please ensure Jobicy is clearly credited with a direct link to the source, and all application buttons redirect to the original job URL provided in this feed."
- **Arbeitnow** - "This is a free public API for jobs, please do not abuse. I would appreciate linking back to the site."
- **Himalayas** - the feed is published at himalayas.app/jobs/api; credit Himalayas and link to the job's page (`sourceUrl`). Read the current terms on himalayas.app before you republish.

Company logo addresses are deliberately **not** included in the rows. The conditions can change; the feeds themselves carry the current wording.

### What you get

| Column | Meaning |
|---|---|
| `title`, `company` | Job title and company name as given by the feed |
| `location`, `locationRestrictions` | Location or region text (`USA`, `Anywhere`, `Berlin`), and the list of allowed countries/regions when the feed gives one |
| `employmentType`, `employmentTypeRaw` | `full-time`, `part-time`, `contract`, `freelance`, `internship` or `temporary` when the feed's wording is recognised, and the wording as written. Remote OK gives no type |
| `seniority` | As written by the feed (Himalayas, Jobicy) |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | As given by the feed, not converted. Remote OK gives amounts without a currency or period, so those two stay `null`. A salary of 0 in a feed means "not given" and is returned as `null` |
| `tags`, `categories` | The feed's own tags and categories |
| `postedAt`, `expiresAt` | Posting date (ISO 8601, UTC); expiry date when the feed gives one (Himalayas) |
| `description`, `descriptionIsFull` | A plain-text excerpt of up to 500 characters, or the full text with HTML tags removed when `includeFullDescription` is on |
| `source`, `sourceUrl`, `applyUrl`, `sourceJobId` | The site the row was read from, the job's page on that site, the apply address from the feed, and the job's id on that site |
| `sources`, `foundOnSources` | Every site the job was found on in this run, each with its own `sourceUrl`, `applyUrl` and `sourceJobId` |
| `sourcesQueried` | The feeds that were read in this run |
| `change`, `previousCheckedAt` | Monitoring runs only: `new-job`, and when jobs were last recorded for these filters |
| `status`, `scrapedAt` | `ok` for a job row; when the run delivered it |

A value the feed does not give is `null`, never an empty string.

When a job is found on several sites, the row's values come from the first site in this order: Himalayas, Remote OK, Jobicy, Arbeitnow. Values that site does not give (location, type, seniority, tags, categories, salary) are filled from the other sites. Two jobs with the same company and title on the *same* site stay two rows.

### How much of each site this covers

A run reads the **newest postings** of each feed, not every job the sites hold:

| Feed | Requests per run (at most) | Jobs per request | Note |
|---|---|---|---|
| Himalayas | 10 | 20 | The feed said it held about 116,000 jobs on 2026-10-05; a run reads the newest 200 at most |
| Remote OK | 1 | about 100 | The feed carries only its newest postings |
| Jobicy | 1 | 100 | |
| Arbeitnow | 3 | 325 | Arbeitnow also lists on-site jobs; only postings it marks as remote are used (19 of 325 on the first page on 2026-10-05) |

So one run can return up to about 450 jobs before filters, and `maxJobs` is capped at 500. Filters (`keywords`, `locations`, ...) are applied to these newest postings after they have been read; they are not sent to the sites as a search. If nothing matches, the run says so in a free `no-match` row. For a steady stream, use monitoring mode on a schedule.

### Input

- **`sources`** - any of `himalayas`, `remoteok`, `jobicy`, `arbeitnow`. Empty means all four.
- **`keywords`** - any one must appear in the title, company, tags or categories, as a word or the start of a word (`ai` matches "AI Engineer" but not "Retail"; `engineer` matches "Engineering").
- **`categories`** - any one must appear in the tags or categories.
- **`locations`** - any one must appear in the location text or the allowed countries/regions. A job with no location in the feed does not match.
- **`employmentTypes`** - `full-time`, `part-time`, `contract`, `freelance`, `internship`, `temporary`. A job with no recognised type does not match.
- **`withSalaryOnly`** - only jobs with a minimum or maximum salary in the feed.
- **`postedAfter`** - only jobs posted on or after this date.
- **`maxJobs`** - with monitoring off, at most this many rows, newest first (default 200, at most 500).
- **`includeFullDescription`** - full description text instead of the excerpt.
- **`monitoringMode`**, **`resetMonitoringState`** - see below.

All filters that are set must match. With monitoring off, the feeds are read in rounds - one request to each feed per round - until enough matching jobs for `maxJobs` have been read or the request limits above are reached.

### Monitoring mode

Turn **monitoringMode** on and schedule the Actor with the same input.

- The **first time a feed is read** with a given set of filters, its newest jobs are only remembered (Himalayas: the newest 60; the others: one request). Nothing is returned and nothing is charged for that feed (a free `baseline-saved` row). To get the jobs that are listed now, run once with monitoring off.
- On **later runs**, each feed that is read and compared is charged one monitoring check (`source-checked`), and only jobs that have not been seen before come back, with `change: "new-job"`. If nothing is new, the run returns one free `no-new-jobs` row and costs only the checks.
- A job counts as **seen** when its id on that site is remembered, or when a job with the same company and title is remembered - from any of the four sites. So a job that shows up on a second site later, or is reposted under the same title, is not returned again.
- A job older than the oldest job remembered at the first read of its feed is not returned as new.
- Each feed is read from the newest posting down until a page with nothing new is reached (or the request limits above). If more jobs were posted between two runs than those limits cover, the older ones among them are not read.
- `maxJobs` is not applied in monitoring mode: every new job that was read is returned.
- A job that could not be delivered because of a charge limit is not remembered; it still counts as new, and later runs read that feed up to the request limits until a run delivers everything. The same applies when a later page of a feed could not be read. A feed that could not be read at all is not charged and nothing about it is remembered.
- When you add a feed to a monitoring schedule that already runs, the added feed gets its own first read; jobs that are also new on the feeds you already watch are still returned.
- A job that did not match your filters when it was first seen is remembered and is not returned later.

Jobs are remembered per set of filters, so two schedules with different filters do not affect each other. Do not run two schedules with the same filters at the same moment: the platform's key-value store has no atomic update, so two runs finishing together can overwrite each other's record and return the same job twice.

### What is charged

- **`job-returned`** - one per job row. A job found on several sites is one row and is charged once.
- **`source-checked`** - monitoring mode only: one per feed that was read (its first answer held at least one remote job) and compared with the remembered jobs, whether or not it carried a new job. The first read of a feed (which only remembers its jobs) is not charged, and runs with monitoring off never charge it.

There is no start fee and no monthly fee. Jobs dropped by your filters are not charged. Rows that explain why something was not returned are free: `source-unreadable`, `source-blocked`, `invalid-input`, `no-jobs`, `no-match`, `no-new-jobs`, `baseline-saved`, `limit-reached`, `not-read`, `budget-reached`.

If you set a maximum total charge for a run, a feed is not read when the limit has no room for one job row (in monitoring mode: one check plus one row), and a free `not-read` row names the feeds that were not read. Rows that do not fit inside the limit are not delivered and not charged, and a free `budget-reached` row says how many.

### How it reads

- Only these addresses are requested: `himalayas.app/jobs/api` (paged with the feed's own cursor), `remoteok.com/api`, `jobicy.com/api/v2/remote-jobs` and `www.arbeitnow.com/api/job-board-api`.
- One request at a time, with a pause of more than one second between requests. A request that fails with a connection error or HTTP 5xx is retried once. HTTP 403 or 429 is not retried and no proxy is used.
- If a feed cannot be read, the run adds a free row that names the feed and the reason, and continues with the other feeds. If a feed's answer no longer has the expected format, it is reported as `source-unreadable`, not as "no jobs".

### Limits

- Coverage is the newest postings of each feed (see above), not the whole of each site.
- Duplicates are matched by company name and job title after removing case, punctuation and accents. Jobs whose company or title is written differently on two sites are not merged, and two different openings with the same company and title on different sites are merged.
- Dates, salaries and locations are what the feeds give; nothing is converted or guessed.
- This Actor is not affiliated with or endorsed by Himalayas, Remote OK, Jobicy or Arbeitnow. You are responsible for how you use the data, including each site's conditions above.

# Actor input Schema

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

The job feeds to read. Leave empty to read all four: Himalayas, Remote OK, Jobicy and Arbeitnow. The log and the sourcesQueried column of every row say which feeds were read. In one run the Actor sends at most 10 requests to Himalayas (20 jobs each), 1 to Remote OK, 1 to Jobicy and 3 to Arbeitnow, so a run covers the newest postings of each feed, not every job the sites hold.

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

Keep only jobs where at least one of these appears in the job title, company name, tags or categories, as a word or the start of a word (ai matches "AI Engineer" but not "Retail"). Not case-sensitive. One per line. Filters are applied to the newest postings each feed returns; they are not sent to the sites as a search.

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

Keep only jobs where at least one of these appears in the tags or categories the feed gives (for example: marketing, design, customer support, devops). Not case-sensitive. One per line.

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

Keep only jobs whose location text or list of allowed countries/regions contains at least one of these (for example: USA, United States, Europe, Germany, Anywhere). The sites write locations in their own words, so give the spellings you want to catch. A job with no location in the feed does not match.

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

Keep only jobs of these types. The type is read from the feed (Himalayas, Jobicy and Arbeitnow give one; Remote OK does not). A job whose type is missing or written in a way that is not recognised does not match.

## `withSalaryOnly` (type: `boolean`):

Keep only jobs for which the feed gives a minimum or maximum salary. Amounts are returned as given by the feed, with its currency and period when it states them; nothing is converted.

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

Keep only jobs posted on or after this date (for example 2026-10-01). A job with no posting date in the feed does not match.

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

With monitoring off, at most this many job rows are returned, newest first. The feeds are read in rounds (one request to each feed per round) until enough matching jobs have been read or the per-run request limits are reached. Not applied in monitoring mode, where every new job that was read is returned.

## `includeFullDescription` (type: `boolean`):

Off: the description column holds a short excerpt (up to 500 characters). On: it holds the full description text from the feed, with HTML tags removed.

## `monitoringMode` (type: `boolean`):

Remembers jobs between runs. The first time a feed is read with a given set of filters, its newest jobs are only remembered: nothing is returned and nothing is charged for that feed. On later runs each feed that is read (with at least one remote job in its first answer) and compared is charged one monitoring check (source-checked), and only jobs that have not been seen before are returned (job-returned). A job counts as seen when its id on that site is remembered, or when a job with the same company and title is remembered (from any of the four sites). A job that did not match your filters when first seen is remembered and not returned later. Jobs are remembered per set of filters. Do not run two schedules with the same filters at the same moment.

## `resetMonitoringState` (type: `boolean`):

Clears everything remembered for this Actor on your account (for every set of filters) before this run reads anything, so with monitoring on every feed is a first read again: its jobs are remembered and nothing is returned. Use it for one run and turn it off again.

## Actor input object example

```json
{
  "withSalaryOnly": false,
  "maxJobs": 200,
  "includeFullDescription": false,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per remote job read from the chosen feeds (Himalayas, Remote OK, Jobicy, Arbeitnow): title, company, location, employment type, salary, tags, categories, posting date, description excerpt, source site, the job page on that site and the apply link. A job found on several sites is one row that lists every site. Feeds that could not be read, runs with no match or no new job, and runs that hit a limit come back as their own rows and are not charged.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/remote-jobs-feed-monitor").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/remote-jobs-feed-monitor").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 '{}' |
apify call neverempty/remote-jobs-feed-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/remote-jobs-feed-monitor"
        }
    }
}
```

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/5es1HOp9wwIaGfUQa/builds/ngleMg2RdGRQKAQBi/openapi.json
