Track new job postings from Naukri, StepStone and Job Bank Canada for a niche role (Python) A developer built a Python script that queries Apify Actors for Naukri.com, StepStone.de and Canada's Job Bank once a day, de-duplicates postings locally by job ID, and appends only unseen listings to a CSV. Job Bank's feed mode tracks delivered jobs for 60 days per search, while Naukri and StepStone rely on their own postedWithinDays filters plus local de-duplication. The script normalizes differing field names across the three boards into a single CSV schema. Disclosure: I built the Apify Actor used in this tutorial. This post was written with AI assistance. If you are watching the market for one specific role, say data engineer, across more than one country, the job boards' own alert e-mails give you one list per board, with no salary column and no record of what you have already seen. This tutorial replaces them with a Python script that runs once a day, asks three boards for recent postings, keeps only the ones it has not seen before, and appends them to a CSV you can sort and filter. The three boards are India's Naukri.com, Germany's StepStone.de and Canada's Job Bank, each through an Apify Actor: All three charge only for jobs delivered. Unknown links and unknown places come back as free records with a reason; Job Bank also returns searches with no jobs as free records. You need Python 3.11 or newer and an Apify https://apify.com API token. pip install apify-client export APIFY TOKEN=apify api xxxxxxxx The boards differ in one thing that shapes the script. Job Bank Canada has a feed mode. Set onlyNewListings: true and the Actor remembers every job it has delivered for that exact search in a key-value store in your Apify account, for 60 days per job. The first run returns everything; later runs return only jobs not seen before, each with a firstSeenAt timestamp, and seen jobs are neither saved nor charged. Naukri and StepStone do not , so for those we use the boards' own date filter postedWithinDays to fetch only recent postings, and de-duplicate locally by job ID. The local de-duplication is applied to all three boards anyway: it is cheap, and it makes the script robust if you ever change a Job Bank filter which resets its feed memory . One detail that will bite you if you copy inputs between Actors: Naukri's postedWithinDays is a string "1" , "3" , "7" , "15" , "30" , because it maps to Naukri's own freshness filter; StepStone's and Job Bank's are integers. Each Actor accepts keywords plus a place. Naukri takes a list of locations and searches every keyword in every place; StepStone and Job Bank take a single location string. ROLE = "data engineer" SEARCHES = { "naukri": "highbrow fame/naukri-jobs", { "keywords": ROLE , "locations": "Bangalore", "Hyderabad" , "postedWithinDays": "1", "sortBy": "date", "maxListingsPerSearch": 200, } , "stepstone": "highbrow fame/stepstone-jobs", { "keywords": ROLE , "location": "Berlin", "radiusKm": 50, "postedWithinDays": 1, "sortBy": "newest", "includeRelatedJobs": False, "includeDetails": True, "maxJobsPerSearch": 200, } , "jobbank": "highbrow fame/job-bank-canada-jobs", { "keywords": "engineer" , single word: see the note below "location": "Toronto", "sortBy": "newest", "onlyNewListings": True, "maxJobsPerSearch": 200, } , } Three settings deserve a comment: includeRelatedJobs: False keeps only keyword matches; left on, every record carries a match field keyword or related for filtering later. includeDetails on to get the estimate; opening the ads costs nothing extra, only time. Set it to False if you only need the search cards. engineer . Test your phrase on jobbank.gc.ca first; if it finds nothing, use a single keyword. The boards return different field names. Pick the handful you want in the CSV and map each board to them: python def normalize board, j : if board == "naukri": return { "id": j.get "jobId" , "title": j.get "title" , "company": j.get "company" , "location": ", ".join j.get "locations" or , "salary": j.get "salary" or "", "posted": j.get "postedAt" , "url": j.get "url" , } if board == "stepstone": salary = "" if j.get "estimatedSalaryMin" : salary = f"est. {j 'estimatedSalaryMin' }-{j 'estimatedSalaryMax' } " f"{j.get 'estimatedSalaryCurrency' }/{j.get 'estimatedSalaryPeriod' }" return { "id": j.get "jobId" , "title": j.get "title" , "company": j.get "company" or "", "location": j.get "location" , "salary": salary, "posted": j.get "postedAt" , "url": j.get "url" , } return { jobbank "id": j.get "jobId" , "title": j.get "title" , "company": j.get "employer" or " private household ", "location": f"{j.get 'city' }, {j.get 'province' }", "salary": j.get "salary" or "", "posted": j.get "postedDate" , "url": j.get "url" , } What to expect in the salary column: Naukri shows the employer's figure when published in one of my live runs about 60% of ads said "Not disclosed" and the parsed fields stayed empty . StepStone shows no employer salaries to logged-out visitors; you get StepStone's own estimate, which exists for most ads. Job Bank postings state the wage as written, with salaryMin , salaryMax and salaryPeriod parsed alongside. python import csv, json, os, sys from datetime import date from pathlib import Path from apify client import ApifyClient client = ApifyClient os.environ "APIFY TOKEN" SEEN, OUT = Path "seen.json" , Path "jobs.csv" FIELDS = "first seen", "board", "id", "title", "company", "location", "salary", "posted", "url" seen = set json.loads SEEN.read text if SEEN.exists else set new rows = for board, actor name, run input in SEARCHES.items : run = client.actor actor name .call run input=run input for item in client.dataset run.default dataset id .iterate items : if item.get "status" not in None, "OK" : print f"{board}: {item.get 'status' } {item.get 'error' }", file=sys.stderr continue row = normalize board, item key = f"{board}:{row 'id' }" if key in seen: continue seen.add key new rows.append {"first seen": date.today .isoformat , "board": board, row} write header = not OUT.exists with OUT.open "a", newline="", encoding="utf-8" as f: w = csv.DictWriter f, fieldnames=FIELDS if write header: w.writeheader w.writerows new rows SEEN.write text json.dumps sorted seen print f"{len new rows } new postings, {len seen } known in total" Notes on the code: call blocks until the run ends, so the three runs go one after another. status other than OK are the free "nothing found" records an unknown place, an empty search ; they go to stderr, not the CSV. seen.json is a flat list of board:jobId strings and grows only by the day's new postings. SQLite with a unique index on board, id is the natural upgrade. Put SEARCHES and normalize in the same file above this block, save it as jobs.py , and run it once by hand: python jobs.py The first run fills jobs.csv with everything the date filters allow and, for Job Bank, everything in the search, because the first feed run returns all . From the second run on you see only new postings. The simplest scheduler is cron on any machine that is on at the time: 0 8 cd /home/you/jobwatch && APIFY TOKEN=apify api xxx /usr/bin/python3 jobs.py jobs.log 2 &1 A GitHub Actions workflow on a schedule: trigger works too if the job commits seen.json and jobs.csv back to the repository. Apify's own Schedules could run each Actor daily, but the cross-board de-duplication and the merged CSV live in this script, so the script is what needs scheduling. If you also want a push notification, Job Bank's Actor accepts an alertWebhookUrl and POSTs a short JSON summary new count, up to five sample items, dataset link to Slack, Discord or any HTTPS endpoint whenever a feed run finds new jobs. For Naukri and StepStone, send your own message from the script when new rows is not empty. Worst case on the first day is 200 jobs from each of the four searches two Naukri locations, StepStone, Job Bank , about 800 records: 400 × $0.0008 + 200 × $0.0009 + 200 × $0.0008 = $0.66. After that each run is billed only for the jobs it delivers, at $0.80 per 1,000 on Naukri and Job Bank and $0.90 per 1,000 on StepStone. url , and you apply on the board. Full input schemas, example records and pricing are on the Actor pages: Naukri https://apify.com/highbrow fame/naukri-jobs , StepStone https://apify.com/highbrow fame/stepstone-jobs and Job Bank Canada https://apify.com/highbrow fame/job-bank-canada-jobs .