# The Muse Jobs $0.49/1K — Remote Job Board, Education Jobs (`snow_leo_data/the-muse-remote-company-jobs-scraper`) Actor

$0.49/1K | 411,812 jobs on The Muse — the public API stops at 1980 per query; this splits the search to pass it. Full job description text inline, no extra call per job. Software engineering jobs, entry level jobs, sales jobs, retail jobs, marketing jobs, customer service jobs, remote work listings.

- **URL**: https://apify.com/snow\_leo\_data/the-muse-remote-company-jobs-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.49 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## The Muse Jobs Scraper — 411K jobs, full text, past the 1,980 cap

**$0.49 per 1,000 jobs** — half the price of the other job scrapers in this
store, because this source hands over the **entire job advert text in the
search response**. No second request per job, so a run is both faster and
cheaper.

|  |  |
|---|---|
| **411,813 jobs on The Muse** | the API's own counter, read live on the day this page was last updated |
| **The public API stops at 1,980 per query** | measured: page 99 is the last with data, page 100 returns HTTP 400, and the page size is fixed at 20 |
| **This Actor goes past it** | measured: 2,060 unique jobs in 176 s from one run, zero duplicates |
| **27 fields** | city, state and country parsed out of the location; emails and links pulled from the advert body |
| **Full advert inline** | 5,000-character bodies arrive with the listing, not behind a second call |

### The 1,980 wall

Ask for page 100 and the API answers **HTTP 400**. The page size is fixed at 20
no matter what you pass — send items\_per\_page=50 and you still get 20 rows back.
So one query can never hand you more than **1,980** jobs, out of 411,813. A
scraper that simply pages through a search reaches **0.48%** of this curated
job board.

This Actor splits a wide search until every slice fits under the wall: first by
category, then by seniority, then by location. The values are read from the
source's own results — The Muse publishes no taxonomy endpoint, so inventing a
category list would mean inventing data. Slices overlap; every job is
deduplicated by id during the walk.

```
walk: 2060 unique jobs in 176 s, 30 queries, split by category, level, location
```

### Who this is for

**Anyone building a job board.** The advert text arrives with the listing, so a
posting can be republished without a second fetch, and the landing page URL is
right there.

**Recruiters watching a market.** Turn on *Only jobs not seen before*: delivered
jobs are remembered in a named store that survives between runs, and a daily
schedule charges you only for what appeared since yesterday.

**AI and agent pipelines.** *Compact output* returns 14 core fields and no advert
body — the shape an LLM actually consumes — and *Drop empty fields* removes what
the employer never filled in.

### Quick start

Press **Start**. With no filters you get a 300-job sample of the newest listings.

```json
{
  "categories": ["Software Engineering"],
  "levels": ["Senior Level"],
  "locations": ["New York, NY"],
  "maxItems": 0
}
```

### What a row lets you filter on

27 fields per job; the full list is under **Fields in detail**. Three of them
decide what a run is worth to you. The Muse publishes a location as one string,
and this Actor turns it into job location data — city, state and country as
separate fields, by a rule spelled out further down, so New York jobs separate
from New Jersey without matching strings. Job level data is the board's own
seniority band rather than a reading of the title, and job tags data is the set
of labels the employer itself attached to the advert.

***

### Job category data and seniority, as fields

The Muse files every advert under exactly one category and tags it with a
seniority band, so job category data and job seniority data arrive as fields
instead of as something you infer from the title. That is what turns a slice
like internship listings in software engineering, or remote job listings in
healthcare, into a filter rather than a keyword guess.

Internship listings here are the board's own Internship tag, not adverts that
happen to contain the word intern: internship jobs data and senior jobs data are
two values of the same `level` column, so one filter gives you either. Remote
job listings are the ones whose location entry reads Flexible / Remote, not the
ones whose advert text says "no remote work".

The Muse sells employer branding — companies pay to be profiled here — so what
comes back is employer branding data as much as a job feed: the employer list is
self-selected, which is why tech company jobs and large-employer adverts are
over-represented and small local shops are not here at all.

#### Remote as a field, not a filter

`is_remote` is a column on every row, so a run doubles as remote work data:
which companies post remote roles, in which of the 32 categories and at which
seniority. Filter it after the fact instead of guessing from the location
string: work from home data and US remote jobs are a cut across the whole board,
not a corner of it. Remote hiring data therefore needs no separate run — it is
one column on the rows you already paid for.

Category names are The Muse's own, not ours: data science jobs sit under
Data and Analytics, social work under Social Services, and remote work jobs
are a column rather than a category. The rest of the map is just as hard to
guess — sales jobs data lives under Sales and under Account Management,
marketing jobs data under Advertising and Marketing, designer jobs data under
Design and UX, operations jobs data under Business Operations, HR jobs data
under Human Resources and Recruitment, finance jobs data under Accounting and
Finance, healthcare jobs data under Healthcare, and engineering jobs data under
Science and Engineering. Ask for a name the board does not use and you get nothing back rather
than a guess, and `Unknown` is one of the 32 real names, not a parsing failure.

Every run leaves a job listings dataset — JSON, CSV or Excel — with the job
listings full text already in it, so the job posting text is in the same row as
the title rather than behind a second call: the advert arrives inside the search response,
so nothing is fetched twice.

### How does this Actor get past the cap?

#### Why a Max jobs setting cannot help you

Raising a limit does not raise the ceiling. The ceiling belongs to the source:
the public search endpoint refuses page 100 and pins the page size at 20, so a
single query is worth 1,980 rows and no input value changes that. Ask one broad
query for fifty thousand jobs and you still get 1,980, then silence. The only
way through is to stop asking one broad question.

#### What the Actor does instead

Before paging, the Actor reads how many jobs the current query claims. If that
number is under the cap, it simply pages the query out. If it is over, the query
is split along the next unused axis — category first, then seniority, then
location — and each child query is measured the same way. The walk repeats until
every leaf fits, which is why one run can return far more than any single search
could.

The values used for splitting are not a hard-coded list. The Actor samples the
first three pages of the current query and collects the category, level and
location names that the returned jobs actually carry. The Muse has no taxonomy
endpoint to read, and a hand-written list would drift out of date the moment the
source added a category.

#### What happens to jobs that appear in two slices

They are returned once. A job filed under both *Data and Analytics* and
*Software Engineering* shows up in both slices, so the walk keeps the set of ids
it has already yielded and drops the repeat before it reaches the dataset — which
means before it reaches your bill. The measured run above ended with zero
duplicates across 30 queries.

The walk also has a hard stop of 2,500 HTTP requests per run, so a pathological
split cannot spin forever. When it trips, the run report says so instead of
pretending the board was exhausted.

### Fields

| Field | What it is |
|---|---|
| `title`, `company` | headline and employer |
| `location`, `location_city`, `location_state`, `location_country` | the label split up — `Jacksonville, FL` becomes city, state and `United States` |
| `is_remote` | taken from the job's own location entry, never guessed from the text |
| `category`, `level`, `employment_type`, `tags` | how The Muse files the job |
| `description_text`, `description_markdown`, `description_html` | the full advert in three forms; pick one to keep rows small |
| `contact_emails`, `contact_urls` | pulled out of the advert body |
| `published_at`, `posted_days_ago` | when it appeared |
| `change_type` | in incremental runs: NEW, UPDATED or UNCHANGED |
| `url` | the listing page |

Empty means the source did not publish it. `location_country` stays empty rather
than being filled with a guess when the label carries only a city.

#### How the location label is taken apart

The Actor splits the label on commas and reads the tail: a two-letter code means
a US state, and the country is then set to `United States`; anything else is
treated as the country itself. So `Jacksonville, FL` yields a city, a state and
a country, while `Munich, Germany` yields a city and a country with
`location_state` left empty.

Nothing is inferred beyond that rule. A bare `Flexible / Remote` stays in
`location` as written, and `locations_all` keeps every place the employer
listed, not just the first.

### Fields in detail

#### Three shapes of the same advert

The source ships the advert as HTML. The Actor keeps that original in
`description_html`, renders a readable plain-text version into
`description_text`, and converts headings, lists, links, bold and italic into
`description_markdown`. Markdown is usually the right choice for an LLM prompt;
plain text is the cheapest to store.

All three together roughly triple the weight of a row, which is why the
*Description format* input defaults to plain text and drops the rest. If you
need every form, choose *All three* deliberately rather than by accident.

#### Contacts out of the advert body

Some employers put a hiring manager's address or an application link straight
into the advert. The Actor scans the plain-text body and returns whatever it
finds in `contact_emails` and `contact_urls`, deduplicated and stripped of
trailing punctuation. Most jobs have neither; the *Contacts* dataset view exists
for the ones that do.

#### What is deliberately missing

There is no salary field. The Muse does not publish structured pay on the public
jobs endpoint, and a number guessed out of the advert text by a model is a
number you cannot use. Rather than ship a column that is wrong often enough to
matter, this Actor ships no column at all — if pay figures are in the body, the
body is right there in `description_text`.

### Remote is a location, not a word

*Remote only* uses the job's own location entry. Searching the advert body for
"remote" instead would match "no remote work" just as happily —
`tests/test_logic.py` keeps a trap job for exactly that case.

### Filters run before you are charged

Keywords, exclusions, country, category, seniority, remote, posting age,
employer allow and block lists. A row a filter removes never reaches the dataset
and never appears on the bill. Dataset views can only choose columns — no view
on Apify can drop rows — so the filtering lives in the input, where it is also
cheaper. See the
[dataset documentation](https://docs.apify.com/platform/storage/dataset) for what
a view can and cannot do.

The run report records how many rows each filter removed, so a surprisingly
small result is explainable rather than mysterious.

### Running it on a schedule

#### How do I pay only for what is new?

Turn on *Only jobs not seen before* and attach a
[schedule](https://docs.apify.com/platform/schedules). The Actor keeps the ids
and a short content fingerprint of everything it has delivered in a **named**
key-value store, which — unlike the default store — survives from one run to the
next. Tomorrow's run returns the jobs that appeared since today and marks them
`NEW`, and you are billed for those alone.

The memory holds 150,000 keys. A key weighs about 25 bytes, and one record in a
[key-value store](https://docs.apify.com/platform/storage/key-value-store) is
capped at 9 MB; 300,000 keys measured 8.0 MB, close enough to the ceiling that
half of it is kept in reserve. When the memory fills, the oldest keys are
dropped first, never random ones.

#### What counts as a change

The fingerprint covers the title, the employer and the first 2,000 characters of
the advert. A job whose text was rewritten comes back as `UPDATED`; a job that
merely sat there comes back as `UNCHANGED` and, by default, is not returned at
all — you already paid for it once. Switch on *Also return unchanged jobs* if
your pipeline would rather see the full picture every morning.

#### Putting the output somewhere useful

The dataset can be pushed to Google Sheets, Slack, a webhook or your own
database through the platform's
[integrations](https://docs.apify.com/platform/integrations) and
[webhooks](https://docs.apify.com/platform/integrations/webhooks), or pulled
directly from the [Apify API](https://docs.apify.com/api/v2). For agent
pipelines, the store's [MCP server](https://docs.apify.com/platform/integrations/mcp)
can call this Actor as a tool — *Compact output* is there precisely for that.

### Questions people ask

#### How many jobs can one run return?

There is no cap in the Actor itself. The measured run returned 2,060 unique jobs
in 176 seconds, which is already above what a single query of the source can
produce, and the walk keeps splitting as long as slices remain. What bounds a
real run is your *Max jobs* value, your spending limit and the 2,500-request
ceiling on the walk, in that order.

#### Why does the public API refuse page 100?

That is the source's rule, not a guess. Page 99 returns 20 jobs, page 100
returns HTTP 400, and the page size ignores whatever you pass — send fifty and
twenty come back. Twenty rows times ninety-nine pages is the 1,980 ceiling every
naive The Muse scraper runs into, including the ones that do not tell you.

#### Does it fetch the full advert text?

Yes, and without a second request. The Muse returns the advert body inside the
search response itself, which is unusual and is the reason this Actor is priced
below the rest of the category. Bodies of about 5,000 characters arrive with the
listing, already converted to plain text and Markdown as well as the original
HTML.

#### What does a first run cost?

Press Start with nothing filled in and the run stops after 300 jobs — about
fifteen cents at $0.49 per 1,000. That default exists so an empty form is a
trial and not an accidental order for four hundred thousand rows. Set *Max jobs*
to 0 once you know what you want.

#### What happens if I set a spending limit?

The Actor reads the limit at start and refuses to return more rows than it
covers. This matters because the platform stops charging when a limit is
reached but does not stop the run, so an Actor that ignored the limit would
keep scraping at the developer's expense and yours in compute. See
[pay-per-event pricing](https://docs.apify.com/platform/actors/publishing/monetize)
for how the charging model works.

#### How do I get only new jobs each morning?

Set your filters, switch on *Only jobs not seen before*, save the configuration
as a task and give it a daily schedule. The first run fills the memory and bills
for everything it returns; every run after that returns and bills for the
difference. Leave *Also return unchanged jobs* off unless you need the full set
each time.

#### Can I feed this straight into an LLM?

Turn on *Compact output* for 14 core fields with no advert body, or keep the
body and set *Description format* to Markdown so headings and lists survive the
trip into a prompt. *Drop empty fields* removes keys the employer never filled
in, which shortens the JSON an agent has to read and cuts the token bill.

#### Why are some fields empty?

Because the employer left them empty at the source. Nothing here is back-filled
by inference: a job with no seniority stated returns an empty `level`, a
location with no country in it returns an empty `location_country`, and an
advert with no email in it returns an empty `contact_emails`. An empty cell is
information — a wrong cell is not.

#### Does it need a proxy?

There is no proxy input, and every number quoted on this page was measured
without one. The client retries on failure, honours the source's Retry-After on
HTTP 429, and applies that cooldown across all threads at once so a slowdown
request is not undone by the rest of the run hammering the same host.

#### How do I filter by country?

Use *Only these countries* with the country as it appears after parsing —
`United States`, `Germany`. Because the country is derived from the location
label rather than a field the source publishes, a job whose label is only a city
has no country and will not pass a country filter. *Location contains* is the
looser tool for those cases.

### Output views

The dataset arrives with four views, so the same run can be read four ways: the
default **Jobs** table, **Location** for the parsed city, state, country and
remote flag, **Contacts** for the addresses and links found in advert bodies, and
**All fields** for everything including both extra description formats. A view
selects columns only — it never removes rows, and it never changes the bill.

### Honest limits

- **No salary column**, for the reason given above.
- **Splitting is by category, seniority and location** — those are the axes the
  source exposes on the search endpoint. A query that stays above the cap even
  after all three are used is reported as capped, with the number of jobs it
  could not reach, rather than quietly truncated.
- **The category list in the input is a snapshot** of what the source was
  serving when it was taken; The Muse publishes no endpoint that lists them.
- **`posted_days_ago` is computed in UTC** from the publication timestamp, so a
  job posted late in the day may read as one day old sooner than you expect.

### Jobs this board does not carry

The Muse is a curated board: employers pay to be listed, so it is deliberately
narrower than the open market. Three sibling Actors cover the vacancies that
never reach it, and each stamps its rows with a `source` field so their datasets
can be concatenated and still be told apart:

- [ATS Jobs Scraper](https://apify.com/snow_leo_data/greenhouse-workday-lever-ashby-ats-jobs-scraper)
  — straight from the employer's own careers page across 20 applicant tracking
  systems, which is where a listed employer's remaining vacancies sit.
- [Jobs.ch Scraper](https://apify.com/snow_leo_data/jobs-ch-scraper-swiss-switzerland-jobs)
  — Switzerland.
- [SEEK, JobStreet & JobsDB Scraper](https://apify.com/snow_leo_data/seek-jobstreet-jobsdb-australia-jobs-scraper)
  — Australia, New Zealand and six Asian markets.

#### A run succeeded but the dataset is empty — is that a failure?

Not necessarily, and the Actor always says which case it is. The run report
carries `empty_reason` and the log carries a warning naming it:
`nothing-changed` (the incremental mode working as asked — everything had
already been delivered by an earlier run, so nothing was written and nothing
was charged), `filtered-out` (your filters removed everything that was
collected), `source-returned-nothing` (the search matched nothing at all), or
`charge-limit-reached` (the run's spending limit was already used up). A silent
empty dataset is treated as a defect, not as a normal outcome.

### Reproducing the numbers

```
python3 tests/run_all.py --live
```

Every figure above comes from that run.

# Actor input Schema

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

Leave empty for all. The Muse files every job under exactly one of these, from software engineering to hospitality jobs.

## `levels` (type: `array`):

Leave empty for all.

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

Uses the job's own location entry, not a word search in the advert — looking for "remote" in the text also matches "no remote work".

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

As the site spells them — `New York, NY`, `London, United Kingdom`, `Flexible / Remote`.

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

Exact employer names as The Muse spells them, e.g. `Bank of America`.

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

Checked against title, advert text and tags.

## `excludeKeywords` (type: `array`):

Drops a job if any of these appears in its title or advert text.

## `countries` (type: `array`):

Matched against the parsed country, e.g. `United States`, `Germany`.

## `locationContains` (type: `string`):

Extra text match across all of a job's locations.

## `categoryContains` (type: `array`):

Substring match, narrower than the picker above.

## `levelContains` (type: `array`):

Substring match on the level name.

## `companyContains` (type: `array`):

Substring match on the employer name.

## `excludeCompanies` (type: `array`):

Substring match. Useful for leaving out staffing agencies.

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

0 = no date filter.

## `withDescriptionOnly` (type: `boolean`):

Drops listings whose body is empty at the source.

## `descriptionFormat` (type: `string`):

Three representations triple the row size. Keep the one you use.

## `compactOutput` (type: `boolean`):

14 core fields only, no advert body — for AI agents and MCP pipelines.

## `excludeEmptyFields` (type: `boolean`):

Leave out what the employer did not publish instead of returning it empty.

## `newestFirst` (type: `boolean`):

Sort by publication date.

## `onlyNew` (type: `boolean`):

For schedules. Delivered jobs are remembered in a named store that survives between runs, so a daily run returns — and charges for — only what appeared since yesterday.

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

Off by default: an unchanged job is something you already paid for.

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

0 = no limit. The form starts at 300 so a first run is quick and cheap.

## Actor input object example

```json
{
  "categories": [],
  "levels": [],
  "remoteOnly": false,
  "locations": [],
  "companies": [],
  "keywords": [],
  "excludeKeywords": [],
  "countries": [],
  "locationContains": "",
  "categoryContains": [],
  "levelContains": [],
  "companyContains": [],
  "excludeCompanies": [],
  "postedWithinDays": 0,
  "withDescriptionOnly": false,
  "descriptionFormat": "text",
  "compactOutput": false,
  "excludeEmptyFields": false,
  "newestFirst": true,
  "onlyNew": false,
  "emitUnchanged": false,
  "maxItems": 300
}
```

# Actor output Schema

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

Every job collected in this run.

## `report` (type: `string`):

How the query was split past the cap and what the filters removed before billing.

# 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 = {
    "maxItems": 300
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/the-muse-remote-company-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 = { "maxItems": 300 }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/the-muse-remote-company-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/bEZvhvESMdB4Zjopq/builds/uyHnKMQBMo9N7a333/openapi.json
