# We Work Remotely Scraper - Expiry & Ghost Job Filter (`rolescout/weworkremotely-jobs-scraper`) Actor

Scrape the We Work Remotely feed into a clean JSON or CSV job dataset, splitting company from title. Real expiry dates make the freshness and ghost-job scoring evidence-based here: expired and stale job postings are detected outright, not guessed from age.

- **URL**: https://apify.com/rolescout/weworkremotely-jobs-scraper.md
- **Developed by:** [Dave Fergins](https://apify.com/rolescout) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 6 total users, 4 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.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?

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

## We Work Remotely Jobs Scraper

**Scrape the We Work Remotely feed into a clean JSON or CSV job dataset, splitting company from title. Real expiry dates make the freshness and ghost-job scoring evidence-based here: expired and stale job postings are detected outright, not guessed from age.**

### Two things this board does that break naive scrapers

**1. There is no company field.** We Work Remotely publishes an RSS feed where the employer is glued to the front of the title: `"Fullscript: Senior Product Designer"`. A scraper that maps `title` straight through gives you a company column that does not exist and a job title that starts with a company name.

This Actor splits them — but only on the first colon, and only when both halves are plausible. Titles legitimately contain colons (`"Engineer: Platform Team"`), and splitting those would invent a company that does not exist.

**2. It publishes real expiry dates.** Along with Himalayas, this is one of only two boards here that states when a listing closes. That makes the ghost-job scoring unusually well-grounded: an expired posting is **detected outright** rather than guessed at from age.

Region coverage is strong too — around 96% of listings resolve to a hiring region, since the feed carries separate region, country and state fields rather than one free-text blob.

***

### What each row contains

| | |
|---|---|
| **Identity** | stable `id` across runs, posting URL, direct `applyUrl` where published |
| **The role** | title, company, plain-text description, tags, employment type, seniority |
| **Where** | `remote` flag, the board's own location text, and normalised `regions` (worldwide / usa / canada / latam / uk / europe / apac / africa / middle\_east) |
| **Pay** | min, max, currency and period as numbers, annualised on request |
| **When** | posted date, expiry where published, age in days |
| **Trust** | `freshness` 0-1, `ghostRisk` low/medium/high, and `ghostReason` explaining the verdict in plain words |

#### The trust fields

Job boards are full of postings still published but no longer open — filled roles left up for pipeline, evergreen "talent pool" adverts, and listings nothing ever expires. Every row here carries a risk band and the reasons behind it:

```json
{
  "title": "Senior Backend Engineer",
  "company": "Acme",
  "freshness": 0.71,
  "ageDays": 10.4,
  "ghostRisk": "low",
  "ghostReason": ["recent, and nothing contradicts it"]
}
```

Signals come from what the board actually publishes: age, its own expiry date where there is one, missing application links, and evergreen phrasing. Nothing is inferred by a model. Freshness decays on a 21-day half-life, and a posting with **no** date scores 0.5 rather than 1.0 — absence of evidence is not evidence of freshness.

### Example input

```json
{
  "query": ["golang", "backend"],
  "regions": ["usa"],
  "seniority": ["senior", "lead"],
  "maxAgeDays": 21,
  "maxGhostRisk": "low",
  "maxItems": 200
}
```

Everything is optional — run it empty and you get the whole board, best-first.

- **Search terms are OR-ed.** `["go", "rust"]` returns jobs mentioning either.
- **Worldwide jobs match every region filter**, because a job open to everyone is open to you.
- **`minSalary` is annualised first**, so hourly and monthly rates compare correctly.
- **`maxItems` is your cost ceiling** — you are billed per job returned.

### Output

One dataset item per job, ordered best-first: lowest ghost-job risk, then freshest. Export as JSON, CSV or Excel, or read it from the API like any Apify dataset.

### Common uses

- **Filtering out expired and stale job postings.** This board publishes real expiry dates, so `ghostRisk`, `ghostReason` and `freshness` are unusually well grounded here — an expired posting is detected outright rather than inferred from age.
- **Job posting freshness checks.** If your question is "is this listing still open?", this is the board where the answer is evidence-based.
- **Salary benchmarking and compensation data.** Where this board publishes pay, `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryPeriod` come through as numbers, and `minSalary` annualises before comparing.
- **Job market and hiring data.** Run it on a schedule and track how hiring, salary ranges, seniority mix and regions move over time.
- **Building a job board, job feed or job alerts.** `id` is stable across runs, so diffing today's dataset against yesterday's gives you genuinely new jobs rather than a board reshuffle.
- **Recruitment and talent research.** Company, title, tags, seniority, employment type and normalised regions on every row.

Export as **JSON, CSV or Excel**, or read the dataset straight from the Apify API.

### How it fetches

We Work Remotely publishes a public RSS feed and this Actor reads it — paced, budgeted, with an honest user agent. No HTML scraping, no bot-check evasion, no personal data. Job adverts only.

### Want more than one board?

**[Remote Jobs Aggregator](https://apify.com/wfh_scout/remote-jobs-aggregator-multi-board-salary)** runs this same pipeline across six boards at once — Remote OK, Remotive, Himalayas, Arbeitnow, We Work Remotely and Jobicy — folding duplicates and recording which boards carry each job.

Measured while building it: those six boards barely overlap. Only **1 of 1,513** company+title pairs appeared on more than one. The aggregator is not about removing duplication — there is almost none — it is about getting six boards' worth of distinct jobs from one call.

# Actor input Schema

## `query` (type: `array`):

Keywords matched against job title, company and tags. Terms are OR-ed, so \["go", "rust"] returns jobs mentioning either. Leave empty to get everything.

## `regions` (type: `array`):

Restrict to these hiring regions. Jobs open worldwide always match, whatever you pick here.

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

Rows whose type We Work Remotely never stated are excluded when you set this.

## `seniority` (type: `array`):

Taken from the board's own seniority field where it publishes one, falling back to the job title.

## `maxGhostRisk` (type: `string`):

Drop postings that look expired, stale or evergreen. Every row carries the reasons behind its rating, so you can filter on those instead if you disagree.

## `maxAgeDays` (type: `integer`):

Drop postings older than this. Rows whose posting date the board never published are also dropped, because their age cannot be checked. 0 means no age limit.

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

Annualised before comparison, so hourly and monthly rates are handled correctly. Only applies to rows that publish pay.

## `salaryOnly` (type: `boolean`):

Keeps only rows where a salary could be read.

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

This board lists remote work only, so this changes nothing here. Kept for consistency across the family.

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

You are billed per job returned, so this is your cost ceiling. 0 means no cap.

## `maxPagesPerSource` (type: `integer`):

We Work Remotely serves its whole feed in one call, so this setting has no effect for this board.

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

Turn off for a much smaller dataset when you only need titles, companies and links.

## `descriptionMaxLen` (type: `integer`):

Characters. Descriptions are plain text, never raw HTML. Use -1 for no cap.

## Actor input object example

```json
{
  "query": [
    "golang",
    "backend"
  ],
  "regions": [],
  "employmentTypes": [],
  "seniority": [],
  "maxGhostRisk": "high",
  "maxAgeDays": 14,
  "minSalary": 0,
  "salaryOnly": false,
  "remoteOnly": true,
  "maxItems": 200,
  "maxPagesPerSource": 10,
  "includeDescription": true,
  "descriptionMaxLen": 4000
}
```

# Actor output Schema

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

All matching jobs as JSON. Each row carries the role, company, normalised hiring regions, parsed salary, posting age, and a ghostRisk band with the plain-language reasons behind it.

## `jobsCsv` (type: `string`):

The same rows as CSV, for spreadsheets and tools that do not read JSON. Columns are ordered for reading — job, company, ghost risk, then the rest. Use the JSON link above for every field including the full description.

## `shortlist` (type: `string`):

The same rows, trimmed to the columns you scan when shortlisting: role, company, regions, top salary, age, ghost risk and the apply link. Rows are ordered lowest ghost-risk first; to keep only the low-risk ones, set maxGhostRisk to "low" on the run.

# 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("rolescout/weworkremotely-jobs-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 = {}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rolescout/weworkremotely-jobs-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/krggfaDYr0Xpy9e7U/builds/gYYLg5PKSJan3DFp1/openapi.json
