# HelloWork Scraper - France Jobs, Experience & Skills (`neverempty/hellowork-jobs`) Actor

Every job on HelloWork with what the employer actually requires: years of experience, education level and the skills asked for - on every listing measured, not just some. Plus company, city with postal code, employment type, posted and expiry dates, and the salary when published. Unofficial.

- **URL**: https://apify.com/neverempty/hellowork-jobs.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Jobs, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.73 / 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

## HelloWork Scraper - France Jobs, Experience & Skills

Pull jobs from **HelloWork** with the part most job feeds leave out: **how many years of experience the employer wants**, **what education level**, and **the exact skills asked for** - present on every listing measured, not on a lucky few.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by HelloWork. All product names are trademarks of their respective owners.*

No API key. No proxy needed. Reads the same public pages a visitor sees.

***

### Why this one is different

#### The salary is honest about how often it exists

Measured on 20 live listings: **only 7 (37%) publish a salary**. French employers frequently do not.

So a job with no published salary comes back with `salaryMin: null` - **not `0`**. A zero would quietly turn into "€0 per year" in a spreadsheet, which is worse than an empty cell. The currency and the period (`year`, `month`, `hour`) are taken from what HelloWork actually states; when it states nothing, `salaryPeriod` is `null` rather than a guess.

#### What the employer requires, on every job

The same 20 listings, measured:

| field | how often it is filled |
|---|---|
| `experienceMonths` | **100%** |
| `educationRequirements` | **100%** |
| `skills` | **100%** |
| `salaryMin` / `salaryMax` | 37% |

That is the reason to use this one: you can filter French roles by required experience and education without opening a single advert.

#### An empty search is an answer, not a silent fallback

Measured: `?l=paris` narrows `developpeur` from **834 to 206** jobs. A city HelloWork does not have returns **zero results** - it never falls back to every job and lets you think you filtered. A keyword it does not have does the same.

When that happens you get **one `no-results` row saying so**, and it is not charged. A page that could not be read at all is a different row (`unreadable`) carrying the HTTP status.

#### Read from the structured data, not from the layout

Each job is taken from the **JSON-LD `JobPosting`** block HelloWork publishes for search engines - 21 fields, present on 19 of 20 pages measured. That block is maintained for Google, so it survives visual redesigns that break HTML scrapers. HTML tags are stripped from every text field before you get it.

***

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `keyword` | string | `developpeur` | French keyword: `commercial`, `comptable`, `infirmier`… |
| `city` | string | — | `paris`, `lyon`, `marseille`… (834 → 206 for Paris) |
| `maxJobs` | integer | 30 | 1-1000. One job = one row = one charged event |
| `includeDescription` | boolean | `true` | Full advert text, HTML stripped |
| `maxPages` | integer | 0 | Safety cap. 0 = decide from `maxJobs` (30 per page) |
| `keywords` | array | — | Keep only jobs containing one of these words |
| `keywordMatch` | string | `any` | `any` or `all` |
| `excludeKeywords` | array | — | Drop jobs containing one of these |
| `companyNames` | array | — | Keep only these employers (partial match) |
| `minSalary` | integer | 0 | **A job with no published salary is kept** |
| `requireSalary` | boolean | `false` | Drop the ~63% that hide the salary |
| `postedWithinDays` | integer | 0 | 0 = no limit. **An unreadable date is kept** |
| `monitoringMode` | boolean | `false` | Return only jobs not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

#### Filters are applied before you are charged

The filters narrow the jobs already read - **nothing extra is fetched from HelloWork** - and a job that does not match is never delivered and never charged. If a search returns jobs but none match your filters you get **one unpaid row saying exactly that** (`no-filter-match`), so you can tell "the filter was too tight" apart from "HelloWork has nothing".

Two of them refuse to guess. Only **37% of French listings publish a salary**, so `minSalary` **keeps** a job whose salary is unknown rather than treating it as low - otherwise the filter would silently hide most of the market. A job with an **unreadable posted date is kept** by `postedWithinDays` for the same reason. Turn on `requireSalary` when you really do want only the priced ones.

#### Monitoring mode turns this into a daily feed

Schedule it with `monitoringMode: true` and each run returns **only the jobs it has not returned before**. Already-seen jobs are skipped **before their pages are opened**, so a repeat run is fast as well as free. The first run returns everything and remembers it.

The minimal call is an empty input: 30 developer jobs across France.

***

### Output columns

| Column | Example |
|---|---|
| `jobId` | `80665390` |
| `url` | the job page |
| `title` | `Développeur H/F` |
| `company` / `companyUrl` | `Akanea Développement` |
| `city` / `region` / `postalCode` | `La Penne-sur-Huveaune` / `Provence-Alpes-Côte d'Azur` |
| `salaryMin` / `salaryMax` | `32000` / `40000` — or `null` when not published |
| `salaryPeriod` / `salaryCurrency` | `year` / `EUR` |
| `employmentType` | `["FULL_TIME"]`, `["TEMPORARY","FULL_TIME"]` |
| `experienceMonths` | `12` — the months of experience asked for, as a number |
| `educationRequirements` | `["bachelor degree"]` — always an array |
| `skills` | `["SQL", "WinDev", …]` — always an array |
| `qualifications` | the advert’s own wording, HTML stripped |
| `industry` / `occupationalCategory` | HelloWork's own classification |
| `datePosted` / `validThrough` | `2026-09-02T18:21:34Z` / … |
| `directApply` | `true` when you can apply on HelloWork itself |
| `description` | full advert text, HTML stripped |
| `status` / `note` | only on rows that carry no job, with the reason |

***

### Measured, not estimated

Checked against the live site on 2026-09-03:

- `developpeur` returns **834 jobs**, 30 per page; page 2 shares only 2 with page 1.
- `?l=paris` → **206 jobs**. A city that does not exist → **0 results**, not a full list.
- **JSON-LD `JobPosting` on 19 of 20 pages** (21 fields).
- **Salary on 7 of 20 (37%)**; experience, education and skills on **20 of 20**.
- `https://www.hellowork.com/robots.txt` has **no `User-agent: *` section at all**.

The Actor ships with **15 checks** run against saved copies of a listing page, a job with a salary and a job without one, plus a **mutation suite of 21 deliberate breakages - all 21 are caught**.

***

### Typical uses

- Filter French roles by required experience or education without reading adverts.
- Track which skills a role asks for across a city or a whole sector.
- Build a salary picture where one exists, without pretending the other 63% are zero.
- Feed a job board or a newsletter with fresh French roles and their expiry dates.

***

### Notes and limits

- HelloWork lists French jobs. Keywords are in French; `developer` finds far less than `developpeur`.
- Salaries are in EUR and exist on roughly a third of listings (measured 37%).
- `description` is the advert text with HTML removed, not the original markup.
- Some employers publish only a region, not a town (measured: 1 in 8). Those rows have `city: null` and `region` filled - the town is not invented from the region.

### Other tools by NeverEmpty

- **[Reed Jobs Scraper - UK Salaries, Remote & Expiry](https://apify.com/neverempty/reed-jobs)**
- **[Internshala Scraper - India Jobs & Internships, Salary](https://apify.com/neverempty/internshala-jobs)**
- **[Instahyre Scraper - India Tech Jobs & Company Data](https://apify.com/neverempty/instahyre-jobs)**

### Support

Found a row that looks wrong? Open an issue on the Actor page with the `url` from the row.

# Actor input Schema

## `keyword` (type: `string`):

What to search for, in French: `developpeur`, `commercial`, `comptable`, `infirmier`. HelloWork returns zero results for a word it does not have - you get one row saying so instead of a silently unfiltered list.

## `city` (type: `string`):

Filter by city, for example `paris`, `lyon`, `marseille`, `toulouse`. Measured on 2026-09-03: this narrows `developpeur` from 834 jobs to 206, and a city HelloWork does not have returns zero results rather than every job.

## `maxJobs` (type: `integer`):

How many jobs to return at most. One job = one row = one charged event. HelloWork lists 30 jobs per page. A job that appears again on a later page is dropped, so it is never charged twice. Pages that could not be read are returned as their own rows and are never charged.

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

Default: true. The full advert text with HTML stripped. Turn it off for a smaller dataset - the price is the same either way.

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

Safety cap on how many listing pages to walk. 0 lets the Actor decide from Maximum jobs (30 per page).

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

Keep only jobs whose title, description, employer, industry, skills or location contains one of these words. Case is ignored. Nothing extra is fetched from HelloWork - this narrows the jobs already read, and jobs that do not match are never charged.

## `keywordMatch` (type: `string`):

`any` keeps a job that contains at least one keyword. `all` keeps only jobs that contain every keyword.

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

Drop any job containing one of these words - for example `alternance`, `stage`, or an agency you do not want. Applied after the keyword filter.

## `companyNames` (type: `array`):

Keep only jobs from employers whose name contains one of these (partial match, case ignored). Leave empty for all employers.

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

Keep only jobs paying at least this much, in the currency and period HelloWork publishes. A job with no published salary is kept, not dropped - an unknown salary is not a low one. Measured on 2026-09-03: only 37% of French listings publish a salary, so dropping them would silently hide most of the market.

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

Default: false. Turn it on to drop the roughly 63% of French listings that hide the salary.

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

Keep only jobs posted in the last N days. 0 means no limit. A job whose posted date cannot be read is kept, not dropped.

## `monitoringMode` (type: `boolean`):

Default: false. Turn it on and schedule this Actor daily: each run returns only the jobs it has not returned before. Already-seen jobs are skipped before their pages are opened, so repeat runs are fast as well as free.

## `resetMonitoringState` (type: `boolean`):

Default: false. Turn it on for a single run to forget which jobs were already returned and start monitoring again from scratch.

## Actor input object example

```json
{
  "keyword": "developpeur",
  "city": "",
  "maxJobs": 30,
  "includeDescription": true,
  "maxPages": 0,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "companyNames": [],
  "minSalary": 0,
  "requireSalary": false,
  "postedWithinDays": 0,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per job: title, company, city and region with postal code, the salary when the employer published one, the experience and education required, the skills asked for, employment type, posted date and expiry date, and the full advert text. Searches with no results and pages that could not be read come back as their own rows and are not charged.

# 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 = {
    "keyword": "developpeur"
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/hellowork-jobs").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 = { "keyword": "developpeur" }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/hellowork-jobs").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 '{
  "keyword": "developpeur"
}' |
apify call neverempty/hellowork-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/hellowork-jobs"
        }
    }
}

```

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/Hpgepk12BVrhXFKGy/builds/S18hgdvt7lWGRc3gf/openapi.json
