By the end of this tutorial, you will have a working Python script that turns nofluffjobs.com IT vacancy listings into structured data: job title, hiring company, seniority levels, skill tags, work mode, and numeric salary ranges with their contract type and currency.
No Fluff Jobs is a tech job board with markets for Poland, Hungary, Czechia, Slovakia and Ukraine. Standard web scrapers face mechanical hurdles here: searches clamp at 100 listings per search URL, and pagination relies on endpoints disallowed in robots.txt.
In this guide, we use Python and the Apify Actor No Fluff Jobs Scraper (reventadata/nofluffjobs-scraper), an unofficial tool not affiliated with No Fluff Jobs. The sample row below comes from a capture dated 2026-10-21, parsed with the Actor's own parser.
⏱️ Time estimate: one sitting: install one library, export an environment token, and run three compact Python blocks.
Prerequisites
To follow this tutorial, you will need:
-
Python 3.11+ (
apify-clientversion 3.3.0 requires Python 3.11 or newer). - The official Apify API client package.
- An Apify account and personal API token from the API & Integrations page in Apify Console.
Install the required library in your terminal:
pip install -U "apify-client~=3.0"
Export your personal authentication token to your active shell session:
export APIFY_API_TOKEN="your_apify_token_here"
What you'll achieve
You will extract clean, structured records from nofluffjobs.com directly into Python dictionaries. Each record contains the vacancy identifier, canonical URL, job title, company name, primary technology, seniority array, skill tags shown on the listing, contract type, and work mode (remote, hybrid, or onsite).
Crucially, compensation is parsed into distinct numeric fields: salaryFrom, salaryTo, salaryCurrency, and salaryPeriod. Postings that disclose pay only during interviews are flagged with salaryPublished: false and retain their interview milestone string rather than fabricated values.
Note: The Actor reads public listings only, nothing from behind an applicant login. Recruiter direct contact emails are not present on public cards and are never scraped.
Example scenario: Suppose you are monitoring senior Python roles in backend engineering that publish explicit salary ranges. You configure searchQueries: ["python"], categories: ["backend"], seniority: ["senior"], and onlyWithSalary: true. The Actor handles query validation and pushes structured records to your default dataset.
Step 1: Access No Fluff Jobs Scraper
Visit the No Fluff Jobs Scraper page on Apify Store. The documentation describes every available filter and output field. In this tutorial, we automate the entire execution through Python code, so all configuration happens directly within your script.
Step 2: Configure the search
The Actor accepts several optional criteria to focus your search across No Fluff Jobs. You can supply search terms, category selections, seniority tiers, contract types, city slugs, or salary boundaries.
Key configuration properties include:
-
searchQueries: Array of keyword phrases or tech names (e.g.["python"]). Words within an entry are combined. Do not include=symbols. -
categories: Array of site categories from the 35 supported slugs (e.g.backend,frontend,devops,fullstack,data). -
seniority: Target career levels (trainee,junior,mid,senior,expert,lead). -
onlyWithSalary: Set totrueto drop listings that withhold compensation figures until candidate interviews. -
maxItems: Caps the total number of records returned (default 100, prefill 20).
Here is your setup script. It targets senior backend vacancies mentioning Python with confirmed salaries:
import os
from apify_client import ApifyClient
# 1. Initialize ApifyClient with the token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Define the search: backend category, senior level, published salary only, at most 20 jobs
run_input = {
"searchQueries": ["python"],
"categories": ["backend"],
"seniority": ["senior"],
"onlyWithSalary": True,
"maxItems": 20,
}
No proxy configuration is needed in run_input: the proxy setting defaults to disabled, and testing on 2026-10-21 confirmed the site answered directly without requiring proxy egress.
Step 3: Add optional filters
Beyond primary keywords, several additional parameters allow fine-tuning dataset results before and after fetching:
| Parameter | Type | Purpose |
|---|---|---|
employmentTypes |
array | Select contract types: b2b, permanent (UoP), or zlecenie (mandate). |
cities |
array | City slugs such as warszawa, krakow, wroclaw, or work modes remote and hybrid. |
salaryAmount |
integer | Not a minimum. It is the site's own match on ads whose published range contains this value, and it also matches ads that reveal pay only at an interview stage; add onlyWithSalary to drop those. |
salaryCurrency |
string | Currency for salary filtering (pln, eur, usd, gbp, chf). Defaults to pln. Only applies together with salaryAmount. |
salaryPeriod |
string | Period for salary filtering (month or hour). Defaults to month. Only applies together with salaryAmount. |
country |
string | Market regional portal (pl, hu, cz, sk, ua). Defaults to pl. |
postedWithinDays |
integer | Filters cards posted or renewed within the preceding N days. |
includeDetails |
boolean | Opens individual vacancy pages to gather full descriptions, daily tasks, and company sizes. |
Three example configurations:
-
Warsaw B2B contracts:
cities: ["warszawa"],employmentTypes: ["b2b"],categories: ["devops"]. -
Recent remote roles:
cities: ["remote"],postedWithinDays: 7,onlyWithSalary: true. -
International markets:
country: "hu",categories: ["data"],salaryAmount: 3000,salaryCurrency: "eur".
⚠️ The site silently converts unrecognized filter parameters into free-text search queries. To prevent corrupted result sets, the Actor validates all enumeration inputs prior to execution and checks that the response's
criteriaSearchechoes what it sent.
Step 4: Run the Actor
Trigger execution using client.actor("reventadata/nofluffjobs-scraper").call(run_input=run_input). This method dispatches the task to Apify cloud infrastructure and blocks until the crawl finishes:
# 3. Start the Actor run and wait for it to finish
print("Starting No Fluff Jobs extraction...")
run = client.actor("reventadata/nofluffjobs-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
status = run.status if run else "unknown"
raise SystemExit(f"Run did not succeed (status: {status}).")
# 4. Fetch the saved job rows
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} jobs.\n")
Execution mechanics:
- The Actor validates all requested filter parameters against supported site enumerations.
- It fetches listing cards from the Angular server state without calling disallowed internal API routes.
- Because a single search URL returns at most 100 listings, broader sweeps are split by category, then seniority, then contract type, only on the filters you left unset.
- Postings are deduplicated by their unique 8-character reference identifier before records are pushed to storage.
Step 5: View and download results
Iterate through the retrieved dataset items to print the extracted job specifications:
for item in items:
print(item["title"])
print(f" Company: {item.get('company') or 'undisclosed'}")
print(f" Seniority: {', '.join(item.get('seniority') or [])}")
print(f" Work mode: {item.get('workMode')} (fullyRemote: {item.get('fullyRemote')})")
if item.get("salaryPublished"):
print(f" Salary: {item['salaryFrom']} - {item['salaryTo']} {item['salaryCurrency']}/{item['salaryPeriod']} ({item.get('contractType')})")
else:
print(f" Salary: disclosed {item.get('salaryDisclosedAt')}")
print(f" Skills: {', '.join(item.get('skills') or [])}")
print("-" * 40)
Below is an authentic sample row from a capture dated 2026-10-21 (page 1 of the backend category search on nofluffjobs.com/pl/), parsed with the Actor's own parser. A saved row also carries scrapedAt, the UTC timestamp of extraction:
{
"id": "SKQKAXAX",
"url": "https://nofluffjobs.com/pl/job/senior-software-engineer-with-python-codilime-remote-2",
"title": "Senior Software Engineer with Python",
"company": "CodiLime",
"category": "backend",
"technology": "Python",
"seniority": [
"Senior"
],
"skills": [
"Python",
"AI",
"Testing",
"API"
],
"jobLanguages": [],
"contractType": "b2b",
"salaryFrom": 20500,
"salaryTo": 30000,
"salaryCurrency": "PLN",
"salaryPeriod": "Month",
"salaryPublished": true,
"salaryDisclosedAt": "VISIBLE",
"workMode": "remote",
"fullyRemote": false,
"locations": [
{
"city": "Warszawa",
"street": "Grzybowska 5A",
"postalCode": "00-132",
"country": "POL",
"latitude": 52.23689400000001,
"longitude": 21.0008258
}
],
"provinces": [
"lower-silesian",
"kuyavian-pomeranian",
"lodz",
"lublin",
"lubusz",
"lesser-poland",
"opole",
"subcarpathian",
"podlaskie",
"pomeranian",
"silesian",
"holy-cross",
"warmian-masurian",
"greater-poland",
"west-pomeranian"
],
"postedAt": "2026-10-19T12:27:59+00:00",
"renewedAt": "2026-10-21T12:27:59+00:00",
"regions": [
"pl"
],
"search": "category=backend"
}
Understanding your results
The schema provides structured fields that distinguish reliable compensation figures from interview disclosures.
In the capture of 20 backend postings dated 2026-10-21, 16 postings included published salary ranges, while 4 withheld figures until interview milestones. The hidden roles recorded interview stage markers: 1 at AT_FIRST_INTERVIEW, 2 at AFTER_LAST_INTERVIEW, and 1 at AT_SECOND_INTERVIEW. Both salaryFrom and salaryTo remain null for these entries, allowing clean filtering via salaryPublished.
The same 20-card sample contained diverse contract arrangements and currencies: 17 listings specified PLN, 2 specified USD, and 1 specified EUR. Across contract types, 18 were B2B agreements and 2 were permanent employment contracts (UoP). Pay frequency comprised 11 hourly rates, 7 monthly salaries, and 2 daily rates.
Workplace flexibility also varied: 13 positions had workMode hybrid, 6 remote, and 1 onsite (fullyRemote was false on all 20). Seniority distribution spanned 11 Senior roles, 5 Mid roles, 3 Expert positions, and 1 Junior vacancy. All 20 postings belonged to the Polish regional market (regions: ['pl']).
Pro tips and advanced usage
When integrating recruitment data pipelines into analytics dashboards or internal databases, consider these practical techniques:
-
Automate recurring runs with Schedules. Navigate to the Schedules tab in Apify Console to execute scrapes periodically using cron expressions. For instance, setting
0 8 * * *triggers extraction daily at 8:00 AM UTC to harvest recently added tech postings. -
Dispatch notifications via Webhooks. Configure an
ACTOR.RUN.SUCCEEDEDwebhook in the Integrations panel. Apify delivers an HTTP POST payload containing the run object and its associateddefaultDatasetIddirectly to your destination endpoint or Slack webhook. - Export across multiple formats. Apify dataset storage supports instant export formats including JSON, CSV, XML, Excel, and JSONL via direct HTTP query parameters.
-
Target deep ad specs when required. If your use case demands company size, full requirements text, and daily task lists, activate
includeDetails: true. The Actor opens each vacancy URL with controlled concurrency to compile comprehensive ad records.
Troubleshooting common issues
If your scraping pipeline encounters unexpected outcomes, verify these frequent configuration checks:
-
Zero rows pushed: The Actor raises a runtime error if no rows match your search. Verify city slugs against live website URLs (for example
warszawa). Ensure search keywords do not include=symbols. - Input rejected prior to launch: Apify validates schema constraints before task initiation. Invalid category names, unsupported currencies, or non-numeric salary values are rejected immediately.
- Search truncation at 100 jobs: A single search URL clamps at 100 listings. When extracting high-volume categories, leave category, seniority and contract type unset so the Actor can split the search on them; if a search is still capped, narrow it with cities or a keyword.
-
Missing salary numbers: Many postings defer salary disclosure to interviews. If your analysis strictly requires compensation figures, enable
onlyWithSalary: trueto discard undisclosed listings.
Pricing and next steps
For current pricing and details, see the Store page.
From here, you can load your dataset into a pandas DataFrame for salary analysis, set up scheduled recurring runs to track fresh tech openings, or wire an Apify webhook into your team's internal applicant tracking dashboard.
What tech stacks, seniority tiers, or salary trends across Central European tech hubs are you looking to track? Let me know in the comments below!
Top comments (0)