DEV Community

Your Evenings Data
Your Evenings Data

Posted on Edited on

Ontario nurse job postings from Canada's Job Bank open data, into a CSV every month

Article type: Tutorial

Prerequisites: curl, jq and awk for the free route. An Apify API token for the scheduled Actor route. cron (or an Apify schedule) to run it monthly or weekly.

Tested on: 31 October 2026

Canada's national Job Bank publishes every posting it carries as open data: one file a month, from January 2023, on open.canada.ca. If you track hiring for one occupation in one province, for a workforce report, a settlement service or a pay comparison, that file has what you need: occupation code, place, vacancies, pay range, hours and the date the job was first posted. It does not include employer names, contact details or apply links.

This post pulls one slice, registered nurses (NOC 2021 code 31301) in Ontario, first with nothing but curl and awk, then as a scheduled job that adds each new month to a CSV.

Disclosure: the hosted Actor in the second half is ours (we built it) and it's paid per row. The free route comes first and is enough for a one-off.

The free route: one file, one awk line

The dataset record lists every monthly file. This gets the newest English one:

URL=$(curl -s "https://open.canada.ca/data/api/action/package_show?id=ea639e28-c0fc-48bf-b5dd-b8899bd43072" \
  | jq -r 'first(.result.resources[] | select(.format=="CSV" and (.language|index("en"))) | .url)')
curl -sL -o latest.csv "$URL"
Enter fullscreen mode Exit fullscreen mode

On 31 October 2026 that was the September 2026 file: 46 MB, UTF-16 and tab-separated, with 65 columns and 52,368 postings. Convert it, then filter on NOC code (column 6) and province (column 17):

iconv -f UTF-16 -t UTF-8 latest.csv | tr -d '\r' > latest.tsv
awk -F'\t' 'NR>1 && $6=="31301" && $17=="Ontario" {print $2"\t"$18"\t"$10"\t"$39"\t"$40}' latest.tsv | head -3
Enter fullscreen mode Exit fullscreen mode
registered nurse - telehealth   NA              10   Hour   38
registered nurse (R.N.)         Lindsay         10   Week   1562.72
registered nurse (R.N.)         Sturgeon Falls  1    Hour   41.15
Enter fullscreen mode Exit fullscreen mode

That month had 83 Ontario postings under 31301. Column 8, External Indicator, splits them by source: 21 were posted on Job Bank by employers (0) and the rest were shared from other job boards (1).

Some things you'll hit if you keep doing this by hand:

  • The column numbers above are for this file. Columns have been added over time, and some 2024 months are UTF-8 and comma-separated instead, so match on header names, not positions.
  • Missing values are the text NA.
  • Rows shared from other job boards carry less detail. In September 2026 nearly all of them had a pay figure, but few had hours and none had an education or experience level.
  • open.canada.ca asks automated clients to wait 20 seconds between requests.

For one month, that's all you need. To get every month automatically, read on.

The same slice as clean rows

The Actor reads the same official files, filters while it streams and returns one flat JSON row per posting. Here's a real run from 31 October, capped at three rows and limited to postings made on Job Bank itself:

curl -s -X POST \
  "https://api.apify.com/v2/acts/yourevenings~canada-job-bank-data/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" -H 'Content-Type: application/json' \
  -d '{"months":["latest"],"nocCodes":["31301"],"provinces":["ON"],"postedVia":"jobBank","maxResults":3}' \
  > sample.json
Enter fullscreen mode Exit fullscreen mode

It checked 2,284 rows of the September 2026 file and stopped after the third match. One row, shortened:

{
  "rowType": "posting",
  "postingId": "2026-09:15871793",
  "month": "2026-09",
  "jobTitle": "registered nurse (R.N.)",
  "noc2021Code": "31301",
  "noc2021Name": "Registered nurses and registered psychiatric nurses",
  "postedVia": "Job Bank (employer account)",
  "firstPostingDate": "2026-09-14",
  "vacancies": 1,
  "province": "Ontario",
  "city": "Sturgeon Falls",
  "postalArea": "P2B",
  "economicRegion": "Northeast Region",
  "industry": "Health care and social assistance",
  "employmentType": "Part time",
  "employmentTerm": "Permanent employment",
  "salaryText": "$41.15 to $58.98 hourly (to be negotiated)",
  "salaryPer": "Hour",
  "salaryMin": 41.15,
  "salaryMax": 58.98,
  "hoursPer": "Week",
  "hoursMin": 33.75,
  "hoursMax": 33.75,
  "educationLevel": "College/CEGEP",
  "experienceLevel": "3 years to less than 5 years",
  "sourceFile": "https://open.canada.ca/data/dataset/ea639e28-c0fc-48bf-b5dd-b8899bd43072/resource/03523cbf-a005-40ee-81ad-aca5bb74cf03/download/job-bank-open-data-all-job-postings-en-sept2026.csv",
  "licence": "Contains information licensed under the Open Government Licence - Canada (https://open.canada.ca/en/open-government-licence-canada). Source: Employment and Social Development Canada, Job Bank open data."
}
Enter fullscreen mode Exit fullscreen mode

The run also returned one free status row, {"rowType":"status","status":"max_results", ...}, saying it stopped at the limit. Status rows sit in the same dataset, so filter on rowType when you build the CSV.

Every row keeps a link to the file it came from (sourceFile) and the licence statement (licence), so the attribution goes wherever the data goes. Postal codes are cut to their first three characters.

Every new month into one CSV

Turn on monitor mode and the Actor remembers which months it has already returned. Run it weekly. Most weeks it finds nothing new and returns a free already_reported row. When ESDC publishes the next month's file, usually a few weeks after the month ends, it returns that month's rows once.

#!/bin/sh
# on-nurses.sh: run weekly (cron, or an Apify schedule with the same input)
curl -s -X POST \
  "https://api.apify.com/v2/acts/yourevenings~canada-job-bank-data/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" -H 'Content-Type: application/json' \
  -d '{"months":["latest"],"nocCodes":["31301"],"provinces":["ON"],"maxResults":1000,
       "monitorMode":true,"stateStoreName":"on-nurses-31301"}' \
  > new.json

[ -f on-nurses.csv ] || echo 'month,firstPostingDate,jobTitle,city,economicRegion,vacancies,employmentType,salaryMin,salaryMax,salaryPer,postedVia' > on-nurses.csv
jq -r '.[] | select(.rowType=="posting")
  | [.month,.firstPostingDate,.jobTitle,.city,.economicRegion,.vacancies,.employmentType,.salaryMin,.salaryMax,.salaryPer,.postedVia] | @csv' \
  new.json >> on-nurses.csv
Enter fullscreen mode Exit fullscreen mode

Some notes on the settings:

  • First run. It returns the newest month straight away. Set "emitBaselineOnFirstRun": false if you only want months published from now on.
  • maxResults. A month that maxResults cuts short still counts as read, so set it well above your slice. 1,000 is plenty for 83 postings a month.
  • stateStoreName. Use a different one for each saved search.
  • A different slice. Swap nocCodes (a code or its first digits, such as "72" for trades) or provinces, or add cities, keywords, employmentTypes, or salaryPer with minSalary.
  • Timeouts. Run-sync waits up to 300 seconds. If a call times out, start a normal run and read its dataset afterwards.

The CSV opens as-is in a spreadsheet. If you'd rather pull it straight into Google Sheets or Excel, the run's dataset can also be downloaded as CSV or Excel from the Apify API.

Honest limits

  • Monthly, not live. The newest file covers last month, so for today's postings use jobbank.gc.ca.
  • No employer names, contacts or apply links. The open data leaves them out. This is labour-market data, not a way to apply.
  • English files only. French versions exist with French column names, but the Actor doesn't read them yet.
  • Externally shared postings have fewer fields. Set "postedVia":"jobBank" if you need hours, education and experience on every row.
  • Each extra month adds about 20 seconds, because the Actor honours open.canada.ca's request spacing. Up to 12 months per run.
  • Cost: $0.002 per posting returned and $0.00005 per run start (one start event per GB of memory; the default 1 GB run pays one). Monitor mode adds $0.01 on a run that returns a new month. (These are base prices; higher Apify plans pay less.) Rows your filters drop, status rows and weeks with nothing new carry no posting or monitor charge. For the slice above, a month like September 2026 would be 83 × $0.002 + $0.01 + $0.00005 = $0.17605. If you only want the 21 postings made on Job Bank itself, it's $0.05205.

Contains information licensed under the Open Government Licence – Canada. Source: Employment and Social Development Canada, Job Bank open data. This Actor and this post are not operated, sponsored, approved or endorsed by Employment and Social Development Canada, Job Bank or the Government of Canada.

The Actor, with the inputs above ready to run: Canada Job Bank Data: Job Postings, NOC & Salaries on Apify

Your Evenings

Top comments (0)