# Jobs.cz - Czech Jobs, Salary, Benefits & GPS (`abotapi/jobs-cz-scraper`) Actor

Scrape Czech job listings from Jobs.cz. Search builder (keyword, location, field, salary, employment type) or paste any Jobs.cz URL. Title, company, salary range with currency and period, benefits, GPS coordinates, required education and languages, employer contact info, remote/hybrid flag,.

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

## Pricing

from $1.20 / 1,000 job 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/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.cz Scraper

Turn **Jobs.cz**, the largest job board in the Czech Republic, into structured hiring data. Build a search (keyword, location, field, salary range, employment type) or paste any Jobs.cz URL, and get one clean row per job with salary, benefits, GPS coordinates, required education and languages, the full description, and employer contact info.

### Why use this scraper?

- **Search builder + URL mode** — build a search from Czech-language keywords, locations, fields, salary range and employment type, or paste any Jobs.cz listing/search/position URL (they mix freely in one run).
- **No empty rows** — jobs published on Jobs.cz itself come back fully populated, and so do employer micro-sites that expose their listing. When a page genuinely cannot be read, the run says so instead of returning a row of nulls you would still be billed for.
- **Rich structured output** — salary min/max with currency and period, GPS coordinates, benefits, required education and languages, employment/contract type, suitability flags, employer phone/email/contact, remote/hybrid flag, posting date, and the full HTML + plain-text description.
- **Czech-language aware** — field labels (`Požadované vzdělání`, `Požadované jazyky`, `Typ pracovního poměru`, …) and descriptions are extracted and normalized, and the salary text (`150 000 – 185 000 Kč`) parses into structured `salary.min`/`salary.max`.
- **Incremental monitoring** — schedule a daily run against the same search and get only what changed (NEW / UPDATED / REAPPEARED / EXPIRED), remembering state itself between runs.
- **Cheap** — light footprint, runs at **512 MB** (vs. heavier scrapers), over Apify datacenter or residential proxy.

### Inputs

#### Search mode

Jobs.cz has two kinds of search input, and they behave differently. **Keyword**
and **field** say *what* to look for and the site treats them as alternatives —
adding a field returns *more* jobs, not fewer. Everything else **narrows** what
those matched.

| Input | Kind | Description |
|---|---|---|
| `keyword` | what to look for | Free-text search, e.g. `python`, `programátor`, `účetní`. Czech text works. |
| `field` | what to look for | Professional field (obor) as Jobs.cz names it, e.g. `IS/IT`, `Doprava`. Looked up in the site's field list for you. **Widens** the results — use it on its own to sweep a whole field. |
| `location` | narrows | Free-text location, e.g. `Praha`, `Brno`, `Plzeň`. Resolved to a Jobs.cz locality automatically. |
| `minSalary` | narrows | Minimum gross salary in CZK/month. |
| `employmentType` | narrows | Full-time, part-time, employment contract, trade license, agreement, or internships. |
| `education` | narrows | Required education: without diploma / high-school / university. |
| `arrangement` | narrows | Work arrangement: partial from home, mostly from home, flexible hours. |

How much each one narrows, measured live on 2026-08-10 against a 247-job
`python` search: minimum salary 60 000 CZK → 178 · 150 000 CZK → 15 · full-time
→ 239 · part-time → 13 · internships → 7 · employment contract → 222 · trade
licence → 20 · agreement → 7 · without diploma → 2 · high-school → 117 ·
university → 108 · partial from home → 153 · mostly from home → 22 · flexible
hours → 159 · Praha → 160.

#### URL mode

Paste any Jobs.cz URL — listing pages (`/prace/{profession}/`, `/prace/{location}/`, filtered searches) and direct position URLs (`/rpd/{id}/`) are auto-classified and mixed freely.

A direct position URL whose job has been taken down returns no row and is
reported as gone (and, in incremental mode, marked `EXPIRED`). A position whose
page could not be read at all — a block, or an employer careers template this
Actor cannot parse — also returns no row, but is reported as **unread**: nothing
is billed for it, nothing is marked expired, and the incremental baseline is
left untouched so a temporary failure cannot look like a job disappearing.

#### Output and limits

`maxItems` is the run's cap (default 20). `maxPages` is unlimited by default and defers to `maxItems`.

#### Resume & recurring updates

- `resumeFromRunId` — continue ONE interrupted crawl without re-returning already-collected jobs.
- `incrementalMode` — recurring monitoring: the actor remembers the previous run of the same search itself (keyed on the search setup) and returns only NEW / UPDATED / REAPPEARED / EXPIRED jobs.

**`EXPIRED` is only ever emitted after a run that read every search page it asked for.** If a run is capped by `maxItems`/`maxPages`, resumed from a run id, or stopped because a page could not be read, the actor keeps the previous state as-is, tells you so in the log, and does **not** advance its saved baseline, so a temporary block can never mark your tracked jobs as gone (and never bills you for those rows, nor for the `REAPPEARED` rows when they come back).

#### Send results into your apps (MCP connectors)

| Input | Description |
|---|---|
| `mcpConnectors` | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). Leave empty to skip. |
| `notionParentPageUrl` | Notion connector only: the page under which item pages are created. |
| `maxNotifyListings` | Cap on items exported to each connector per run (default 50). Does not affect the dataset. |

#### Detail enrichment (optional surcharge)

`fetchDetails` (off by default) opens each job's detail page and adds the full description, structured salary, benefits, GPS, education/languages, employer contact and remote/hybrid flags. It adds one request per job and is billed as a separate `detail-enrichment` surcharge on top of the base result price.

You are charged the surcharge **only for rows you actually receive, and only when their detail page was really read** — never more than once per job:

- A minority of Jobs.cz positions live on an employer careers site that publishes no readable detail page. Those jobs are still returned, with their search-page fields and `detailSource: null`, and are **not** charged. If a whole run turns out to be like that, the actor stops opening detail pages altogether and says so in the log and in the run summary (`detailEnrichmentDisabled`), rather than paying for pages it cannot read.
- In **Incremental mode** an unchanged job's detail page still has to be read to tell whether anything changed, but if the row is suppressed (not delivered) it is **not** charged. A run that returns nothing therefore costs no surcharge at all.

### Output

Each dataset item is one job. Key fields:

| Category | Fields |
|---|---|
| Identity | `jobId`, `jobUrl`, `title` |
| Employer | `companyName`, `companyLogo`, `employerPhone`, `employerEmail`, `contactName`, `contactCompanyName`, `companyAddress`, `employerProfileUrl` |
| Location | `location`, `addressLocality`, `addressRegion`, `postcode`, `country`, `coordinates` (`latitude`/`longitude`) |
| Compensation | `salary` (`currency`, `min`, `max`, `unit`, `raw`), `salaryRaw` |
| Classification | `categories`, `employmentTypes`, `contractType`, `contractDuration`, `hoursPerWeek`, `requiredEducation`, `requiredLanguages`, `benefits`, `remote`, `arrangement` |
| Suitability | `suitableForGraduate`, `suitableForDisabled`, `suitableForPensioner`, `suitableForMaternityLeave` |
| Dates | `postedDate` |
| Content | `description` (HTML), `descriptionText` (plain), `teaser` |
| Apply | `applyUrl`, `applyType` |
| Meta | `detailSource` (`job-page` / `employer-site`), `scrapedAt` |
| Incremental | `changeType` (`NEW` / `UPDATED` / `REAPPEARED` / `EXPIRED`), `changedFields`, `firstSeenAt`, `lastSeenAt` |

#### Output sample

Illustrative only — the employer, contact details and coordinates below are
fictional placeholders, not a real posting.

```json
{
  "jobId": "0000000000",
  "jobUrl": "https://www.jobs.cz/rpd/0000000000/",
  "title": "Senior Data Science, Python Developer",
  "companyName": "Example Company s.r.o.",
  "location": "Praha 1, Hlavní město Praha",
  "coordinates": { "latitude": 50.0755, "longitude": 14.4378 },
  "salary": { "currency": "CZK", "min": 140000, "max": 180000, "unit": "měsíc", "raw": "140000–180000 Kč měsíc" },
  "employmentTypes": ["Práce na plný úvazek"],
  "requiredEducation": "Vysokoškolské / univerzitní",
  "requiredLanguages": ["Čeština — Výborná"],
  "benefits": ["Firemní akce", "Možnost občasné práce z domova", "Nadstandardní lékařská péče"],
  "remote": true,
  "arrangement": "hybrid",
  "postedDate": "2026-06-12T16:22:22+02:00",
  "employerPhone": "+420 555 010 042",
  "employerEmail": "kariera@example.com",
  "detailSource": "employer-site",
  "descriptionText": "Hledáme datového vědce se znalostí Pythonu…"
}
```

### Proxy

Jobs.cz works over both the Apify datacenter and residential proxy groups. Leave the default (no group) for the cheapest runs; switch to **Residential** with country CZ for larger runs where a Czech exit improves reliability. The actor fails fast on a repeated block rather than grinding.

If it cannot read a single search page after retrying on fresh exit IPs, the run **fails** with a message saying so, rather than finishing green with an empty dataset. A run that read some pages but not all finishes with a partial-results warning. Either way nothing is marked `EXPIRED` and the incremental baseline is left untouched. Re-run, ideally on Residential with country CZ.

> Free-tier users: the actor works over the Apify proxy included with your plan. Larger runs work best on a paid plan with residential access.

### Send results into your apps (MCP connectors)

Optionally pipe results into the apps you already use through Model Context
Protocol (MCP) connectors. Authorize a connector once under Apify, Settings,
Integrations, then select it in the `mcpConnectors` field. Each connector
receives a condensed, human-readable summary per job (title plus key fields),
while the complete record always stays in the Apify dataset. For Notion, set
`notionParentPageUrl` to the page the item pages should be created under, and
use `maxNotifyListings` to cap how many items are exported per connector per
run. Supported connectors: Notion, Linear, Airtable and Apify. Leave the field
empty to skip; it never changes the dataset output.

### FAQ

**Does it return salary for every job?** No — many Czech listings don't publish one. When the site shows a salary, it's parsed into `salary.min`/`salary.max` with currency and period.

**Does it return GPS for every job?** Employer-branded micro-site listings come back with exact coordinates. Other listings are geocoded from their address (best-effort). Not every job publishes an address.

**How do I get only fresh jobs?** Turn on Incremental mode and schedule the run. Every later run of the same search returns only what changed — new jobs, jobs whose details moved, and (optionally) jobs that have gone.

**What does `detailSource` mean?** Which page a row's fields came from: `job-page` for a listing hosted on Jobs.cz itself, `employer-site` for one published on the employer's own careers micro-site.

### Legal

This Actor is an independent tool and is not affiliated with or endorsed by Jobs.cz, Alma Career, or LMC s.r.o. It accesses only publicly available job-listing pages. Users are responsible for complying with Jobs.cz's Terms of Service and applicable law.

# Actor input Schema

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

search = build a Jobs.cz search from keywords, location, field, salary and employment type. url = paste one or more Jobs.cz listing/search or direct position URLs and walk them forward.

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

Free-text search, e.g. "python", "programátor", "účetní" (Czech text works — Jobs.cz normalizes it). Leave blank to list everything.

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

Free-text location, e.g. "Praha", "Brno", "Plzeň". Matches Jobs.cz localities (regions, cities, districts). Leave blank for all of Czechia.

## `field` (type: `string`):

Professional field (obor) as Jobs.cz names it, e.g. "IS/IT", "Doprava", "Ekonomika a podnikové finance". The Actor looks the name up in the Jobs.cz field list for you. Note this ADDS every job in that field to the results alongside your keyword — Jobs.cz treats keyword and field as alternatives, not as a combined filter — so use it on its own to sweep a whole field. Leave blank for all fields.

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

Only jobs advertising at least this monthly gross salary. 0/empty = no salary filter.

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

Filter by employment type (typ úvazku). Empty = all types. All six values narrow the results (measured live: on a 247-job baseline, full 239 · part 13 · internships 7 · employment contract 222 · trade licence 20 · agreement 7).

## `education` (type: `string`):

Filter by required education. Empty = all.

## `arrangement` (type: `string`):

Filter by work arrangement — this is where the remote/hybrid flag comes from. Empty = all.

## `urls` (type: `array`):

Full Jobs.cz URLs to crawl. Listing pages are walked forward page by page; direct /rpd/{id}/ URLs are scraped as single jobs. To build a search URL, run your query on jobs.cz and copy the address bar URL.

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

Stop after this many jobs across all searches/URLs. This is the run's cap.

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

How many result pages to walk per search (Jobs.cz shows about 20 to 30 results per page). Leave empty / 0 for unlimited, in which case the run stops at Max items instead.

## `fetchDetails` (type: `boolean`):

COSTS EXTRA. Off by default. Turning it on adds one request per job and is billed as a "detail-enrichment" surcharge on top of the base result price. You are charged only for rows you actually receive whose detail page was really read, and never more than once per job. Off = SERP-card fields only (title, company, location, teaser).

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

Paste a previous run ID or dataset ID to continue a large crawl without returning or charging for jobs already collected there. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn on for daily/recurring monitoring. The first run returns all matching jobs as NEW. Later runs return only NEW, UPDATED, and REAPPEARED jobs, plus EXPIRED ones when you enable Emit expired. State is kept separately for each keyword/location/field/salary/employment-type/URL setup; use State key to name or share a campaign.

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

Optional. Name this monitoring campaign to keep its state stable, or to share state across differently-configured runs. Leave empty to derive a key automatically from the search setup — different searches then never mix state.

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

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

Off by default. Turn on to also return jobs that were present in a previous run but are no longer found, marked EXPIRED. This returns — and bills — extra rows. Only produced once a run fully scanned the tracked search: never when Max items or Max pages capped it, never when Resume was used, and never when a search page could not be read, so a temporary block cannot mark your live jobs as gone.

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

Jobs.cz works over both the Apify datacenter and residential proxy groups. Leave the default (no group) for the cheapest runs; switch to Residential with country CZ for larger runs where a Czech exit improves reliability.

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

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion, Linear, Airtable, Apify.

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

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

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

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "keyword": "python",
  "location": "Praha",
  "minSalary": 0,
  "employmentType": "",
  "education": "",
  "arrangement": "",
  "urls": [
    "https://www.jobs.cz/prace/programator/"
  ],
  "maxItems": 20,
  "maxPages": 0,
  "fetchDetails": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

One row per job position: identity, employer, location and GPS, structured salary, classification, suitability flags, dates, description and apply link. In incremental mode each row also carries changeType (NEW / UPDATED / REAPPEARED / EXPIRED), changedFields, firstSeenAt and lastSeenAt.

## `output` (type: `string`):

Run summary: jobs returned, how many of them were charged the detail-enrichment surcharge, searches walked, whether any page could not be read, whether the incremental baseline was advanced, how many exits the run had to open, and — if detail enrichment had to be switched off mid-run because the positions had no readable detail page — why, and how many jobs were returned with search-page fields only.

# 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",
    "keyword": "python",
    "location": "Praha",
    "minSalary": 0,
    "employmentType": "",
    "education": "",
    "arrangement": "",
    "urls": [
        "https://www.jobs.cz/prace/programator/"
    ],
    "maxItems": 20,
    "maxPages": 0,
    "fetchDetails": false,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/jobs-cz-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",
    "keyword": "python",
    "location": "Praha",
    "minSalary": 0,
    "employmentType": "",
    "education": "",
    "arrangement": "",
    "urls": ["https://www.jobs.cz/prace/programator/"],
    "maxItems": 20,
    "maxPages": 0,
    "fetchDetails": False,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/jobs-cz-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",
  "keyword": "python",
  "location": "Praha",
  "minSalary": 0,
  "employmentType": "",
  "education": "",
  "arrangement": "",
  "urls": [
    "https://www.jobs.cz/prace/programator/"
  ],
  "maxItems": 20,
  "maxPages": 0,
  "fetchDetails": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/jobs-cz-scraper --silent --output-dataset

```

## MCP server setup

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