# CEE Tech Jobs Scraper — justjoin.it + pracuj.pl + NoFluffJobs (`atlasdataworks/cee-tech-jobs-scraper`) Actor

One actor, three Polish tech-job boards, one normalized schema with salary normalization and cross-board dedup. Pay-per-event.

- **URL**: https://apify.com/atlasdataworks/cee-tech-jobs-scraper.md
- **Developed by:** [Atlas Dataworks](https://apify.com/atlasdataworks) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 job scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## CEE Tech Jobs Scraper — justjoin.it, pracuj.pl, NoFluffJobs in one actor

Scrape **Polish tech job offers** from the three boards that matter — [justjoin.it](https://justjoin.it), [it.pracuj.pl](https://it.pracuj.pl), and [nofluffjobs.com](https://nofluffjobs.com) — in a single run, with **one normalized schema**, **normalized salaries**, and **cross-board deduplication**. Built for job-market analytics, salary benchmarking, recruiting intelligence, and AI agents.

### Why this actor

- **3 boards, 1 schema.** Every existing scraper covers a single board with its own field names. This one returns identical records regardless of source, so your pipeline has exactly one shape to handle.
- **Salary normalization.** `"13 000–15 000 zł / mies. (zal. od umowy)"`, NoFluffJobs salary objects, and schema.org `baseSalary` blocks all become `{min, max, currency, period, gross}`. Currency is recorded as published — no FX guessing.
- **Cross-board dedup.** The same job posted on two or three boards becomes **one record** with a `sources[]` array listing every board and URL it appeared on.
- **Freshness.** Runs read the boards live; `postedAt` / `expiresAt` come from the boards themselves.

### Output schema

One record per job, pushed to the default dataset:

| Field | Type | Notes |
|---|---|---|
| `source` | `justjoin` | `pracuj` | `nofluffjobs` | Board the record came from (first board, if merged) |
| `sourceUrl` | string | Canonical offer URL |
| `externalId` | string | Board-native id / slug |
| `title` | string | Job title |
| `company` | string | Employer name |
| `locations` | `{city?, country?}[]` | All named workplaces; `country` normalized to ISO alpha-2 (best effort — unrecognized values pass through as published) |
| `remote` | `remote` | `hybrid` | `onsite` | `unknown` | Normalized work mode |
| `seniority` | string? | `junior` / `mid` / `senior` / `expert` / `manager` / `intern` when derivable |
| `contractTypes` | string\[]? | e.g. `b2b`, `permanent`, `mandate` |
| `salary` | `{min?, max?, currency, period, gross?}`? | Only when the board discloses it; one-sided offers ("od 8 000 zł") keep only the disclosed bound |
| `techStack` | string\[]? | Technologies named by the board |
| `postedAt` / `expiresAt` | ISO date? | As published |
| `description` | string? | Full description — only with **Fetch full details** |
| `scrapedAt` | ISO date | Run timestamp |
| `sources` | `{source, sourceUrl}[]`? | Present when boards were merged by dedup |

### Input

```json
{
  "boards": ["justjoin", "pracuj", "nofluffjobs"],
  "keyword": "python",
  "maxJobsPerBoard": 100,
  "fetchDetails": false
}
```

- `keyword` — server-side search on NoFluffJobs; title/company/tech-stack match on justjoin.it and pracuj.pl.
- `maxJobsPerBoard` — strict per-board cap.
- `fetchDetails` — adds full descriptions (justjoin.it JSON-LD, NoFluffJobs detail API) and fires the premium `job-detail` event.
- `proxyConfiguration` — used **only** for pracuj.pl; the default (Apify datacenter group `BUYPROXIES94952`) is the cheapest tier that passes.

### Pricing (pay-per-event)

| Event | Fires | Price |
|---|---|---|
| `job-scraped` | per normalized job record delivered | **$0.0015** ($1.50 / 1,000 jobs) |
| `job-detail` | per record enriched with a full description | **$0.005** ($5.00 / 1,000 details) |
| `Actor Start` | once per run, charged by the platform | $0.00005 ($0.05 / 1,000 runs) |
| `Result` | per record stored, charged by the platform | $0.00005 ($0.05 / 1,000 records) |

`job-detail` is charged **in addition to** `job-scraped`, not instead of it — a job delivered
with its full description costs $0.0065. Details are charged after deduplication, over records
actually delivered.

The last two rows are Apify platform events rather than ours. They are small (a 10,000-job run
pays about $0.50 across both) and they are listed here so this table matches the pricing shown
on the Store page exactly. Platform usage — compute and proxies — is bundled: there is no
separate usage line on your bill.

You pay for delivered records, not runtime. Runs respect your budget cap
(`maxTotalChargeUsd`): when the cap is reached the actor stops delivering,
reports how much of the result set was delivered, and never charges past it.

### Using it from an AI agent (MCP)

The actor is callable as an MCP tool via Apify's MCP server. Example agent prompt:

> Call `cee-tech-jobs-scraper` with `{"keyword": "data engineer", "maxJobsPerBoard": 50}` and summarize median salary by seniority.

Or via the API:

```bash
curl -X POST "https://api.apify.com/v2/acts/<user>~cee-tech-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keyword": "python", "maxJobsPerBoard": 100}'
```

### Honest limits

- **justjoin.it depth:** one listing page yields ~100 offers per run (the listing is an infinite-scroll app). Enough for fresh-offers monitoring; not a full-board dump.
- **pracuj.pl descriptions:** listing-level data only in this version — full pracuj descriptions are on the roadmap.
- **Dedup key** is normalized company+title, matched across boards. Two consequences to know about: (a) *false negatives* — the same employer often spells its name differently per board ("Acme" vs "Acme Sp. z o.o."), and those records will NOT merge; (b) *over-merges* — distinct openings sharing both company and title (e.g. the same role posted separately per city) merge into one record, with every posting's URL kept in `sources[]` and all locations unioned. `sources[]` can therefore also list the same board twice.
- **Keyword runs on pracuj.pl and justjoin.it filter client-side** (title/company/tech match) after fetching listing pages, so a keyword run can return fewer than `maxJobsPerBoard` records from those boards even when more matches exist deeper in the listing. NoFluffJobs applies the keyword server-side and does not have this limit.
- **Salary `gross`** is only set when the board says brutto/netto explicitly; B2B rates carry no gross/net flag by nature.
- **No FX conversion** by design — currency is reported as published (almost always PLN).
- Boards change their markup; the pracuj extractor has a built-in DOM fallback, but a board redesign can still degrade a run. Scheduled test runs guard freshness.

# Actor input Schema

## `boards` (type: `array`):

Which boards to scrape. Defaults to all three.

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

Optional keyword (e.g. "python", "devops"). Applied server-side on NoFluffJobs, client-side (title/company/tech stack match) on justjoin.it and pracuj.pl.

## `maxJobsPerBoard` (type: `integer`):

Hard cap on jobs collected from each board.

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

When enabled, full job descriptions (justjoin.it JSON-LD, NoFluffJobs detail API) are included and the premium job-detail event is charged per detailed record.

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

Proxy used ONLY for pracuj.pl (it blocks datacenter-naked egress). NoFluffJobs and justjoin.it are fetched directly. Verified working: Apify datacenter group BUYPROXIES94952.

## Actor input object example

```json
{
  "boards": [
    "justjoin",
    "pracuj",
    "nofluffjobs"
  ],
  "maxJobsPerBoard": 100,
  "fetchDetails": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "BUYPROXIES94952"
    ]
  }
}
```

# Actor output Schema

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

One record per unique job across every selected board. Salaries are parsed into {min, max, currency, period, gross} with the currency recorded as published. A job posted on more than one board appears once, with every board and URL listed in sources\[].

## `runSummary` (type: `string`):

Per-run counts and health: boards queried, degradedBoards, rawCount, uniqueCount, mergedCount, deliveredCount, detailCount and chargeLimitReached. Check degradedBoards to tell a genuinely empty result apart from a board that failed.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("atlasdataworks/cee-tech-jobs-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("atlasdataworks/cee-tech-jobs-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 '{}' |
apify call atlasdataworks/cee-tech-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,atlasdataworks/cee-tech-jobs-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/8YU9i9JP7y6vlvRlk/builds/sFdJSdggzxVPPFfdD/openapi.json
