# We Work Remotely Scraper – Remote Jobs, Keywords & URLs (`abotapi/we-work-remotely-jobs-scraper`) Actor

Extract current remote job listings from weworkremotely.com. Use keyword search or WWR feed, category, and remote-jobs URLs. Returns clean job rows with title, company, category, region, tags, salary text, description matches, and filtered structured output.

- **URL**: https://apify.com/abotapi/we-work-remotely-jobs-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Automation, Lead generation
- **Stats:** 3 total users, 0 monthly users, 96.6% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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

Extract current remote job listings from We Work Remotely into clean dataset rows for recruiting, lead sourcing, market tracking, and job alert workflows.

The scraper supports two clear modes:

- Keyword search: search current listings by terms such as `python`, `designer`, `support`, `sales`, or `manager`.
- URL list: read specific We Work Remotely feed, category, or remote-jobs URLs.

### Why use it

- Two input modes with explicit mode selection.
- Category, region, date, salary, company website, apply link, and description-length filters.
- More than 60 fields when full descriptions are enabled.
- Normalized URLs, category slugs, job slugs, posting dates, expiry dates, salary hints, domains, section text, technologies, and seniority hints.
- Optional MCP connector export for Notion, Linear, Airtable, and Apify after the dataset is written.

### Input modes

#### Keyword search

Use keyword mode when you want matching jobs from current WWR listings.

`All categories` scans all supported WWR category feeds and deduplicates jobs by listing URL before applying your keyword and filters. This gives broader coverage than the main feed alone.

Supported category selectors:

| Category | Use for |
| --- | --- |
| All categories | Broad discovery across supported categories |
| Programming | General engineering roles |
| Front-End Programming | Front-end and UI engineering roles |
| Back-End Programming | Backend and server-side roles |
| Full-Stack Programming | Full-stack roles |
| Design | Product, UI, UX, and graphic design roles |
| Customer Support | Support and success roles |
| Sales and Marketing | Sales, growth, and marketing roles |
| Product | Product management and product operations roles |
| DevOps and Sysadmin | Infrastructure, platform, SRE, and operations roles |
| Management and Finance | Leadership, operations, finance, and admin roles |

#### URL list

Use URL mode when you already know the WWR source to read.

Accepted URL examples:

```text
https://weworkremotely.com/remote-jobs.rss
https://weworkremotely.com/categories/remote-programming-jobs
https://weworkremotely.com/categories/remote-design-jobs.rss
```

URL mode ignores keyword terms and category selectors. The URLs define the source set.

### Filters

Filters apply after the source listings are read and normalized.

| Input | What it does |
| --- | --- |
| `regions` | Keeps listings whose region contains one of your values, such as `Anywhere in the World`, `United States`, or `Europe`. |
| `postedWithinDays` | Keeps listings posted within the selected age window. |
| `requireSalary` | Keeps only rows where salary text was found. |
| `requireCompanyWebsite` | Keeps only rows with a company website URL. |
| `requireApplyUrl` | Keeps only rows with an apply URL value. |
| `minDescriptionLength` | Keeps rows whose cleaned description text is at least this long. |
| `includeDetails` | Adds full description HTML/text and parsed description sections. |
| `maxItems` | Maximum number of matching rows to save. It is a cap, not a guarantee. If the selected keyword and filters only match 11 current jobs, the dataset will contain 11 rows even when `maxItems` is 20. |

### Resume and recurring updates

Two separate features that solve two different problems.

| Input | What it does |
| --- | --- |
| `resumeFromRunId` | Continues ONE specific interrupted run. Paste that run's ID (or its dataset ID) after a run was aborted, timed out, or stopped early at its `maxItems` cap. Jobs that run already returned are matched by `jobId` and skipped, so you only pay for the remainder. Combine both datasets for the full set. An ID this account cannot read fails the run immediately rather than silently re-scraping everything. |
| `incrementalMode` | For running the same search on a schedule. The actor remembers the previous run by itself, so the first run returns everything as `NEW` and later runs return only `NEW`, `UPDATED`, and `REAPPEARED` jobs. |
| `stateKey` | Optional name for a monitoring campaign. Leave empty and the actor derives one from your mode, search terms or URLs, every filter, and `includeDetails`, so two different setups never share a baseline. |
| `emitUnchanged` | Off by default. Also returns jobs that have not changed, marked `UNCHANGED`. These are extra billed rows. |
| `emitExpired` | Off by default. Also returns jobs that were tracked before and are no longer in the feed, marked `EXPIRED`. These are extra billed rows. |

Use one or the other. Pointing `resumeFromRunId` at a search that already has a saved incremental baseline fails fast with an explanatory message, because a resumed run skips jobs on purpose and cannot tell a missing job from an already collected one. For the same reason, `EXPIRED` rows are never produced by a resumed run, nor by a run that `maxItems` capped before every selected feed was read.

In incremental mode every row carries `changeType`, `changedFields`, `firstSeenAt`, and `lastSeenAt`. Change detection ignores `scrapedAt`, `postedAgeDays`, `expiresInDays`, and `isExpired`, which move on their own with the calendar, so an untouched posting is never reported as `UPDATED` just because a day passed.

### Data You Get

Typical rows include these field groups:

| Group | Fields |
| --- | --- |
| Job identity | `jobId`, `jobSlug`, `rowType`, `title`, `rawTitle`, `companyName` |
| URLs | `url`, `canonicalUrl`, `listingDomain`, `applyUrl`, `applicationUrl`, `externalApplyUrl`, `applyUrlIsExternal`, `applyUrlDomain` |
| Company | `companyWebsiteUrl`, `companyWebsiteDomain`, `companyLogoUrl`, `headquarters` |
| Location | `region`, `regionSlug`, `locationText`, `country`, `state`, `isRemote`, `isWorldwide` |
| Category and role | `category`, `categorySlug`, `jobType`, `employmentType`, `seniorityLevel`, `skills`, `skillsRaw`, `skillsSource`, `skillsCount`, `technologies`, `tags` |
| Dates | `postedAt`, `postedAtText`, `postedAgeDays`, `expiresAt`, `expiresAtText`, `expiresInDays`, `isExpired`, `scrapedAt` |
| Salary | `salaryText`, `salaryMin`, `salaryMax`, `salaryCurrency`, `hasSalary` |
| Description | `descriptionText`, `descriptionHtml`, `descriptionLength`, `sectionHeadings`, `descriptionSections`, `aboutCompanyText`, `responsibilitiesText`, `requirementsText`, `benefitsText` |
| Source metadata | `sourceUrl`, `sourceType`, `searchKeyword`, `hasCompanyWebsite`, `externalLinks`, `externalLinkCount` |
| Change tracking (incremental mode only) | `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt` |

### Field Notes

- `maxItems` limits saved matches; it does not create more jobs than the selected source and filters currently match.
- `skills` and `skillsRaw` come from the WWR feed when present. `skillsSource` marks those values as WWR-sourced.
- `technologies` and `seniorityLevel` are derived from the listing text to make filtering and post-processing easier.
- `applyUrl` is the WWR listing URL when WWR does not expose a separate external apply URL in the feed. Use `applyUrlIsExternal` and `externalApplyUrl` to distinguish this.
- `externalLinks` captures non-WWR links found in the description, including company URLs and application links when present.
- `descriptionSections`, `aboutCompanyText`, `responsibilitiesText`, `requirementsText`, and `benefitsText` are populated when `includeDetails` is enabled and the listing text has recognizable sections.

### Example Input

Keyword search across all categories:

```json
{
  "mode": "keyword",
  "searchTerms": ["python"],
  "categories": ["all"],
  "includeDetails": true,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  },
  "mcpConnectors": [],
  "maxNotifyListings": 50
}
```

URL list:

```json
{
  "mode": "url",
  "startUrls": [
    { "url": "https://weworkremotely.com/categories/remote-design-jobs" }
  ],
  "includeDetails": false,
  "maxItems": 10,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  },
  "mcpConnectors": []
}
```

### Example Output

```json
{
  "rowType": "job",
  "jobId": "ad0db063721b33ed",
  "jobSlug": "close-senior-software-engineer-backend-python-usa-only-100-remote-1",
  "title": "Senior Software Engineer - Backend/Python - USA Only (100% Remote)",
  "companyName": "Close",
  "category": "Back-End Programming",
  "region": "Anywhere in the World",
  "jobType": "Full-Time",
  "technologies": ["Python", "AWS", "PostgreSQL"],
  "seniorityLevel": "Senior",
  "postedAt": "2026-06-10T22:30:21Z",
  "expiresAt": "2026-07-10T22:30:21Z",
  "applyUrlIsExternal": false,
  "url": "https://weworkremotely.com/remote-jobs/close-senior-software-engineer-backend-python-usa-only-100-remote-1",
  "descriptionLength": 4200,
  "sourceType": "rss",
  "searchKeyword": "python"
}
```

### Export To Your Apps

MCP connector support is included. Select one or more authorized connectors in the `MCP connectors` input field to send a compact summary of saved jobs after the dataset is written.

Supported connector targets include Notion, Linear, Airtable, and Apify.

The connector export is separate from the dataset:

- Complete records stay in the Apify dataset.
- Connectors receive a compact text summary with key fields.
- `Max jobs to export per connector` limits how many saved rows are sent to each connector.
- For Notion, set `Notion parent page` to the page URL or page ID where new pages should be created.

If no connector is selected, the run only writes the dataset.

### Proxy

Apify Proxy is enabled by default with the standard pool. You can adjust the proxy field if your run environment needs a different route.

# Actor input Schema

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

Keyword search filters live WWR job feeds by search terms. URL list reads WWR feed, category, or remote-jobs URLs.

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

Accepts WWR RSS feeds, category pages, category RSS feeds, and the main remote jobs page.

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

Search terms to match in current remote job listings.

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

Restrict keyword search to selected WWR categories. Leave empty or choose All categories to scan all supported category feeds.

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

Only keep listings whose region contains one of these values. Examples: Anywhere in the World, United States, Europe.

## `includeDetails` (type: `boolean`):

When enabled, each row includes full description HTML and cleaned description text.

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

Maximum number of matching job rows to save. The run can return fewer rows when the selected keywords and filters match fewer current listings.

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

Only keep jobs posted within this many days. Leave empty for no date filter.

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

Only keep rows where a salary hint was found in the listing text.

## `requireCompanyWebsite` (type: `boolean`):

Only keep rows with a company website URL.

## `requireApplyUrl` (type: `boolean`):

Only keep rows with an apply URL.

## `minDescriptionLength` (type: `integer`):

Only keep rows whose cleaned description text has at least this many characters.

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

ID of a previous run of this actor (or a dataset ID). Jobs already collected by that run (matched by job id) are skipped, so this run returns only the remainder, a delta. Combine both runs' datasets for the full set. Use this when a run was aborted, timed out, or stopped early at its Max items cap before every selected feed was read. Leave empty for a normal fresh run. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns all matching jobs as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED jobs. Turn on "Emit unchanged" or "Emit expired" only when you also want those jobs returned (and billed). State is kept separately per search/URL, filter, and detail-mode setup; use State key to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

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

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/URL, filters, and detail settings: different searches then never mix state with each other.

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

Off by default. Turn on to also return jobs that have not changed since the last run, marked UNCHANGED. This returns (and bills) extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

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

Off by default. Turn on to also return jobs that were present in a previous run but are no longer found in the feed, marked EXPIRED. Only produced once a run has fetched and fully scanned every requested feed, not when Max items capped it and not when Resume from a previous run was used (a resumed run deliberately skips jobs, so it cannot tell missing from already collected). This returns (and bills) extra synthetic rows, so leave it off unless you need expiry tracking.

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

Uses Apify Proxy by default with the standard datacenter pool. Adjust only if your run environment needs a different route.

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

Optionally send the scraped jobs into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → API & Integrations, then select it here. Notion gets a rich page-per-job export; other connectors receive a best-effort write/digest. Leave empty to skip; this never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

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

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

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

Cap on how many jobs are written to each selected connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "keyword",
  "startUrls": [
    {
      "url": "https://weworkremotely.com/remote-jobs.rss"
    },
    {
      "url": "https://weworkremotely.com/categories/remote-programming-jobs"
    },
    {
      "url": "https://weworkremotely.com/categories/remote-design-jobs.rss"
    }
  ],
  "searchTerms": [
    "python"
  ],
  "categories": [
    "all"
  ],
  "includeDetails": true,
  "maxItems": 20,
  "requireSalary": false,
  "requireCompanyWebsite": false,
  "requireApplyUrl": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `results` (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 = {
    "startUrls": [
        {
            "url": "https://weworkremotely.com/remote-jobs.rss"
        },
        {
            "url": "https://weworkremotely.com/categories/remote-programming-jobs"
        },
        {
            "url": "https://weworkremotely.com/categories/remote-design-jobs.rss"
        }
    ],
    "searchTerms": [
        "python"
    ],
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": []
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/we-work-remotely-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 = {
    "startUrls": [
        { "url": "https://weworkremotely.com/remote-jobs.rss" },
        { "url": "https://weworkremotely.com/categories/remote-programming-jobs" },
        { "url": "https://weworkremotely.com/categories/remote-design-jobs.rss" },
    ],
    "searchTerms": ["python"],
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": [],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/we-work-remotely-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 '{
  "startUrls": [
    {
      "url": "https://weworkremotely.com/remote-jobs.rss"
    },
    {
      "url": "https://weworkremotely.com/categories/remote-programming-jobs"
    },
    {
      "url": "https://weworkremotely.com/categories/remote-design-jobs.rss"
    }
  ],
  "searchTerms": [
    "python"
  ],
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  }
}' |
apify call abotapi/we-work-remotely-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/we-work-remotely-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/VZSxl4NFVPdu7uB8g/builds/rnmR631EMMcBhHjBv/openapi.json
