# RemoteOK Jobs Scraper - Remote Listings, Salaries & Details (`abotapi/remoteok-com-scraper`) Actor

Scrape RemoteOK jobs by keyword or job URL. Extract title, company, location, remote details, tags, salary when available, listing URL, description, and metadata into clean structured dataset rows.

- **URL**: https://apify.com/abotapi/remoteok-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Lead generation, Developer tools
- **Features:** Limited permissions
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 dataset items

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

## RemoteOK Scraper: Remote Job Listings, Filters & Sorting

RemoteOK Scraper turns RemoteOK into clean, structured job rows. Search current listings by keyword or paste RemoteOK listing and tag URLs directly, then filter by salary, company, location, posting age and tags, and sort the saved rows client-side. Every row carries normalized fields such as salary midpoint and posting age alongside the original listing data, and you can export straight to JSON, CSV or Excel, or read it through the API.

### Why This Scraper?

- **Two ways to find jobs.** Keyword search across current listings, or paste RemoteOK listing and tag URLs directly. No need to build search URLs by hand.
- **Salary, company, location, age and tag filters.** Narrow a run to exactly the roles you care about before you pay for a row.
- **Normalized fields on every row.** Salary midpoint, posting age in days, a cleaned plain-text description, and matched search tokens, in addition to the site's own fields.
- **Client-side sorting.** Order saved rows by posting time, salary, or company name without a second pass.
- **Full-catalogue pulls across runs.** Leave the page cap open and continue an interrupted or capped pull in a later run without re-saving (or re-billing for) jobs the previous run already collected.
- **Optional raw fields.** Keep the complete original listing object alongside the normalized ones, or drop it to save space.
- **Optional MCP connector export.** Pipe a summary of saved jobs into apps such as Notion, Linear, Airtable, or Apify.

### Use Cases

- **Recruiting and sourcing:** pull current remote roles by keyword, salary band, or tag to build candidate outreach lists.
- **Job board and job-alert apps:** feed filtered, sorted listings into your own site, bot, or notification tool.
- **Market and compensation research:** track salary ranges, posting volume, and tag trends for remote roles over time.
- **Lead generation:** find companies actively hiring in a given location or stack, using the company and tag filters.
- **Competitive and industry analysis:** monitor which companies and roles a segment of the remote job market is posting.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `id` / `jobId` | `"1000001"` (the key used for de-duplication and resume) |
| `slug` | `"remote-senior-backend-engineer-sample-labs-1000001"` |
| `title` | `"Senior Backend Engineer"` |
| `companyName` | `"Sample Labs"` |
| `locationText` | `"Remote, Worldwide"` |
| `postedAt` | `"2024-01-15T09:00:00+00:00"` |
| `postedEpoch` | `1705309200` |
| `postedAgeDays` | `12` |
| `salary_min` / `salary_max` | `90000` / `130000` |
| `salaryMid` | `110000` |
| `hasSalary` | `true` |
| `tags` | `["python", "backend", "senior"]` |
| `tagCount` | `3` |
| `descriptionText` | `"We are a fully remote team looking for..."` |
| `url` | `"https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001"` |
| `applyUrl` | `"https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001"` |
| `sourceUrl` | the search or URL-mode source this row came from |
| `sourceType` | `"search-term"` (also `"url"`) |
| `searchTerm` | `"python"` (`null` for `all jobs`, or in url mode with no query term) |
| `matchedSearchTokens` | `["python"]` |
| `matchedTagFilters` | matched values from your `tags` filter, if set |
| `companyLogoUrl` | `"https://remoteok.com/assets/sample-logo.png"` |
| `scrapedAt` | `"2024-01-27T10:00:00.000Z"` |

When `includeRawFields` is on (the default), the complete original listing object is also kept in `raw`.

### How to Use

1. Pick a **mode**: `search` (keyword) or `url` (paste RemoteOK links).
2. Fill in the terms or URLs for that mode, and set the filters and sort order you want.
3. Set **Max items** (per term or URL) and, optionally, **Max pages**, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Broad keyword search:**

```json
{
  "mode": "search",
  "searchTerms": ["all jobs"],
  "sortBy": "posted-desc",
  "maxItems": 20
}
```

**Filtered keyword search:**

```json
{
  "mode": "search",
  "searchTerms": ["python", "devops"],
  "tags": ["senior"],
  "minSalary": 80000,
  "requireSalary": true,
  "sortBy": "salary-desc",
  "maxItems": 30
}
```

**Paste RemoteOK URLs:**

```json
{
  "mode": "url",
  "startUrls": [
    { "url": "https://remoteok.com/remote-dev-jobs" },
    { "url": "https://remoteok.com/remote-design-jobs" }
  ],
  "requireSalary": true,
  "maxItems": 15
}
```

**Full-catalogue pull with no page cap:**

```json
{
  "mode": "search",
  "searchTerms": ["all jobs"],
  "maxItems": 5000,
  "maxPages": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/remoteok-com-scraper").call(run_input={"mode": "search", "searchTerms": ["python"], "maxItems": 20})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], job["companyName"], job["salaryMid"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/remoteok-com-scraper').call({ mode: 'search', searchTerms: ['python'], maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Caps and full-catalogue pulls

**Max items** and **Max pages** apply per search term or per URL, not to the whole run: a run with three search terms and `maxItems: 20` can save up to 60 rows. Leave **Max pages** empty to walk every result page for a target; an explicit value stops after that many pages of that target regardless of `maxItems`.

For a full-catalogue pull (all current listings for a term or URL, not just the first page), leave **Max pages** empty and set **Max items** high enough (or paste multiple listing URLs, each with its own cap). Use **Resume from a previous run** to continue such a pull across separate runs: paste the prior run's ID (or its dataset ID) and this run skips every job `id` already saved there, appending only new jobs. It skips only the jobs in that one run's dataset, not in runs before it, so for a third pull point it at a dataset that holds everything collected so far, or de-duplicate by `id` on your side. A checkpoint is also saved automatically as the run progresses, so a run that was interrupted by a platform migration, or that failed and is then resumed with **Resurrect**, picks back up without re-saving (or re-billing for) jobs it had already pushed in that same run.

**Interrupted runs:** a job is marked as handled only after it has been saved, so a search term or URL that stops at its **Max items** cap leaves the rest of that page's jobs available to later search terms or URLs in the same run. If a run is interrupted (migration, abort or failure) between pages of one search term or URL, jobs of that term not yet saved are not marked, and a **Resurrect** or **Resume from a previous run** picks them up.

### Send results into your apps (MCP connectors)

Optionally pipe the saved jobs into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each job, not the full JSON. Each row becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where job pages should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Connector failures never change the dataset output. Leave the field empty to skip.

### Input Parameters

**Supported URL shapes (URL mode).** Each pasted URL is read on its own; facets in the URL win over the run-level filters.

| URL shape | Example | Behaviour |
| --- | --- | --- |
| Tag listing | `https://remoteok.com/remote-react-jobs` (also `.json`) | Jobs for that tag (in the default list) |
| Homepage, `/api`, `/remote-jobs` | `https://remoteok.com/` | The current feed of all jobs (in the default list) |
| Tag or location facets | `https://remoteok.com/api?tag=python`, `https://remoteok.com/?tags=python`, `https://remoteok.com/remote-jobs?location=Remote` | A single tag is fetched from RemoteOK's own tag feed for that tag (about 100 jobs); two or more tags, and location, are applied as filters on the current feed (location is a text match on the job's location field, which is often empty). The single-tag form is in the default list; the location form is not (it returns rows only when a job's location text matches) |
| Search query | `https://remoteok.com/remote-jobs?search=python` | Applied as a keyword filter (in the default list) |
| Page and sort | `https://remoteok.com/remote-python-jobs?page=2`, `...?order_by=salary` | `page` sets the start page; `order_by=salary` sorts by salary (both in the default list) |
| Location page | `https://remoteok.com/remote-jobs-in-europe` | Jobs whose location text contains the place; often 0 rows because most jobs have an empty or generic location (not in the default list) |
| Job page | `https://remoteok.com/remote-jobs/remote-software-engineer-infrastructure-sodalis-inc-1137614` | That job, only if it is still in the current feed (older or closed jobs return 0 rows; not in the default list) |
| Short job link | `https://remoteok.com/l/1137614` (or `https://remoteok.com/1137614`) | That job, only if it is still in the current feed (not in the default list) |
| Company page | `https://remoteok.com/remote-companies/sodalis-inc` (or `/company/sodalis-inc`) | That company's jobs in the current feed; 0 rows when the company has none right now (not in the default list) |
| Other hosts | `https://www.remoteok.com/...`, `https://remoteok.io/...` | Treated as remoteok.com (the `www` form is in the default list) |

The company directory (`/remote-companies`) and any other URL shape are skipped with a warning in the log; if every URL is skipped the run fails instead of finishing with zero rows.

**Behaviour change:** URL mode with **RemoteOK URLs** left untouched now reads the tag, homepage, API, facet, search, page, sort and host examples above that return rows from the current feed (it used to have no default); location, job and company pages are left out of the default because they only return rows when a matching job is in the feed. Replace the list with your own URLs to scrape only those. Plain names work in search terms and tags: `React Native` matches the `react-native` tag.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (keyword) or `url` (paste RemoteOK links). |
| `searchTerms` | array | `["all jobs"]` | Job keywords or tag phrases, such as `python` or `senior+devops` (search mode). |
| `startUrls` | array | one example per supported URL shape | RemoteOK listing, tag, location, company or job URLs to read directly (url mode). |
| `tags` | array | (none) | Keep only jobs whose tag list contains at least one of these values. |
| `companies` | array | (none) | Keep only jobs whose company name contains one of these values. |
| `locations` | array | (none) | Keep only jobs whose location text contains one of these values. |
| `minSalary` | integer | (none) | Keep only jobs whose salary range reaches at least this amount. |
| `maxSalary` | integer | (none) | Keep only jobs whose salary range starts at or below this amount. |
| `postedWithinDays` | integer | (none) | Keep only jobs posted within this many days. |
| `sortBy` | string | `posted-desc` | Client-side sort order applied after filtering: `posted-desc`, `posted-asc`, `salary-desc`, `salary-asc`, or `company-asc`. |
| `requireSalary` | boolean | `false` | Keep only jobs where a salary range is present. |
| `includeDescriptionText` | boolean | `true` | Add a cleaned plain-text version of the listing description. |
| `includeRawFields` | boolean | `true` | Keep the full original job object alongside normalized fields, in `raw`. |
| `keywordFilter` | string | (none) | Comma-separated keywords; keep only jobs whose title, company, location, description or tags contain at least one. |
| `includeUnverified` | boolean | `true` | Turn off to keep only listings RemoteOK marks as verified. |
| `filterNonTech` | boolean | `false` | Keep only technical roles: the job title must name an engineering, developer, devops, data, security, QA or similar role. Sales, recruiting, marketing, support and coordinator roles are dropped even when RemoteOK tags them with technology words. |
| `maxResults` | integer | (none) | Alias of `maxItems`: above 0 it replaces `maxItems`, `0` means unlimited. |
| `maxItems` | integer | `20` | Maximum matching rows to save for each search term or start URL. A cap, not a guarantee. |
| `maxPages` | integer | (none, walks every page) | Result pages to walk per search term or start URL. An explicit value is honored with no upper limit. |
| `resumeFromRunId` | string | (none) | A previous run ID or dataset ID to continue a full-catalogue walk across separate runs; jobs already collected there (matched by `id`) are skipped. |
| `incrementalMode` | boolean | `false` | For scheduled runs: deliver only NEW, UPDATED and REAPPEARED jobs compared with the previous run of the same tracked scope. Rows gain `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`. Unchanged jobs are not delivered or billed. |
| `stateKey` | string | (auto) | Incremental mode only: name of the baseline. Empty derives it from your search terms, URLs and filters (not from caps, sort or proxy). |
| `emitUnchanged` | boolean | `false` | Incremental mode only: also deliver unchanged jobs (`changeType` UNCHANGED); each delivered row is billed like any other. |
| `emitExpired` | boolean | `false` | Incremental mode only: deliver a row with `changeType` EXPIRED for each tracked job that left the feed. Reported only when the run covered the whole tracked scope (no item cap hit, no failed or skipped search, not resumed, not starting past page 1). Each delivered row is billed like any other, and EXPIRED rows are delivered and billed in addition to the Max items cap. |
| `ignoreFieldsForChanges` | array | (none) | Incremental mode only: field names left out of change detection. `scrapedAt`, `scraped_at` and `postedAgeDays` are always ignored. |
| `proxy` | object | Apify Proxy, automatic group | Connection settings: Apify Proxy group, country, or your own proxy URLs (see Plan Requirement). |
| `mcpConnectors` | array | (none) | Optional: send a summary of each saved job to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which job pages are created. |
| `maxNotifyJobs` | integer | `50` | Cap on jobs written to each connector per run. Does not affect the dataset. |
| `maxNotifyListings` | integer | (none) | Same cap as `maxNotifyJobs`; when set it wins. |

#### Notifications and webhooks

There is no custom webhook input. To get a call when a run finishes, use Apify's built-in webhooks and integrations on the run or on a schedule; they deliver the dataset to your own endpoint with Apify's signed payloads.

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "id": "1000001",
  "jobId": "1000001",
  "slug": "remote-senior-backend-engineer-sample-labs-1000001",
  "title": "Senior Backend Engineer",
  "position": "Senior Backend Engineer",
  "company": "Sample Labs",
  "companyName": "Sample Labs",
  "location": "Remote, Worldwide",
  "locationText": "Remote, Worldwide",
  "descriptionText": "We are a fully remote team looking for a senior backend engineer to join our platform team.",
  "tags": ["python", "backend", "senior"],
  "tagCount": 3,
  "url": "https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001",
  "applyUrl": "https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001",
  "sourceUrl": "https://remoteok.com/remote-python-jobs",
  "sourceType": "search-term",
  "searchTerm": "python",
  "postedAt": "2024-01-15T09:00:00+00:00",
  "postedEpoch": 1705309200,
  "postedAgeDays": 12,
  "salary_min": 90000,
  "salary_max": 130000,
  "salaryMid": 110000,
  "hasSalary": true,
  "companyLogoUrl": "https://remoteok.com/assets/sample-logo.png",
  "scrapedAt": "2024-01-27T10:00:00.000Z",
  "scraped_at": "2024-01-27T10:00:00.000Z",
  "source": "remoteok",
  "source_url": "https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001",
  "company_name": "Sample Labs",
  "company_slug": "sample-labs",
  "company_url": "https://remoteok.com/company/sample-labs",
  "company_logo_url": "https://remoteok.com/assets/sample-logo.png",
  "type": null,
  "work_mode": "remote",
  "experience_level": null,
  "currency": "USD",
  "salary_period": "year",
  "markets": ["python", "django"],
  "posted_at": "2024-01-15T09:00:00+00:00",
  "expires_at": null,
  "data_source": "remoteok-api",
  "data_source_url": "https://remoteok.com/api",
  "verified": true,
  "matchedSearchTokens": ["python"],
  "matchedTagFilters": [],
  "raw": {}
}
```

#### Run summary

The dataset has one **Jobs** overview view. Every run also writes an `OUTPUT` record to the key-value store with the total saved, a per-search or per-URL entry (status, matched and saved counts) and the filters applied.

### Plan Requirement

Requests go through Apify Proxy, using the **Proxy & connection** setting (default: Apify Proxy, automatic group). In a local check through Apify Proxy, both the default (datacenter) connection and the residential group loaded RemoteOK's public feed, so the default needs no change. Apify Proxy access requires a paid Apify plan.

If the default connection is refused, the actor retries on other connections and logs "Served over the backup connection (the default connection was unavailable)." when it was served that way. If residential access is unavailable on your plan, the log shows "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." If a single search term or URL cannot be loaded, the run logs "Could not load this search" and continues with the rest.

If nothing could be loaded, the run fails with one of these messages instead of returning an empty dataset:

- "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid."
- "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested RemoteOK pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again."
- "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

You can also supply your own proxy URLs in the same field.

### FAQ

#### How much does it cost?

Pay per event. The **Pricing** tab shows the current rates. Two events are charged: the actor start (once per run) and each dataset item saved (once per job row pushed to the dataset). Rows that are skipped, filtered out or already saved by a resumed run are not charged. Use **Max items** to cap the job rows per search term or URL; with `emitExpired` on, EXPIRED rows for tracked jobs that left the feed are delivered and billed in addition to that cap.

#### Is it legal to scrape RemoteOK?

This actor collects only publicly available job listing data. You are responsible for how you use it: follow RemoteOK's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.

#### Why did a search term or URL return fewer jobs than I expected within a mixed run?

A job counts as handled only after it has been saved. Search terms or URLs that stop at their **Max items** cap do not hide the rest of that page from later terms or URLs in the same run. A later term or URL still skips jobs that an earlier one in the same run already saved, so overlapping terms return each job once. Jobs that do not pass your filters never count as handled. **Resume from a previous run** only skips jobs actually found in the prior run's dataset.

#### Can I get only new job listings on a schedule?

There is no built-in change-detection mode. For a recurring pull, pass the previous run's ID (or dataset ID) into **Resume from a previous run**: this run then skips every job `id` saved in that one run and appends the rest. It does not remember runs before that one, so a job saved two runs ago can be saved again. For a clean recurring feed, de-duplicate by `id` on your side. You also have to pass the latest run ID into the next run's input yourself.

#### Why did my run fail instead of returning an empty dataset?

The run stops with a clear error when no search terms or URLs are supplied, when **Resume from a previous run** points to a run or dataset your account cannot read, when every pasted URL is unsupported, or when no results page could be loaded at all (see the messages under Plan Requirement). If only some search terms or URLs fail to load, the run logs a warning, saves the rest, and lists the failed ones in the `OUTPUT` summary. Check the URLs, then run it again.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push saved jobs into Notion, Linear or Airtable.

### 🔗 Want more jobs data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>💼 <a href="https://apify.com/abotapi/we-work-remotely-jobs-scraper"><b>WeWorkRemotely Scraper</b></a><br>Extract current remote job listings from weworkremotely.com. Use keyword search or WWR...</td><td>💼 <a href="https://apify.com/abotapi/careerbuilder-scraper"><b>CareerBuilder Scraper</b></a><br>Scrape CareerBuilder.com job listings by search or URL. Returns title, company profile...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/wellfound-jobs-scraper"><b>Wellfound Jobs Scraper</b></a><br>Scrape startup jobs from Wellfound.com by keyword, location, role, or remote status...</td><td>💼 <a href="https://apify.com/abotapi/jora-jobs-scraper"><b>Jora Jobs Scraper</b></a><br>Scrape Jora.com job listings across supported countries by keyword, filters, or URL...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/monster-com"><b>Monster Jobs Scraper</b></a><br>Scrape Monster.com job listings from search and direct job URLs. Extract full...</td><td>💼 <a href="https://apify.com/abotapi/myworkdayjobs-scraper"><b>Workday Jobs Scraper</b></a><br>Scrape job postings from any Workday-hosted careers site on Myworkdayjobs.com. Paste...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/remoteok-com-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Use keyword search to filter current listings by terms, or URL mode to read one or more RemoteOK listing URLs directly.

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

Enter job keywords or tag phrases such as python or senior+devops. Use all jobs for a broad default run. Each term is processed separately and capped by Max items.

## `startUrls` (type: `array`):

Paste RemoteOK URLs: tag listings, the homepage or /api feed, company pages, job pages and location pages (job, company and location pages return rows only when they match a job in the current feed), with optional ?tags= ?location= ?search= ?page= ?order_by= facets. Each URL is processed separately and capped by Max items. Unsupported URLs are skipped with a warning.

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

Keep only jobs whose tag list contains at least one of these values.

## `companies` (type: `array`):

Keep only jobs whose company name contains one of these values.

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

Keep only jobs whose location text contains one of these values.

## `minSalary` (type: `integer`):

Keep only jobs whose salary range reaches at least this amount.

## `maxSalary` (type: `integer`):

Keep only jobs whose salary range starts at or below this amount.

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

Keep only jobs posted within this many days. Leave empty for no age filter.

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

Client-side sort order applied after filtering.

## `requireSalary` (type: `boolean`):

Keep only jobs where a salary range is present.

## `keywordFilter` (type: `string`):

Keep only jobs whose title, company, location, description or tags contain at least one of these comma-separated keywords (case-insensitive). Applied after the search terms.

## `includeUnverified` (type: `boolean`):

Turn off to keep only listings that RemoteOK marks as verified.

## `filterNonTech` (type: `boolean`):

Keep only technical roles: the job title must name an engineering, developer, devops, data, security, QA or similar role. Sales, recruiting, marketing, support and coordinator roles are dropped even when RemoteOK tags them with technology words.

## `includeDescriptionText` (type: `boolean`):

When enabled, the actor adds a cleaned plain-text version of the listing description.

## `includeRawFields` (type: `boolean`):

When enabled, the actor keeps the full original job object alongside normalized fields.

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

Maximum matching rows to save for each search term or start URL. This is a cap, not a guarantee.

## `maxResults` (type: `integer`):

Alias of Max items. When set above 0 it replaces Max items; 0 means unlimited (every matching job). Leave empty to use Max items.

## `maxPages` (type: `integer`):

Leave empty to walk every result page. When a source has more matching jobs than fit in one page, the actor keeps paginating until Max items is reached or the source is exhausted.

## `proxy` (type: `object`):

Requests go through Apify Proxy (automatic group by default). You can choose a proxy group or country, or paste your own proxy URLs. If the default connection is refused, a backup connection is used and logged.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Jobs already collected there (matched by id) are skipped, so this run only saves the new ones. Leave empty for a normal fresh run.

## `incrementalMode` (type: `boolean`):

For scheduled runs. Remembers the jobs seen by the previous run with the same tracked scope (or the same State key) and delivers only NEW, UPDATED and REAPPEARED jobs, each tagged with changeType, changedFields, firstSeenAt and lastSeenAt. Unchanged jobs are not delivered or billed unless 'Also deliver unchanged jobs' is on. Leave off for a normal run. Different from 'Resume from a previous run', which continues one specific run.

## `stateKey` (type: `string`):

Optional name for the incremental baseline. Leave empty to derive it automatically from your search terms, URLs and filters, so differently configured runs never share a baseline. Set the same text on two runs to make them share one. Only used when Incremental mode is on.

## `emitUnchanged` (type: `boolean`):

When Incremental mode is on, also push jobs that did not change since the last run (changeType UNCHANGED). Each delivered row is billed like any other row, and EXPIRED rows come in addition to the Max items cap. Off by default.

## `emitExpired` (type: `boolean`):

When Incremental mode is on, push a row with changeType EXPIRED for each job tracked last run that is no longer in the feed. Only when the run covered the whole tracked scope: a run capped by Max items, with a failed or skipped search, or starting past page 1 never reports expiry. Each delivered row is billed like any other row, and EXPIRED rows come in addition to the Max items cap. Off by default.

## `ignoreFieldsForChanges` (type: `array`):

Field names to leave out of change detection in incremental mode (dotted paths allowed for nested fields). scrapedAt, scraped_at and postedAgeDays are always ignored. Example: tags.

## `mcpConnectors` (type: `array`):

Optionally send scraped jobs into apps you already use via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify Settings Integrations, then select it here. The connector receives a condensed summary per job; the full record stays in the dataset. Leave empty to skip. Supported: Notion, Linear, Airtable, and Apify.

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which job pages are created. Required for Notion export; ignored by other connectors.

## `maxNotifyJobs` (type: `integer`):

Cap on jobs written to each connector. Does not affect the dataset.

## `maxNotifyListings` (type: `integer`):

Same cap as 'Max jobs to export per connector'. When set, this value wins. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "all jobs"
  ],
  "startUrls": [
    {
      "url": "https://remoteok.com/remote-react-jobs"
    },
    {
      "url": "https://remoteok.com/remote-dev-jobs.json?order_by=salary"
    },
    {
      "url": "https://remoteok.com/"
    },
    {
      "url": "https://remoteok.com/api?tag=python"
    },
    {
      "url": "https://remoteok.com/remote-python-jobs?page=2"
    },
    {
      "url": "https://remoteok.com/remote-jobs?search=python"
    },
    {
      "url": "https://www.remoteok.com/remote-dev-jobs"
    }
  ],
  "sortBy": "posted-desc",
  "requireSalary": false,
  "includeUnverified": true,
  "filterNonTech": false,
  "includeDescriptionText": true,
  "includeRawFields": true,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyJobs": 50
}
```

# Actor output Schema

## `overview` (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": [
        "all jobs"
    ],
    "startUrls": [
        {
            "url": "https://remoteok.com/remote-react-jobs"
        },
        {
            "url": "https://remoteok.com/remote-dev-jobs.json?order_by=salary"
        },
        {
            "url": "https://remoteok.com/"
        },
        {
            "url": "https://remoteok.com/api?tag=python"
        },
        {
            "url": "https://remoteok.com/remote-python-jobs?page=2"
        },
        {
            "url": "https://remoteok.com/remote-jobs?search=python"
        },
        {
            "url": "https://www.remoteok.com/remote-dev-jobs"
        }
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": []
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/remoteok-com-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 = {
    "searchTerms": ["all jobs"],
    "startUrls": [
        { "url": "https://remoteok.com/remote-react-jobs" },
        { "url": "https://remoteok.com/remote-dev-jobs.json?order_by=salary" },
        { "url": "https://remoteok.com/" },
        { "url": "https://remoteok.com/api?tag=python" },
        { "url": "https://remoteok.com/remote-python-jobs?page=2" },
        { "url": "https://remoteok.com/remote-jobs?search=python" },
        { "url": "https://www.remoteok.com/remote-dev-jobs" },
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": [],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/remoteok-com-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 '{
  "searchTerms": [
    "all jobs"
  ],
  "startUrls": [
    {
      "url": "https://remoteok.com/remote-react-jobs"
    },
    {
      "url": "https://remoteok.com/remote-dev-jobs.json?order_by=salary"
    },
    {
      "url": "https://remoteok.com/"
    },
    {
      "url": "https://remoteok.com/api?tag=python"
    },
    {
      "url": "https://remoteok.com/remote-python-jobs?page=2"
    },
    {
      "url": "https://remoteok.com/remote-jobs?search=python"
    },
    {
      "url": "https://www.remoteok.com/remote-dev-jobs"
    }
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  }
}' |
apify call abotapi/remoteok-com-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/remoteok-com-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/1Dxl5fZqSApFyrkdm/builds/OYlrs6fD22X4wziYl/openapi.json
