# Swiss Jobs Scraper (Job-Room / arbeit.swiss) (`normdata/jobroom-scraper`) Actor

Scrape every open job in Switzerland from Job-Room (arbeit.swiss): 80,000+ jobs with recruiter email and phone, salary when stated, GPS and required languages. Search by keyword, occupation or distance around any town, skip staffing agencies, and get alerts on new jobs.

- **URL**: https://apify.com/normdata/jobroom-scraper.md
- **Developed by:** [Norm Data](https://apify.com/normdata) (community)
- **Features:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

![Norm Data](https://raw.githubusercontent.com/FabriAV/normdata/refs/heads/main/assets/banner_norm2.png)

## 🇨🇭 Swiss Jobs Scraper (Job-Room / arbeit.swiss)

Get every open job on **Job-Room**, Switzerland's public job platform run by SECO and the regional employment centres (also known as arbeit.swiss, travail.swiss and lavoro.swiss): more than 80,000 jobs from employers, partner job boards and the RAV offices. Each job comes with the full description, the company with its address, phone and email, the recruiter by name with phone and email, how to apply, workload, contract type, the languages required with their level, the occupation in plain words, exact coordinates and, when the ad states it, the salary. No login, no account.

Search by keyword, occupation, job title, company, canton, postal code, or distance around any Swiss town, and filter on what matters to you: remote work, immediate start, no German required, a contact email, a stated salary, or only employers hiring directly (no staffing agencies). Turn on change tracking and schedule it, and each run tells you which jobs are new, changed or closed, with an optional alert to Slack, Discord, Telegram or your own webhook.

> 👉 **Try it now:** press **Start**. The ready-made input gives you 10 results in a few seconds.<br>
> 💰 **$0.0171 per result** on the Free plan (10 results per run), from $0.0154 on paid plans. [See pricing](#-pricing)

### ✨ Why this scraper?

> - **Fast**<br>
>   About 1,000 jobs in half a minute.
>
> - **Complete**<br>
>   Every job on the platform, not only the first 10,000 of a search.
>
> - **Salary when stated**<br>
>   Parsed into numbers, where other Job-Room scrapers leave it empty.
>
> - **Distance search**<br>
>   "within 15 km of Winterthur" in one field.
>
> - **Filters built for real needs**<br>
>   No German required, remote, immediate start, must have a contact email, no staffing agencies.
>
> - **Full description on every job,**<br>
>   Also for keyword searches.
>
> - **Apify platform**<br>
>   Schedule runs, get only new results, export to Excel or CSV, use the API, or connect Google Sheets, Zapier and Make.

### ⚖️ How it compares

| Capability | This actor | Other Job-Room scrapers on Apify |
|---|:--:|:--:|
| Title, company, location, workload, description, contacts | yes | yes |
| **About 1,000 jobs in half a minute** | **yes** | about 250 per minute |
| **Works without a paid proxy** | **yes** | residential proxy by default |
| **Every job past the 10,000 per search limit** | **yes** | not stated |
| **Salary parsed from the ad** | **yes** | always empty |
| **Radius around a town** | **yes** | no |
| **Occupation groups and exact job titles** | **yes** | no |
| **Company, permanent or temporary, immediate start filters** | **yes** | no |
| **Remote, no experience, languages not required, must have contact filters** | **yes** | no |
| **Tells staffing agency ads apart, and can leave them out** | **yes** | no |
| **Occupation names, education, experience and languages in words** | **yes** | no |
| **Exact coordinates** | **yes** | no |
| Sort by relevance | yes | no |
| Change tracking with closed jobs and repost detection | yes | no |
| Slack, Discord, Telegram and webhook alerts | yes | no |
| Live status of known jobs | yes | yes |

### 🚀 How to scrape Swiss Jobs

1. Create a free Apify account.
2. Pick the filters you need, or keep the ready-made input.
3. Click **Start**.
4. Download the results as Excel, CSV or JSON from the **Output** tab.

### 💰 Pricing

#### How much does it cost to scrape Swiss Jobs?

You pay a one-time **$0.003** per run, plus a price for each result saved:

| Charged for | Free | Bronze | Silver | Gold+ |
| --- | --- | --- | --- | --- |
| **result**: Single result in the default dataset. | 0.0171 | 0.0154 | 0.0137 | 0.012 |

Example: 1,000 results cost about **$15.4** on Bronze. No proxy or compute costs on top.

### 📦 What data you get

| Entity | Useful fields |
| --- | --- |
| Job | Title, full description, occupation name and code, contract type, workload min and max, immediate start, start and end date, number of openings, remote or hybrid, status. |
| Salary | Minimum, maximum, period (year, month or hour), currency and the original wording, when the ad states a salary. |
| Requirements | Experience, education level, qualification, languages with spoken and written level, work forms (shift, night, Sunday and holidays, home based). |
| Location | City, postal code, canton, region, commune code, country, latitude and longitude, location notes. |
| Company | Name, whether a staffing agency posted the ad, street, P.O. box, postal code, city, country, phone, email, website. |
| Contacts | Contact person with salutation, phone and email; apply by email, phone, post or link, with application notes. |
| Source | Posted via (employer, partner job board or RAV office), official job number, employer reference, RAV office code, reporting obligation, shown on EURES, description language. |
| Links and dates | Job-Room link, application link, original ad, published, online until, approved, created, updated. |
| Change tracking | Change type, changed fields, repost flag with the original job ID, first seen, previously seen and closed dates. |

Every job has 71 fields, plus the change tracking fields when tracking is on. Every record includes `scraped_at` (UTC). Rows for a job ID that no longer exists carry an `error` field; it appears only on those rows. The dataset has four ready views: Jobs, Contacts, Requirements and Changes. Download it as CSV, JSON, Excel or XML.

### ⚙️ Input

**How many jobs** sits at the very top. It is 10 (prefilled) so a first run is a quick sample; clear it to get every match. The rest reads like a conversation:

| Section | What it asks |
| --- | --- |
| **1 · What kind of job?** | Job title or keywords, occupations from a list, official Swiss job titles, company. |
| **2 · Where?** | A town with a distance around it, whole cantons, or postal codes. |
| **3 · Contract and hours** | Permanent or temporary, workload from and up to, start right away. |
| **4 · Only show jobs that...** | were published recently, offer home office, state a salary, need no experience, are posted by the employer itself, come with an email, phone, contact person or application link, don't require German, French or Italian. |
| **5 · Results** | Order, and the language of occupation names. |
| **6 · Already have job links?** | Paste links to get just those jobs; the search sections are then ignored. |
| **7 · Get only new jobs on each run** | Change tracking for scheduled runs. |
| **8 · Send me an alert** | Slack, Discord, Telegram or a webhook. |

> **Apify Free plan:** every run is limited to a fixed 10 job sample. Upgrade your Apify plan to run your own settings.

#### Input examples

#### 🧑‍💼 Nursing jobs within 20 km of Bern, with a named contact

```json
{ "occupationGroups": ["222"], "city": "Bern", "radiusKm": "20", "mustHave": ["contactPerson"] }
```

#### 📣 Every company hiring software developers in Zurich this week, without staffing agencies

```json
{ "occupationGroups": ["251"], "cantons": ["ZH"], "postedWithinDays": "7", "excludeAgencies": true, "maxItems": 500 }
```

#### 🌍 Jobs in Geneva and Vaud that don't require French, with home office

```json
{ "cantons": ["GE", "VD"], "languagesNotRequired": ["fr"], "remoteOnly": true }
```

#### 💰 Jobs that state a salary, for pay benchmarks

```json
{ "salaryOnly": true, "maxItems": 1000 }
```

#### 🔔 A daily watch on new Migros jobs, sent to Slack

```json
{ "company": "Migros", "trackChanges": true, "slackWebhookUrl": "https://hooks.slack.com/services/..." }
```

#### 📊 The whole Swiss public job market

```json
{ "maxItems": null }
```

#### 🔎 Check whether known jobs are still online

```json
{ "jobIds": ["https://www.job-room.ch/job-search/b11fd96c-0cca-4960-aeec-4b4814693266"] }
```

### 🔗 Related scrapers

> - [Catho Jobs Scraper](https://apify.com/normdata/catho-scraper)
> - [OCC Mundial Jobs Scraper](https://apify.com/normdata/occ-scraper)
> - [SS.com Scraper](https://apify.com/normdata/ss-com-scraper): Latvia Classifieds (Cars, Flats, Jobs).
> - [Trabajando.cl Jobs Scraper](https://apify.com/normdata/trabajando-scraper): Chile Job Listings.

### ❓ FAQ and troubleshooting

#### Is there a Swiss Jobs API? Do I need an API key?

> Swiss Jobs has no public data API for this. This Actor works as a Swiss Jobs API alternative: it reads the public pages and gives you clean JSON, CSV or Excel, with no API key, no login and no code to maintain. You can also call it from your own code with the Apify API, or from an AI agent through MCP:
> `claude mcp add --transport http apify "https://mcp.apify.com?tools=normdata/jobroom-scraper"`

#### Why do I get only 10 results?

> On the Apify free plan each run saves up to 10 results, so you can try it before paying. Any paid plan removes the limit.

#### How fast is it?

> About 1,000 jobs in half a minute; the whole platform of 80,000+ jobs takes proportionally longer. Rare combinations (for example a stated salary, or no experience needed) have to look through many jobs to find enough matches, so they take a little longer.

#### How is the salary found?

> Job-Room has no salary field, so the ad text is read for a stated amount next to a pay word, such as "zwischen CHF 57'700 bis CHF 67'700 pro Jahr" or "mindestens CHF 4'192 pro Monat". Amounts that are not pay, like a travel allowance, are left out. About 2 in 100 ads state a salary; those get minimum, maximum and period.

#### How does the distance search work?

> Type any Swiss town in "Town or city" and pick a distance. The town is matched to its location and every job within that distance comes back, across canton borders.

#### I got fewer jobs than expected

> Contact filters (email, phone, named contact) and "posted by the employer itself" cut the pool the most, loosen them first.

#### A job link shows as offline

> The job was closed or filled; it's still returned so you know.

#### What are the limits?

> This Actor reads only public job ads. It never signs in and never contacts employers.
>
> Recruiter names, phones and emails are published by the employers on their own ads for applicants. Use them for relevant recruitment and business purposes, and respect Swiss and EU privacy and anti-spam rules when you reach out.

#### Is this an official Swiss Jobs tool?

> No. It is independent and not affiliated with Swiss Jobs. It reads only data Swiss Jobs publishes on its public pages, at a moderate pace. Use the data in line with Swiss Jobs's terms and the laws that apply to you.

### 📧 Contact

Need a scraper for a different site, or found something wrong with this one? norm.data.scrapers@gmail.com

# Changelog

This Actor's version history is a separate document: https://apify.com/normdata/jobroom-scraper/changelog.md

# Actor input Schema

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

The run stops once this many jobs are collected. Leave empty to get every matching job. *(Free plan: always a 10 job sample.)*

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

Words in the job title or description, in any language, e.g. `nurse`, `Koch`, `comptable`. A job matches if it has any of them. The prefilled `software` is only a sample: replace it, or delete it if you use Occupations instead.

## `occupationGroups` (type: `array`):

Pick from the list; start typing to search: nursing, software developers, electricians, cooks, accountants... A job matches if it is in any of them.

## `jobTitles` (type: `array`):

For one exact profession, e.g. `Pflegefachfrau`, `Informatiker`, `Polymechaniker`, `infirmier`. Unlike keywords, this finds every job filed under that profession, whatever words the ad uses. If a title is not found, the closest ones are suggested.

## `company` (type: `string`):

Only jobs from companies whose name contains this, e.g. `Migros`, `SBB`, `Roche`.

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

Any Swiss town, e.g. `Zürich`, `Genève`, `Winterthur`, `Lugano`.

## `radiusKm` (type: `string`):

Jobs up to this far from the town above. Only used when a town is filled in.

## `cantons` (type: `array`):

Jobs anywhere in these cantons.

## `postalCodes` (type: `array`):

A full code like `8001`, or the start of one like `80` for the whole area.

## `contractType` (type: `string`):

Permanent jobs, temporary jobs, or both.

## `workloadMin` (type: `string`):

Pensum in percent of full time. A job matches when its workload range touches yours: 80% to 100% also brings jobs offered at 60 to 80%.

## `workloadMax` (type: `string`):

100% is full time.

## `immediateStart` (type: `boolean`):

Only jobs that start immediately.

## `postedWithinDays` (type: `string`):

Every job online is at most 60 days old.

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

The ad mentions home office, remote or hybrid work.

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

The ad gives an amount; you get the minimum, maximum and period.

## `noExperienceOnly` (type: `boolean`):

The employer says no work experience is needed.

## `excludeAgencies` (type: `boolean`):

Leaves out jobs posted by staffing and recruitment agencies on behalf of another company.

## `mustHave` (type: `array`):

Only jobs that include all the details you pick.

## `languagesNotRequired` (type: `array`):

Leaves out jobs that ask for the language at intermediate level or higher. For people who do not speak German, French or Italian.

## `sort` (type: `string`):

"Best match" only differs from newest first when you search by keywords.

## `language` (type: `string`):

Occupation names come in this language. A job posted in several languages gives its description in this one too.

## `jobIds` (type: `array`):

One per line: a job-room.ch job link or its ID.

## `trackChanges` (type: `boolean`):

The first run remembers every job. Each later run marks jobs as new, updated or reappeared, and lists the ones that closed.

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

Off: later runs return only what changed. On: every current job, each with its change type.

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

Jobs from the last run that are no longer online.

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

Optional. Only needed to run several watches on the same search separately.

## `slackWebhookUrl` (type: `string`):

A Slack incoming webhook link, starting with https://hooks.slack.com/services/ (create one at api.slack.com/apps: Create New App, Incoming Webhooks, Add New Webhook to Workspace, pick the channel).

## `discordWebhookUrl` (type: `string`):

A Discord channel webhook link (Edit Channel, Integrations, Webhooks, New Webhook, Copy Webhook URL).

## `telegramBotToken` (type: `string`):

The token of your Telegram bot, from @BotFather.

## `telegramChatId` (type: `string`):

The chat or channel the bot posts to.

## `webhookUrl` (type: `string`):

Receives a JSON summary with the new and changed jobs.

## Actor input object example

```json
{
  "maxItems": 10,
  "keywords": [
    "software"
  ],
  "radiusKm": "10",
  "contractType": "any",
  "workloadMin": "10",
  "workloadMax": "100",
  "immediateStart": false,
  "postedWithinDays": "60",
  "remoteOnly": false,
  "salaryOnly": false,
  "noExperienceOnly": false,
  "excludeAgencies": false,
  "sort": "newest",
  "language": "en",
  "trackChanges": false,
  "emitUnchanged": false,
  "emitExpired": true
}
```

# Actor output Schema

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

One row per job.

# 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": 10,
    "keywords": [
        "software"
    ],
    "radiusKm": "10",
    "contractType": "any",
    "workloadMin": "10",
    "workloadMax": "100",
    "immediateStart": false,
    "postedWithinDays": "60",
    "remoteOnly": false,
    "salaryOnly": false,
    "noExperienceOnly": false,
    "excludeAgencies": false,
    "sort": "newest",
    "language": "en",
    "trackChanges": false,
    "emitUnchanged": false,
    "emitExpired": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("normdata/jobroom-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": 10,
    "keywords": ["software"],
    "radiusKm": "10",
    "contractType": "any",
    "workloadMin": "10",
    "workloadMax": "100",
    "immediateStart": False,
    "postedWithinDays": "60",
    "remoteOnly": False,
    "salaryOnly": False,
    "noExperienceOnly": False,
    "excludeAgencies": False,
    "sort": "newest",
    "language": "en",
    "trackChanges": False,
    "emitUnchanged": False,
    "emitExpired": True,
}

# Run the Actor and wait for it to finish
run = client.actor("normdata/jobroom-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": 10,
  "keywords": [
    "software"
  ],
  "radiusKm": "10",
  "contractType": "any",
  "workloadMin": "10",
  "workloadMax": "100",
  "immediateStart": false,
  "postedWithinDays": "60",
  "remoteOnly": false,
  "salaryOnly": false,
  "noExperienceOnly": false,
  "excludeAgencies": false,
  "sort": "newest",
  "language": "en",
  "trackChanges": false,
  "emitUnchanged": false,
  "emitExpired": true
}' |
apify call normdata/jobroom-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,normdata/jobroom-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/8zhuLUpcHbj2V8EEf/builds/lkgQ7ycMSawlNKXN5/openapi.json
