By the end of this tutorial you will have a Python script that pulls Ukrainian job vacancies from robota.ua into structured rows: title, employer, city, stated salary bounds, skills, schedule, contact fields and posting timestamp.
robota.ua is a Ukrainian job board whose pages load as an Angular shell and fetch vacancy data from a GraphQL endpoint behind Cloudflare. In tests from a Mac on 7 November 2026, curl hit a Cloudflare 403 challenge on / and /robots.txt, and a browser-TLS HTTP client hit 403 on the GraphQL API instead of JSON. robota.ua Scraper (reventadata/robota-ua-scraper) is the Apify Actor I built to query that endpoint through Apify Proxy and return one typed row per vacancy.
This guide drives it from Python with the Apify API client, and every output line and sample row below comes from a real 20-row run of a python search on 14 November 2026.
Prerequisites
You need three things:
- Python 3.11 or newer (required by the 3.x Apify API client for Python).
- A free Apify account, which you can create with an email address or a GitHub login.
- Your API token, from Settings > API & Integrations in Apify Console.
Install the client and export your token:
pip install -U "apify-client~=3.0"
export APIFY_API_TOKEN="your_apify_token_here"
No browser automation or scraping library is needed on your machine; the Actor handles fetching on Apify.
What you'll achieve
You will run a python keyword search on robota.ua with includeDetails enabled, wait for the run to finish, read its 20 rows in Python and print a two-line summary per vacancy showing title, employer, city, stated salary, remote flag, category and relative posting time.
You will also see how robota.ua data behaves in practice: most listings omit salary numbers, a Kyiv location can still be marked remote in its schedule tags, and one vacancy in the sample run came from an anonymous employer with company set to null. The same script works for any Ukrainian or English keyword by changing the input dictionary.
Step 1: Open robota.ua Scraper
Open the robota.ua Scraper page on Apify Store. The Try for free button opens the Actor in Apify Console if you want to inspect the form. Because this tutorial drives everything from Python, you only need the Actor slug reventadata/robota-ua-scraper.
Step 2: Configure the search
The Actor accepts these main input fields:
-
searchQueries: list of job titles or keywords, one search per term (for examplepython,водій,sales manager). Duplicates across terms are dropped by vacancyidafter saving. Leave it empty to pull the site's default-ranked list across all topics. -
maxItemsandmaxPages: row cap across all queries (default100, minimum1) and pages per query (default10, up to500;20vacancies per page). -
includeDetails: whentrue(defaultfalse), opens each kept vacancy page to add18extra fields includingdescription,postedAt,category,schedules,isRemote,skillsand published contacts. -
city: case- and accent-insensitive substring match onlocation. -
minSalary,maxSalary,onlyWithSalary: salary bounds compared as the API returns them (the API gives no currency field). Vacancies without a stated salary are dropped whenever any of these is set. -
includeKeywords,excludeKeywords: case- and accent-insensitive substring matches acrosstitle,companyandsnippet(for example,"python"also matches"pythonista"). -
proxyConfiguration: Apify Proxy is enabled by default with no group named; switch toRESIDENTIALif a run hits a block.
The city, salary and keyword filters run after each page is read, so they drop rows rather than reducing pages fetched. Here is an example input that narrows two keywords to Kyiv salaries from 60000 upwards:
{
"searchQueries": ["python", "data engineer"],
"city": "Київ",
"onlyWithSalary": true,
"minSalary": 60000,
"maxItems": 40,
"maxPages": 3
}
Step 3: Run it from Python
This script starts the Actor, checks that the run succeeded, reads the dataset and prints two lines per vacancy:
import os
from apify_client import ApifyClient
# 1. Initialise the client with your token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Describe the search: "python" vacancies with full detail pages
run_input = {
"searchQueries": ["python"],
"maxItems": 20,
"maxPages": 2,
"includeDetails": True,
}
# 3. Start the Actor and wait for it to finish
run = client.actor("reventadata/robota-ua-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. Read the rows from the run's default dataset
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} vacancies.\n")
# 5. Print two lines per vacancy
for item in items:
salary_from, salary_to = item.get("salaryFrom"), item.get("salaryTo")
if salary_from is not None and salary_to is not None:
salary = f"{salary_from}-{salary_to}"
elif salary_from is not None:
salary = f"from {salary_from}"
elif salary_to is not None:
salary = f"up to {salary_to}"
else:
salary = "not stated"
remote = "remote" if item.get("isRemote") else "on-site/hybrid"
print(f"{item['title']} | {item.get('company') or 'anonymous'} | {item.get('location')} | salary: {salary} | {remote}")
print(f" category: {item.get('category')}, posted: {item.get('postedText')}")
With includeDetails set to True, the Actor issues one extra request per kept vacancy, so keep maxItems modest while testing.
Step 4: Read the output
The cited run on 14 November 2026 finished in 11.5 seconds and returned 20 vacancies (18 from page 1 and 2 from page 2). Here are the first five lines printed by the loop:
ВІ аналітик | Дарниця, Фармацевтична компанія | Київ | salary: not stated | on-site/hybrid
category: Фармацевтика, posted: 3 години тому
AppSec Engineer (Application Security Engineer) | robota.ua | Київ | salary: not stated | remote
category: IT, posted: 23 години тому
Middle QA Engineer (ML / AI & Data) | robota.ua | Київ | salary: not stated | remote
category: IT, posted: 1 день тому
Провідний фахівець з HR-систем, автоматизації та аналітики | UNIQA, Страхова компанія | Київ | salary: not stated | on-site/hybrid
category: Страхування, posted: 1 день тому
Менеджер/Менеджерка з моделювання та планування продуктів зі зниженим ризиком / Бізнес-аналітик | JTI / Japan Tobacco International | Київ | salary: not stated | on-site/hybrid
category: FMCG, posted: 1 день тому
Every row had location set to Київ, yet two of those first five (isRemote: true) support remote work according to their schedules.
Step 5: Inspect a complete row
When includeDetails is True, each row carries all 42 fields. Here is one complete row copied verbatim from that 14 November 2026 run:
{
"id": "11400591",
"url": "https://robota.ua/company15469395/vacancy11400591",
"title": "Data Scientist (LLM/AI)",
"company": "NDA Recruitment",
"companyId": "15469395",
"companyLogo": "https://company-logo-frankfurt.rabota.ua/15469395_20250619204422.png",
"location": "Київ",
"cityId": "1",
"scheduleIds": [
"8",
"1"
],
"salaryFrom": 121500,
"salaryTo": 150000,
"salaryComment": null,
"snippet": "Про нас:Ми — одна з найбільших українських MilTech-компаній. Активно впроваджуємо AI-рішення для оптимізації та автоматизації внутрішніх бізнес-процесів. Розширюємо нашу LLM & AI команду і шукаємо людину, яка не просто запускає ChatGPT через API, а г...",
"postedText": "2 дні тому",
"isHot": false,
"isActive": true,
"isAnonymous": false,
"publicationType": "PROFESSIONAL",
"benefits": [
"Бронювання співробітників"
],
"applyUrl": null,
"matchedKeyword": "python",
"page": 1,
"detailFetched": true,
"postedAt": "2026-11-12T03:01:00.027000+02:00",
"description": "Про нас:\nМи — одна з найбільших українських MilTech-компаній. Активно впроваджуємо AI-рішення для оптимізації та автоматизації внутрішніх бізнес-процесів. Розширюємо нашу LLM & AI команду і шукаємо людину, яка не просто запускає ChatGPT через API, а готова створювати надійні продуктові ML-рішення.\nЩо треба буде робити:\n- Проєктувати промпти, структуру контексту та будувати нормальні RAG-системи (chunking, retrieval, reranking).\n- Розробляти агентні системи з tool use та валідацією виходів.\n- Будувати пайплайни evaluation (golden datasets, метрики, LLM-as-judge) перед релізом, щоб моделі не галюцинували в найвідповідальніший момент.\n- Проводити fine-tuning (за потреби), писати чистий production-код на Python та дбати про безпеку рішень (guardrails, injection-тести).\nНаші очікування від кандидата:\n- 3+ роки досвіду в DS/ML, з них від 1 року — практична робота з LLM у реальних продакшн-проєктах.\n- Досвід побудови RAG або агентних систем своїми руками (і розуміння, чому саме такі архітектурні рішення були обрані).\n- Практика evaluation: збір golden datasets, вибірка метрик і відстеження деградацій.\n- Впевнений Python (тести, рев'ю, структура сервісу) та вміння пояснити складні технічні речі нетехнічним стейкхолдерам.\nЧому варто долучитися:\nТут немає нудних пет-проєктів заради слайдів. Ваші рішення матимуть реальний вплив на внутрішні процеси та допоможуть наближати перемогу України.\nЗацікавила вакансія?",
"address": null,
"district": null,
"metro": null,
"latitude": 50.4500351,
"longitude": 30.5241356,
"category": "HR",
"schedules": [
"гібридна",
"повна зайнятість"
],
"isRemote": false,
"skills": [
"Мова програмування python",
"Machine Learning Engineer",
"Дата Саєнтист",
"Дата Майнинг",
"Середній розробник Python"
],
"contactName": "Вікторія Н.",
"contactPhones": [],
"contactSocials": [
"viktoriia.n@nda.in.ua"
],
"companyVerified": true,
"companyFoundedYear": null,
"companySize": "від 20 до 50",
"companyBenefits": [
"Їжа в офісі",
"Офіційне працевлаштування згідно КЗпП України",
"Повністю офіційна зарплата"
],
"scrapedAt": "2026-11-14T14:45:19+00:00"
}
Understanding your results
Each row holds 42 fields: 24 list and run metadata fields plus 18 detail fields that stay null when includeDetails is false:
-
salaryFrom,salaryTo,salaryComment: numeric bounds as robota.ua states them (0becomesnull), with no currency field in the API. In the 20-row run, 2 vacancies stated a salary (121500–150000and an open-ended75000) and 18 stated none. -
postedTextvspostedAt: the list gives relative Ukrainian text ("2 дні тому"), whilepostedAtadds the ISO 8601 timestamp with theEurope/Kyivoffset (+02:00here) whenincludeDetailsistrue. -
schedulesandisRemote:isRemoteistruewhenschedulesincludes"віддалена робота"(5 of 20 rows in the run). -
companyandisAnonymous: anonymous listings setcompanyandcompanyIdtonull(1 of 20 rows). -
contactName,contactPhones,contactSocials,applyUrl: populated only when published by the employer (contactNameappeared in 14 of 20 rows,contactSocialsin 3 of 20 rows,contactPhonesin 1 of 20 rows, and an externalapplyUrlin 1 of 20 rows).
Pro tips
-
Leave
includeDetailsoff until you need full text or exact dates. List-only runs fetch one request per 20 vacancies instead of 21, andscheduleIds("3"appeared on all five remote vacancies in the sample) andsnippetstill arrive on the list. -
Expect sparse salaries. Because 18 of 20 vacancies in the sample stated no salary, setting
onlyWithSalary: truedrops most rows after fetching. RaisemaxPagesif you filter on salary. -
Batch related terms in
searchQueries. A vacancy returned under two keywords is pushed once, keeping your dataset deduplicated byid. -
Separate recruiting agencies via
category. Only 3 of the 20pythonvacancies were underIT;Банкиheld 4 andHRheld 3 (including agency postings for technical roles). -
Handle missing detail pages gracefully. If a vacancy disappears before its detail request or fails to load, the Actor keeps the list fields and sets the 18 detail fields to
null.
Troubleshooting
-
Zero rows, status
SUCCEEDED. The API returnedtotalCount: 0with no errors, or post-fetch filters dropped every candidate. The status message shows the rejection tally; loosencity, salary bounds orincludeKeywords. -
Zero rows, status
FAILED. EithermaxItemsormaxPageswas below1,minSalaryexceededmaxSalary, or every list page was blocked or returned an unexpected shape. If blocked, try settingproxyConfigurationto Apify Proxy'sRESIDENTIALgroup. -
Fewer rows than expected. Check the log via
client.log(run.id).get(). It ends withSkipped N: reasoncounts andPushed N row(s)..
robota.ua's terms prohibit unlawful collection of personal data and interference with site operation; verify that your use complies with those terms and applicable data-protection law.
Next steps
For current pricing and details, see the Store page.
Save your input as an Apify task and attach a schedule to monitor new vacancies automatically. Datasets export as JSON, CSV, Excel, XML, HTML table, RSS or JSONL (dataset docs), and webhooks deliver the finished run object including defaultDatasetId to your endpoint (webhook docs).
What roles or cities are you tracking on robota.ua? Drop a comment! 💬
About the author: reventadata builds and publishes data extraction Actors on the Apify Store.
Top comments (0)