# jobs.ch Scraper (`crawlerbros/jobs-ch-scraper`) Actor

Scrape job listings from jobs.ch, Switzerland's largest job board. Search by keyword, or browse by region, industry, occupational field or contract type, with filters for workload, seniority, language, company size and publish date. Returns title, company, location, workload and the job URL.

- **URL**: https://apify.com/crawlerbros/jobs-ch-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Automation, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## jobs.ch Scraper

Scrape **jobs.ch** — Switzerland's largest job board. Search by keyword, browse by canton/language-region, industry/category, occupational field, or employment (contract) type, and refine with a workload-percentage range, seniority level, required language, company size, and published-since date. Returns title, company, location, contract type, workload, posted date, and the job URL — with an optional deeper mode that adds the full job description, benefits, required skills, and salary when the employer discloses it. HTTP-only, no auth, no cookies required.

### What this actor does

- **Five modes:** `search` (keyword), `byRegion` (canton/language-region browse), `byIndustry` (category browse), `byEmploymentType` (contract-type browse), `byCategory` (occupational field / job function browse) — every filter below is combinable in any mode
- **Filters:** city/place, canton/region, industry, occupational field, employment (contract) type, workload % range, seniority level, required language, company size, published-since date
- **Three site languages:** English, German, French
- **Optional enrichment:** full job description, benefits, required skills, apply URL, and salary (when disclosed)
- **Empty fields are omitted**

### Output per job

- `jobId`, `title`
- `companyId`, `companyName`, `companySlug`, `companyUrl`, `companyLogoUrl`
- `location` — primary place name (e.g. `Zürich`)
- `locationDetails[]` — canton code, city, postal code, country code, latitude/longitude (present on jobs with more than one workplace)
- `workloadMin`, `workloadMax`, `workloadText` — employment grade in percent (e.g. `80 – 100%`)
- `contractType`, `contractTypes[]` — e.g. `Permanent position`, `Freelance`, `Internship`
- `seniorityLevel` — `Executive position` / `Specialist` / `Employee`
- `tags[]` — badges jobs.ch shows on the listing, e.g. `Easy apply`, `Quick apply` (present only when the listing carries one)
- `benefitTags[]` — employer-disclosed perks shown on the listing itself (e.g. `Working from home`, `Flexible working hours`) — present only on the share of listings that have any; a larger, differently-sourced `benefits[]` list is also available via `includeDescription`
- `postedDate`, `initialPostedDate`, `postedRelative`, `initialPostedRelative` (present only on reposted/republished listings where it differs from `postedRelative`, or when jobs.ch echoes the same value)
- `isNew`, `isBoosted`, `isPaid`, `isActive`, `headhunterApplicationAllowed`
- `jobUrl`
- With `includeDescription: true` — `description`, `roleSummary` (short, normalized job title, e.g. `"Consultant Finance"` — this is jobs.ch's own internal role-taxonomy label, so it can occasionally read as a broader category than the literal posted title), `industry` (employer's business sector as text, distinct from the job's `category`), `benefits[]`, `skills[]`, `workHoursText`, `category`, `totalOpenings`, `startDate`, `jobImmediateStart`, `directApply`, `companyWebsite` (the employer's own site, distinct from `companyUrl`), `companyOverview`, `contactName`, `contactPhone`, `streetAddress`, `jobImageUrl` (the job posting's own banner/cover image, when the employer supplied one), `applicantLocationRequirement` (e.g. `Switzerland` / `Abroad` — the employer's stated eligibility requirement for applicants), and `salaryMin` / `salaryMax` / `salaryCurrency` / `salaryPeriod` when the employer discloses a salary range. With `includeDescription: true`, `companyUrl` is also refined to the canonical URL the job posting itself declares (jobs.ch occasionally serves a listing-level company slug that only 301-redirects to this canonical one). Use `jobUrl` as the apply entry point — see Limitations below for why a separate `applyUrl` isn't offered.
- `recordType: "job"`, `scrapedAt`

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search` | `search` / `byRegion` / `byIndustry` / `byEmploymentType` / `byCategory` |
| `searchQuery` | string | – | Free-text keyword (mode=search); leave blank to browse all jobs |
| `region` | string | – | Canton / language-region (required for mode=byRegion) |
| `location` | string | – | Free-text city/place refinement, e.g. `Zürich` |
| `industry` | string | – | Industry / employer sector (required for mode=byIndustry) |
| `category` | string | – | Occupational field / job function (required for mode=byCategory) — distinct from `industry` |
| `employmentType` | string | – | Contract type (required for mode=byEmploymentType) |
| `employmentGradeMin` | int | `0` | Workload % lower bound |
| `employmentGradeMax` | int | `100` | Workload % upper bound |
| `position` | string | – | Seniority level filter |
| `publishedSince` | string | – | Only jobs first published in the last 24h / 3 / 7 / 30 days |
| `languageSkill` | string | – | Only jobs that require this working language: `de` / `en` / `fr` / `it` |
| `companySegment` | string | – | Only jobs at large companies (`gu`) or SMEs (`kmu`) |
| `includeDescription` | bool | `false` | Fetch each job's detail page for description/benefits/skills/salary |
| `locale` | string | `en` | Site language: `en` / `de` / `fr` |
| `maxItems` | int | `20` | Hard cap (1–1000) |
| `proxyConfiguration` | object | AUTO | Used first for keyword (`searchQuery`) searches — jobs.ch's own IP reputation narrows relevance for the raw datacenter IP; used only as an automatic fallback for browse/filter modes |

#### Example: keyword search

```json
{
  "mode": "search",
  "searchQuery": "data engineer",
  "employmentGradeMin": 80,
  "employmentGradeMax": 100,
  "maxItems": 50
}
```

#### Example: browse a canton/region

```json
{
  "mode": "byRegion",
  "region": "7",
  "employmentType": "5",
  "maxItems": 100
}
```

#### Example: browse an industry with full descriptions

```json
{
  "mode": "byIndustry",
  "industry": "13",
  "includeDescription": true,
  "maxItems": 25
}
```

#### Example: browse by contract type

```json
{
  "mode": "byEmploymentType",
  "employmentType": "6",
  "location": "Zürich"
}
```

#### Example: browse by occupational field, published in the last week

```json
{
  "mode": "byCategory",
  "category": "106",
  "publishedSince": "7",
  "languageSkill": "en",
  "companySegment": "gu"
}
```

### Use cases

- **Recruiting intelligence** — track how many roles a canton or industry is hiring for
- **Labour-market research** — analyse workload/contract-type distribution across Switzerland
- **Job aggregators** — feed jobs.ch listings into a meta-search product
- **Salary benchmarking** — collect the (rare) publicly disclosed salary ranges
- **Company monitoring** — watch a specific employer's open roles via `searchQuery`

### FAQ

**What's the data source?**  The public jobs.ch job-search results page, in the same language/data the site itself renders. No login, no API key.

**Is this affiliated with jobs.ch?**  No — this is a third-party actor that reads jobs.ch's publicly accessible pages.

**Why is `location` free text but `region` a dropdown?**  jobs.ch's canton/language-regions are a fixed, documented list of ~24 options, so they're a dropdown. City/place names are effectively unbounded, so `location` stays free text and is combinable with `region`.

**Why is salary often missing?**  Most Swiss employers do not publish a salary range on their listings. The `salaryMin`/`salaryMax`/`salaryCurrency` fields only appear on the small share of jobs where the employer disclosed one, and only when `includeDescription` is enabled.

**What does `includeDescription` cost me?**  It fetches one extra page per job, roughly doubling the number of requests and the run time — leave it off for a fast listing-only scrape.

**Can I combine `region`, `industry`, `category`, `employmentType`, `publishedSince`, `languageSkill`, `companySegment` and `searchQuery` together?**  Yes — every filter is honoured in every mode; `mode` only controls which single field is mandatory.

**What's the difference between `industry` and `category`?**  `industry` is the *employer's* business sector (e.g. Banking, Retail, Healthcare). `category` is the *job's* occupational field / function (e.g. IT, Sales, Finance) — jobs.ch tracks these as two separate taxonomies, so a Banking-industry employer can post an IT-category job. Use whichever axis matches your research question, or combine both.

**How fresh is the data?**  Real-time — every run reads jobs.ch's live search results, not a cache.

**Do I need a proxy?**  No configuration needed — the free `AUTO` proxy group is on by default. For `byRegion`/`byIndustry`/`byEmploymentType`/`byCategory` browsing it's only used as an automatic fallback if a run gets temporarily rate-limited. For keyword (`searchQuery`) searches it's used proactively on every request, because jobs.ch's own search-relevance backend returns a smaller, less on-topic result set to plain datacenter IPs (still HTTP 200 — not a rate limit) unless the request goes through Apify's proxy pool.

**Why does an unusual `searchQuery` sometimes return a handful of unrelated jobs instead of 0?**  jobs.ch's own search engine is deliberately typo-tolerant/fuzzy rather than a literal substring match — verified by loading the same query directly on jobs.ch in a browser: it shows the identical small result count and titles. This actor faithfully mirrors whatever jobs.ch itself returns; it never fabricates results for an unmatched query.

### Data source & limitations

This actor reads jobs.ch's server-rendered search-results HTML (the same Redux state the site itself hydrates from) — not a private or authenticated API. Every filter and output field listed above comes from that public payload.

Axes that exist on jobs.ch but are **not** exposed by this actor, and why:

- **Company-name filter** — jobs.ch resolves a company filter to an internal `companyId` via an autocomplete lookup rather than a stable text/slug parameter, so it can't be driven reliably from a plain input field. Use `searchQuery` (jobs.ch's full-text search already matches company names) as a practical substitute.
- **Sort order** (relevance / date) — the `sort=`/`order=` query parameters accepted by the results page are silently ignored by jobs.ch's backend (verified: identical `searchHash`/result order regardless of value), so no `sortBy` input is offered — it would not do anything.
- **Benefit filter (`benefit=`)** — the results page recognizes a `benefit=` query parameter (both numeric ids and benefit slugs echo back into the embedded search state), but verified live: it makes the underlying search silently fail (`fetched: false`, 0 results, still HTTP 200) for every value tried, including the exact slugs jobs.ch's own listings emit (`training`, `flexible-working-hours`, `free-drinks`, ...). Unlike the `employment-grade-min=0` quirk (which has one clean, verified workaround value), no value makes this filter actually run a search, so it is not exposed as an input. The listing-level `benefitTags[]` output field is unaffected — that comes from each job's own record, not from filtering.
- **Job "group" filter (`group=`)** — also recognized into the embedded search state and returns `fetched: true`, but every id from 1–200 tested returns exactly 0 hits with no documented id scheme; excluded as unreliable/undiscoverable rather than exposing a filter that always returns nothing.
- **Listing "label" filter (`label=`)** — accepted into the embedded search state but verified to have zero effect on the result set or count (same `totalHits` with or without it, for both `new` and `boosted` values) — like `sort=`, it's a no-op parameter.
- **`languageSkills`, `regions`, `contacts`, `applicationOptions`, `videos` fields** — jobs.ch's search-results payload declares these fields on every listing row, but they were empty (`[]`/`{}`) across every sampled query, filter and page during testing, including after filtering by `languageSkill`. They are intentionally omitted rather than emitted as always-null placeholders.
- **A broader `benefits` vocabulary** — the listing-level `benefitTags[]` field reformats whatever slug jobs.ch already sent (e.g. `working-from-home` → `Working from home`) rather than mapping against a fixed lookup table, because jobs.ch's benefit-tag vocabulary is open-ended and grows over time; a hardcoded table would silently go stale.
- **Job detail's `employmentUnit` field** — the detail page's ld+json block declares this, but verified live across dozens of listings: its value is always identical to the already-extracted `seniorityLevel` (`Employee` / `Specialist` / `Executive position`), just sourced from a second place. Omitted as a pure duplicate rather than a second field carrying the same information twice.
- **Job detail's `identifier`/`additionalType`/`jobLocation.address.addressRegion`** — `identifier.value` duplicates `jobId`; `additionalType` is a constant boilerplate value (`https://schema.org/Product`) on every listing; `addressRegion` is mislabeled by jobs.ch's own markup (it holds the city name, duplicating `location`, not an actual canton/region). None carry information beyond what's already emitted.
- **Job detail's `datePosted`/`employmentType` (schema.org fields)** — verified live across sampled listings: `datePosted` is always identical to the listing's own `publicationDate` (already emitted as `postedDate`); `employmentType`'s text value is always identical to the already-extracted, locale-translated `contractType`. Both are pure duplicates sourced from a second place on the detail page.
- **Apply URL (`potentialAction.target.urlTemplate` in the detail page's ld+json)** — jobs.ch's own structured data declares this as a directly-fetchable "ApplyAction" URL, but verified live (both `curl -ILSs` with no cookies and a full JS-rendered browser navigation): it 404s. jobs.ch's real "Apply" button is a pure client-side modal — clicking it doesn't even change the browser's URL — so there is no working standalone apply link to expose. `jobUrl` (the job's own detail page, always reachable) is the correct entry point; a user reaches the same Apply button by opening it.

# Actor input Schema

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

What to fetch. `search` runs a free-text keyword search (leave the query blank to browse every job). `byRegion` browses a Swiss canton/language-region. `byIndustry` browses an industry/category. `byEmploymentType` browses a contract type. `byCategory` browses an occupational field / job function.

## `searchQuery` (type: `string`):

Free-text keyword to search job titles/descriptions (mode=search). Leave blank to browse all jobs matching the other filters. Combinable with every filter below in any mode.

## `region` (type: `string`):

Browse jobs by Swiss canton / language-region (required for mode=byRegion; optional filter in every other mode).

## `location` (type: `string`):

Optional free-text refinement by city or place name (e.g. "Zürich", "Geneva", "Basel"). Combinable with `region`.

## `industry` (type: `string`):

Browse jobs by industry sector (required for mode=byIndustry; optional filter in every other mode).

## `employmentType` (type: `string`):

Browse/filter by contract type (required for mode=byEmploymentType; optional filter in every other mode).

## `employmentGradeMin` (type: `integer`):

Lower bound of the workload/employment-grade range, in percent (e.g. 80 for an 80-100% role).

## `employmentGradeMax` (type: `integer`):

Upper bound of the workload/employment-grade range, in percent.

## `position` (type: `string`):

Optional filter by seniority/position level.

## `category` (type: `string`):

Browse/filter by occupational field — the job's function (e.g. IT, Sales, Finance), distinct from `industry` (the employer's business sector). Required for mode=byCategory; optional filter in every other mode.

## `publishedSince` (type: `string`):

Optional filter to only return jobs first published within this time window.

## `languageSkill` (type: `string`):

Optional filter to only return jobs that require this working language.

## `companySegment` (type: `string`):

Optional filter by employer size/segment.

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

When enabled, fetches each job's detail page to add the full job description, benefits, required skills, apply URL and salary (only when the employer discloses it — most Swiss listings do not). This roughly doubles the number of requests per run.

## `locale` (type: `string`):

Language of the jobs.ch site to scrape from — affects the language of translated fields (title/company are unaffected; job counts and set are identical across locales).

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

Maximum number of job listings to return. jobs.ch returns 20 listings per page.

## `proxyConfiguration` (type: `object`):

Optional. jobs.ch is a plain public site that normally does not require a proxy; this is used only as an automatic fallback if the datacenter IP gets temporarily rate-limited.

## Actor input object example

```json
{
  "mode": "search",
  "searchQuery": "python developer",
  "region": "",
  "location": "",
  "industry": "",
  "employmentType": "",
  "employmentGradeMin": 0,
  "employmentGradeMax": 100,
  "position": "",
  "category": "",
  "publishedSince": "",
  "languageSkill": "",
  "companySegment": "",
  "includeDescription": false,
  "locale": "en",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Dataset containing all scraped jobs.ch job listings.

# 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 = {
    "mode": "search",
    "searchQuery": "python developer",
    "region": "",
    "location": "",
    "industry": "",
    "employmentType": "",
    "employmentGradeMin": 0,
    "employmentGradeMax": 100,
    "position": "",
    "category": "",
    "publishedSince": "",
    "languageSkill": "",
    "companySegment": "",
    "includeDescription": false,
    "locale": "en",
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/jobs-ch-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 = {
    "mode": "search",
    "searchQuery": "python developer",
    "region": "",
    "location": "",
    "industry": "",
    "employmentType": "",
    "employmentGradeMin": 0,
    "employmentGradeMax": 100,
    "position": "",
    "category": "",
    "publishedSince": "",
    "languageSkill": "",
    "companySegment": "",
    "includeDescription": False,
    "locale": "en",
    "maxItems": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/jobs-ch-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 '{
  "mode": "search",
  "searchQuery": "python developer",
  "region": "",
  "location": "",
  "industry": "",
  "employmentType": "",
  "employmentGradeMin": 0,
  "employmentGradeMax": 100,
  "position": "",
  "category": "",
  "publishedSince": "",
  "languageSkill": "",
  "companySegment": "",
  "includeDescription": false,
  "locale": "en",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call crawlerbros/jobs-ch-scraper --silent --output-dataset

```

## MCP server setup

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