Skip to content

About

RemoteOK scraper for Node.js: pull current remoteok.com postings filtered by tag, keyword, company or salary into JSON for job-board backends, Slack alerts, webhooks and serverless cron jobs. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

1 Commit

Folders and files

Repository files navigation

RemoteOK Jobs Scraper β€” JavaScript example

RemoteOK Scraper for Node.js: remote job postings as JSON for your app

Run on Apify JavaScript MIT license CI

This repository shows how to use the RemoteOK Jobs Scraper from JavaScript. A single ES module built on Apify's official client starts the Actor, waits for the run and returns RemoteOK postings as plain objects with title, employer, tags, salary bounds, a text-only description, the job page and the apply link. RemoteOK lists remote-only roles, and the Actor reads its complete live board on each run, then narrows it to the tags, keywords, company or salary floor you configure. That fits a Node backend, a serverless timer or a bot that posts new roles to a channel. Run node index.js to save results.json and results.csv, or move the core call into your own service.

Actor: RemoteOK Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in Python: remoteok-scraper-python

πŸ”₯ What you get

  • The whole board in one request: RemoteOK serves every live listing as a single feed, so a run fetches it once and does all filtering afterwards. There is no paging and no visit to individual job pages.
  • Four filters that stack: tags, keywords, a company fragment and a salary floor. Different filters must all pass; within the tag list or the keyword list, any one hit is enough.
  • Loose tag matching on purpose: a wanted tag counts when it appears anywhere inside a posting's tag, ignoring case, so dev catches devops as well as dev. Choose specific tags when you want a narrow cut.
  • Missing values stay missing: RemoteOK writes 0 for an unstated salary and an empty string for an unknown location; both become null so averages and group-bys are not skewed by fake values.
  • Readable descriptions: the HTML body of each ad is flattened to plain text with entities decoded and runs of whitespace collapsed.
  • A stable key on every row: job_id is RemoteOK's numeric id, with the URL slug used as a fallback, and epoch_posted sits next to the ISO date for cheap range filters.
  • Upstream trouble is reported, not hidden: timeouts, rate limits and temporary unavailability are retried with growing pauses; if the feed still cannot be read, the run fails with the reason in its status message instead of returning an empty dataset.

🎯 Use cases

Post new remote roles to Slack or Discord

Run the module every hour from a scheduled function, remember which job_id values you have already announced, and send only the new ones to an incoming webhook. A minSalary value in the input keeps the channel limited to roles worth reading.

Feed a niche job board

A board for, say, remote Go or Elixir developers can call the Actor with the right tags, upsert the rows into Postgres by job_id and render them from its own API. The url field links each card back to the original RemoteOK ad.

Turn hiring into a sales trigger

Companies posting several remote engineering roles are usually growing. A queue worker can group the latest results by company and create or update accounts in your CRM, with the job titles attached as context for the rep.

Answer job questions inside your product

An Express route or a chat assistant can take a user's skill list, map it to tags, start a run and reply with the newest matches. Because description is plain text, it can be shown or summarised without an HTML sanitiser.

⚑ Quick start

You need an Apify account and an API token from Console β†’ Settings β†’ Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.

git clone https://github.com/devilscrapes/remoteok-scraper-javascript.git
cd remoteok-scraper-javascript
npm install
export APIFY_TOKEN=your_token_here
node index.js                       # uses input.json
node index.js my-input.json --out data --max-charge 2

The script starts the RemoteOK Jobs Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.

🧩 The core call

The integration fits in a few statements. call() waits until the Actor run has completed, then listItems() returns the stored postings from its default dataset as an array.

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('DevilScrapes/remoteok-jobs-scraper').call({"tags": ["python"], "maxItems": 10, "proxyConfiguration": {"useApifyProxy": false}});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);

πŸ“₯ Input

index.js forwards input.json untouched. The sample keeps up to ten ads whose tags include python, so a trial costs barely more than the start fee. Put several values in tags to accept any of them, or add keywords to search titles, descriptions and company names as well.

{
  "tags": [
    "python"
  ],
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
Field Type Required Default Allowed values
tags array no [] β€”
keywords array no [] β€”
company string no "" β€”
minSalary integer no β€” β€”
maxItems integer no 100 β€”
proxyConfiguration object no {"useApifyProxy": false} β€”

πŸ“€ Output

Every object is one advert, ready for JSON.stringify into a response or an INSERT into a table. Optional fields come back as null, never as 0 or an empty string, so simple truthiness checks in your templates do what you expect.

Field Type Always present
job_id string yes
slug string yes
position string yes
company string yes
company_url string or null no
location string or null no
tags array yes
salary_min integer or null no
salary_max integer or null no
description string yes
apply_url string yes
url string yes
date_posted string yes
epoch_posted integer yes
logo string or null no

πŸ’° Pricing

Event Price (USD)
Actor start $0.2
Result emitted $0.0015

Prices as configured on 2026-08-20. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.

πŸ› οΈ Working with the data in JavaScript

This module needs nothing beyond Node 20 built-ins, including global fetch. It finds postings that are absent from seen-job-ids.json, prints them newest first, posts them to the URL in WEBHOOK_URL when that variable is set, and records their IDs for the next run.

import { readFile, writeFile } from 'node:fs/promises';

const jobs = JSON.parse(await readFile('results.json', 'utf8'));
const seenFile = 'seen-job-ids.json';
const seen = new Set(JSON.parse(await readFile(seenFile, 'utf8').catch(() => '[]')));

const fresh = jobs
    .filter((job) => !seen.has(job.job_id))
    .sort((a, b) => b.epoch_posted - a.epoch_posted);
const lines = fresh.map((job) => {
    const pay = job.salary_max ? ` (top of range ${job.salary_max.toLocaleString('en-US')})` : '';
    return `${job.position} at ${job.company}${pay}: ${job.url}`;
});

if (lines.length > 0 && process.env.WEBHOOK_URL) {
    await fetch(process.env.WEBHOOK_URL, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ text: lines.join('\n') }),
    });
}
console.log(lines.join('\n') || 'No new postings since the last run.');
await writeFile(seenFile, JSON.stringify([...seen, ...fresh.map((job) => job.job_id)]));

❓ FAQ

How often should I schedule it?

Every run reads the entire live board, so frequency only changes how fast you notice new ads. Hourly suits an alert bot; daily is plenty for research. Each run costs a $0.20 start fee plus $0.0015 per posting returned, so tight filters keep frequent schedules cheap.

Can I search by location?

There is no location filter, because RemoteOK's location is free text that many ads leave empty. Filter on the location field in your own code after the run, and expect plenty of null values.

Is it safe to use from TypeScript?

Yes. apify-client includes type definitions, and the record shape is fixed, so you can declare an interface for a posting once and cast items to it after listItems().

Why did a run fail instead of returning no rows?

A failure means RemoteOK could not be reached or answered with an error after the retries ran out; the status message names the cause. A filter that simply matches nothing finishes successfully with zero items.

🧯 Troubleshooting

  • Set APIFY_TOKEN first: export the token in the same shell, or put it in a local .env file your shell loads. Never commit it.
  • Run finished with FAILED or ABORTED: open the printed Console link; the run log says why. Input validation errors name the field.
  • Fewer rows than expected: a --max-charge cap also limits how many results can be charged. Raise the cap or lower the limits in your input.
  • Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.

πŸ”— More from Devil Scrapes

Related Actors:


MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with RemoteOK (remoteok.com).

About

RemoteOK scraper for Node.js: pull current remoteok.com postings filtered by tag, keyword, company or salary into JSON for job-board backends, Slack alerts, webhooks and serverless cron jobs. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages