# Pracuj.pl Jobs Scraper - New Jobs Monitor (Poland) (`neverempty/pracuj-new-jobs-monitor`) Actor

For recruiters, staffing agencies, job alert bots and hiring trackers: Pracuj.pl jobs (oferty pracy) with company, location, work mode, contract, level and salary as listed. With monitor mode on, each scheduled run returns only new jobs, salary changes and removed jobs. JSON, CSV or Excel.

- **URL**: https://apify.com/neverempty/pracuj-new-jobs-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.79 / 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?

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

## Pracuj.pl Jobs Scraper - New Jobs Monitor (Poland)

**For recruiters, staffing agencies, job alert bots and anyone tracking hiring in Poland:** the newest jobs on Pracuj.pl (nowe oferty pracy) with the company, location, work mode, contract type, level, the salary as listed (plus the amounts in PLN when they can be read), the publish date and the job link.

**Schedule it with monitor mode on → each run returns only new jobs.** Run it hourly or daily on a Pracuj.pl search and each run returns only the jobs published since the last run, plus salary changes and removed jobs, so you pay for what changed, not for the whole list again.

Start with just a keyword, a city or a pasted Pracuj.pl search URL. Export as JSON, CSV or Excel, or send the new jobs to Slack, email or a sheet with an Apify integration.

### What you get

- **New jobs only, on a schedule** (`monitorMode`). Each row says what happened in `changeType`: `new` (published since the last run), `changed` (the salary changed; the previous salary is in `previousSalaryLabel`, `previousSalaryMin`, `previousSalaryMax`, and `salaryDirection` says `up` or `down` when both salaries are comparable amounts), or `removed` (a job you received is no longer in the newest-first list on two checks in a row; jobs near the bottom of the window are not judged). The first run returns the newest `maxResultsPerSearch` jobs (default 100, charged) as the starting point; a search added to an existing watch starts the same way.
- **Newest first, 100 jobs per search by default, up to 2,000.**
- **Salary you can sort.** `salaryLabel` keeps the text exactly as listed (`20 000–25 000 zł netto (+ VAT) / mies.`, `35–45 zł brutto / godz.`). `salaryMin`, `salaryMax`, `salaryPeriod` (`month` or `hour`) and `salaryBasis` (`gross` for brutto, `net` for netto) are read from it, with `salaryCurrency` `PLN`. When a job shows no pay, or the text is not one clear range in zł, these stay empty; nothing is guessed.
- **The filters Pracuj.pl has:** keywords, city with distance, region (województwo, or abroad), remote work (praca zdalna) and other work modes, contract type (umowa o pracę, B2B, umowa zlecenie and more), position level (junior, mid, senior, manager and more), working time, posted within 1-30 days, and jobs with salary only. Or paste any Pracuj.pl search URL with your filters already set.
- **Each job once.** A job found by two searches of the same run is returned and charged once. In monitor mode, a job you already received is not returned as `new` again (each watch remembers the 4,000 jobs it saw most recently), even when it shows up under another search of the same watch, and older jobs that only moved up the list are not returned as new.
- **Nothing new? You get a free row saying so, and no job is charged.**

### Output

One row per job. Main columns:

| Column | Meaning |
|---|---|
| `status` | `ok` for a job row. Other values are free rows (below) |
| `changeType` | Monitor mode: `new`, `changed`, `removed` or `unchanged` (only with `includeUnchanged`). Empty in a normal search. `removed` rows carry the title, company, last salary, date and link only |
| `isFirstCheck` | `true` on the first run of a watch, when the current jobs are returned as the starting point |
| `id`, `url` | Pracuj.pl job ID and the job page |
| `offerIds`, `offerUrls` | One offer number and link per city the job is posted in |
| `title` | Job title |
| `companyName`, `companyId`, `companyProfileUrl`, `logoUrl` | The employer as listed |
| `location`, `locations`, `remoteWorkAllowed` | Where the job is, and whether remote work is allowed |
| `workModes`, `contractTypes`, `workSchedules`, `positionLevels` | As listed on Pracuj.pl, in Polish (`Praca zdalna`, `Kontrakt B2B`, `Pełny etat`, `Specjalista (Mid / Regular)`) |
| `salaryLabel`, `salaryMin`, `salaryMax`, `salaryPeriod`, `salaryBasis`, `salaryCurrency` | Salary text as listed, and the amounts read from it |
| `previousSalaryLabel`, `previousSalaryMin`, `previousSalaryMax`, `previousSalaryPeriod`, `previousSalaryBasis`, `salaryDirection` | Monitor mode, `changed` rows: the salary before the change, and `up` or `down` when both are amounts per the same period and gross/net basis |
| `descriptionSnippet` | The short text shown in the search results |
| `publishedAt`, `firstPublishedAt`, `isRepublished`, `expiresAt` | Last publish date, first publish date, whether it was published again, and the closing date (UTC) |
| `oneClickApply`, `cvOptional` | One-click apply, and whether a CV is optional |
| `positionInSearch`, `jobsInSearch` | Position in the newest-first list, and the number of jobs Pracuj.pl reports for the search |
| `searchKeywords`, `searchCity`, `searchRegion`, `searchUrl` | The search the row came from; `searchUrl` opens the same search on Pracuj.pl |
| `watchName`, `checkedAt` | The watch name you gave, and the time of the check |
| `note` | Free rows only: what happened and what to do |

#### Free rows (never charged)

No jobs, no changes or a spending limit come back as a free row that says why. If Pracuj.pl refuses every request, the run ends as failed and says so.

| `status` | When |
|---|---|
| `no-results` | Pracuj.pl lists no job for this search. |
| `no-changes` | Monitor mode: no new jobs, salary changes or removed jobs since the last run. |
| `none-matching` | Pracuj.pl has jobs, but each one was already returned by an earlier search of the same run or watch. |
| `more-not-returned` | Fewer jobs came back than `maxResultsPerSearch`, because one search reads at most its newest 2,000 listings; the row says how many jobs the search has. Every job row also has that total in `jobsInSearch`. Monitor mode: more changes than `maxResultsPerSearch` (a later run returns them), or on the first run, how many jobs became the starting point. |
| `more-new-than-readable` | Monitor mode: more jobs were published since the last run than this run read, so some new ones may have been missed. Run the watch more often or raise `maxResultsPerSearch`. |
| `incomplete` | A page of the list could not be read. The jobs before it were returned; in monitor mode nothing is judged and nothing is remembered, and the next run checks again. |
| `not-found` | Pracuj.pl could not use the search (for example a pasted address that is not a search page). |
| `blocked` | Pracuj.pl showed a check page or refused the request each time it was asked, from different IP addresses. The run stops, says which searches were not run, and remembers nothing; running it again later usually works. |
| `unreadable` | A page could not be read, even after asking again. Nothing is guessed. |
| `budget-reached` | The run reached the maximum total charge you set; the rest was not returned and not charged (in monitor mode the next run returns them). |
| `bad-input` | The input could not be used (for example a region name that is not a voivodeship). Nothing was requested. |

### Input

| Field | What it does |
|---|---|
| `keywords` | What you would type into Pracuj.pl: `python`, `księgowa`, `kierowca`, `data analyst`. Each keyword is searched in each city and region. No keyword, city, region, URL or filter at all = python jobs in all of Poland. |
| `cities`, `radiusKm` | Polish towns or cities: `Warszawa`, `Kraków`, `Wrocław`, `Gdańsk`; optionally 0-100 km around them. Empty = the whole country. |
| `regions` | Voivodeships, for example `mazowieckie`, `małopolskie`, `śląskie`, or `abroad` for jobs outside Poland. Each region is one search. |
| `remoteOnly` | Only jobs listed as remote work (praca zdalna). |
| `workModes` | Office, hybrid, remote, mobile. Empty = all. |
| `contractTypes` | Employment contract, contract for specific work, mandate contract, B2B, replacement, agency, temporary work, internship. Empty = all. |
| `positionLevels` | Trainee, assistant, junior, mid, senior, expert, team leader, manager, director, president, manual worker. Empty = all. |
| `workSchedules` | Full time, part time, temporary. Empty = all. |
| `postedWithinDays` | Only jobs published within 1, 3, 7, 14 or 30 days. |
| `salaryOnly` | Only jobs that show a salary. |
| `searchUrls` | Pracuj.pl search pages copied from the browser, for example `https://www.pracuj.pl/praca/python;kw/warszawa;wp`. Each is read newest first with its own filters. |
| `maxResultsPerSearch` | Newest jobs to read per search, 1-2000. Empty = 100. In monitor mode, the window checked for new jobs and changes on each run. |
| `monitorMode` | Return only jobs that are new since the last run, salary changes and removed jobs. |
| `includeChanged`, `includeRemoved` | Monitor mode: return salary changes and removed jobs. Empty = yes. |
| `includeUnchanged` | Monitor mode: also return jobs already received that did not change (each one is charged). Empty = no. |
| `watchName` | Give each schedule its own name so separate schedules keep separate memories. |
| `resetMonitoringState` | Forget what this watch remembered; this run is a first check again. |

Up to 10 searches per run (keywords times cities and regions, plus search URLs).

### Examples

New Python jobs in Kraków or remote, every hour:

```json
{ "keywords": ["python"], "cities": ["Kraków"], "remoteOnly": true, "monitorMode": true, "watchName": "py-krakow" }
```

Senior B2B jobs in Warszawa published in the last 3 days:

```json
{ "cities": ["Warszawa"], "contractTypes": ["b2b"], "positionLevels": ["senior"], "postedWithinDays": "3" }
```

The newest 1,000 jobs in a region:

```json
{ "regions": ["mazowieckie"], "maxResultsPerSearch": 1000 }
```

### Monitor mode, step by step

1. First run: reads the newest `maxResultsPerSearch` jobs of each search, returns them once (`changeType: new`, `isFirstCheck: true`) and remembers them as the starting point.
2. Later runs: read the same window again and return only jobs published since the last run (`new`), jobs you received whose salary changed (`changed`, with the previous salary), and jobs you received that are gone from the list two runs in a row (`removed`). When nothing moved, one free `no-changes` row comes back and only the run start fee is charged.
3. Jobs not returned because of `maxResultsPerSearch` or a spending limit come back first in the next run. Nothing is judged from a list that could not be read in full.

Keep one schedule per input and `watchName`. Two overlapping runs of the same watch can both return the same new job.

### Pricing

You pay per job row returned, plus a small start fee once per run. In monitor mode the start fee pays for the check, also when nothing changed. Free rows are never charged, and unchanged jobs are not returned or charged unless you turn on `includeUnchanged`.

The first run of a watch returns and charges up to `maxResultsPerSearch` jobs per search (default 100) as the starting point; set it lower for a cheap first run.

### FAQ

**How do I get new job offers from Pracuj.pl automatically?** Turn on `monitorMode`, enter your keywords, city or a pasted search URL, and add a schedule (hourly or daily). Each run returns only the jobs published since the last run.

**Is there a Pracuj.pl API for job listings?** This Actor gives you the Pracuj.pl search results as structured rows (JSON, CSV, Excel) through the Apify API, with the same filters the site has.

**How often should I schedule it?** Hourly or daily. A busy search (for example a keyword across all of Poland) can get more new jobs in a day than a 100-job window holds; run it more often or raise `maxResultsPerSearch`, and a free row tells you when that happened.

**What counts as a salary change?** Any change of the salary text the job shows, including a salary added to a job that had none. A salary that disappears is not reported as a change.

**Can I watch remote jobs only (praca zdalna)?** Yes. Tick `remoteOnly`, or choose work modes, with or without a city.

### Good to know

- Works without a Pracuj.pl account, and reads only what the site shows anyone without logging in.
- Unofficial and not affiliated with Pracuj.pl.
- Personal contact details (names, phone numbers, emails) are not returned; the company is.
- Polski: monitor nowych ofert pracy z Pracuj.pl. Ustaw harmonogram, a każde uruchomienie zwróci tylko nowe oferty, zmiany wynagrodzenia i oferty usunięte.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

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

Schedule it with monitor mode on to get only new jobs and changes. Type what you would search on Pracuj.pl, for example python, księgowa, kierowca, data analyst. Each keyword is searched in each city and region. No keyword, city, region, URL or filter at all = python jobs in all of Poland.

## `cities` (type: `array`):

Polish towns or cities as you would type them on Pracuj.pl, for example Warszawa, Kraków, Wrocław, Gdańsk. Empty = the whole country.

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

Also include jobs this far around each city. Empty = the site's own default for a city search.

## `regions` (type: `array`):

Whole voivodeships. Each region is one search, like each city. Empty = no region filter.

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

Only jobs Pracuj.pl lists as remote work. The same as ticking home-office under Work mode.

## `workModes` (type: `array`):

Keep only jobs with any of these work modes. Empty = all.

## `contractTypes` (type: `array`):

Keep only jobs with any of these contract types. Empty = all.

## `positionLevels` (type: `array`):

Keep only jobs at any of these levels. Empty = all.

## `workSchedules` (type: `array`):

Keep only jobs with any of these working times. Empty = all.

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

Keep only jobs published within this many days. Empty = any date.

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

Keep only jobs whose listing shows pay (Pracuj.pl's own salary filter).

## `searchUrls` (type: `array`):

Set up a search on pracuj.pl with any filters, then paste the address here, for example https://www.pracuj.pl/praca/python;kw/warszawa;wp. Each URL is one more search. Single job pages are refused.

## `maxResultsPerSearch` (type: `integer`):

Newest first. Up to 2000 per search. Empty = 100. In monitor mode this is the most rows a check can return for one search, and also the size of the window that is compared.

## `monitorMode` (type: `boolean`):

Schedule it and get only new jobs. The first run returns the current list as the baseline; each later run returns only jobs that are new, whose salary changed, or that disappeared. A check with nothing new returns one free no-changes row.

## `includeChanged` (type: `boolean`):

Monitor mode only. Return a changed row with the previous salary when the salary of a job already returned changes. Empty = on.

## `includeRemoved` (type: `boolean`):

Monitor mode only. Return a removed row when a job already returned is missing from two checks in a row. Empty = on.

## `includeUnchanged` (type: `boolean`):

Monitor mode only. Also return jobs that did not change (charged like any job row). Empty = off.

## `watchName` (type: `string`):

Monitor mode only. Give each schedule its own name (letters, digits, dot, dash, underscore) so two schedules with the same searches keep separate memories. Empty = default.

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

Monitor mode only. Forget what this watch has seen; this run returns a new baseline. Untick it again for the next scheduled run.

## Actor input object example

```json
{
  "keywords": [
    "python"
  ],
  "maxResultsPerSearch": 100
}
```

# Actor output Schema

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

One row per Pracuj.pl job: ID, title, company, location, work mode, contract type, level, salary text with amounts in PLN when readable, snippet, dates and link. Monitor mode marks each row new, changed (with the previous salary), removed or unchanged. A search with no jobs, no changes or a refused request comes back as a free row that says why.

# 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 = {
    "keywords": [
        "python"
    ],
    "maxResultsPerSearch": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/pracuj-new-jobs-monitor").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 = {
    "keywords": ["python"],
    "maxResultsPerSearch": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/pracuj-new-jobs-monitor").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 '{
  "keywords": [
    "python"
  ],
  "maxResultsPerSearch": 100
}' |
apify call neverempty/pracuj-new-jobs-monitor --silent --output-dataset

```

## MCP server setup

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

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/3uKm1WtuNBN1YulWl/builds/vcfXXD2ILdXy3Qhve/openapi.json
