# Job Bank Canada Jobs Scraper (`scrapyx/jobbank-canada-jobs-scraper`) Actor

Canadian job postings from the Government of Canada's Job Bank: title, employer, city and province, salary with its unit, work arrangement and apply method. Includes LMIA status and employment-equity tags that commercial job boards do not carry.

- **URL**: https://apify.com/scrapyx/jobbank-canada-jobs-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Job Bank Canada Jobs Scraper

Job postings from **Job Bank** (jobbank.gc.ca), the Government of Canada's
national employment service and the country's largest single source of
vacancies — as structured rows: title, employer, city and province, salary with
its unit, work arrangement and how to apply.

Public-sector data. No login, no API key, no browser.

***

### What makes this dataset distinctive

**LMIA status.** A Labour Market Impact Assessment is what a Canadian employer
must obtain before hiring a foreign worker, so "does this posting have one" is
the single most consequential fact on the page for anyone applying from outside
Canada. It is a first-class filter here (`lmia: requested | approved`), and no
commercial job board publishes it.

**Employment equity tags.** Job Bank marks postings that explicitly welcome
Indigenous people, newcomers to Canada, persons with disabilities, veterans,
mature workers, youth and visible minorities. `equityGroups` filters on them.

**Facet counts for the whole search.** Every run's summary row carries
`facetCounts` — the board's own tally behind each filter (67 entries on a
measured search): jobs per province, per salary band, per work term, per equity
group, per LMIA status. That is a free aggregation over the entire result set,
not just the pages you fetched.

***

### What you get

One `SEARCH_SUMMARY` row per search term, then one `JOB` row per posting.

| Group | Fields |
| --- | --- |
| Identity | `jobId`, `jobNumber`, `jobUrl`, `title`, `employer`, `postedDate` |
| Location | `locationRaw`, `city`, `province` |
| Pay | `salaryRaw`, `salaryMin`, `salaryMax`, `salaryUnit` |
| Nature of the role | `workArrangement`, `applyMethod`, `isNew`, `postedOnJobBank` |
| Provenance | `_input`, `_source`, `_scrapedAt`, `pageFound`, `raw` |

**Salary carries its period.** Job Bank quotes pay hourly, weekly, bi-weekly,
monthly and annually in a single free-text field, and appends commission
language to it. Comparing an hourly figure to an annual one without reading
`salaryUnit` is roughly a 2,000x error, so nothing is converted between periods
and the unit sits beside the number.

`salaryMax` is populated only for a genuine range ("$20.00 to $25.00 hourly").
A trailing "+ 3% commission per sale" stays in `salaryRaw` and is never
mistaken for an upper bound — an earlier build did exactly that and published a
maximum salary of three dollars.

**`postedOnJobBank`** distinguishes postings the employer submitted here
directly from ones syndicated in off external boards — useful when you want the
primary source rather than an aggregator's copy.

***

### Example input

```json
{
  "queries": ["software developer", "data analyst"],
  "provinces": ["ON", "BC"],
  "workTerm": "permanent",
  "postedWithin": "30d",
  "maxPages": 20
}
```

Looking for roles open to foreign applicants:

```json
{
  "queries": ["cook", "truck driver", "farm worker"],
  "lmia": "approved",
  "maxPages": 40
}
```

***

### Limits and behaviours worth knowing

**This Actor is deliberately slow, and that is the point.** Job Bank's
robots.txt disallows nothing at all but asks for `Crawl-delay: 5`. That request
is honoured as a floor: five seconds between request starts, concurrency 1 by
default, and setting `minRequestInterval` lower does not speed it up. A public
service that hides nothing and asks politely gets read politely.

**Work location and LMIA cannot be combined.** Upstream they share a single
parameter, so sending both would silently drop one. The Actor refuses the
combination instead — run two searches.

**"More than 30 days" means older, not within.** That is Job Bank's own
wording for the third `postedWithin` option, kept rather than reinterpreted.

**A search matching nothing reports zero.** The result count is read from its
own element rather than inferred from the absence of cards, so an empty search
comes back as `upstreamTotalResults: 0` with `stoppedReason: no_results` — not
as an error and not as a missing value.

**Bad filter values are refused before any request** rather than sent and
guessed at.

***

### How it works

HTTP-only. Job Bank is a server-rendered Jakarta Faces application with no JSON
layer anywhere — no `__NEXT_DATA__`, no JSON-LD, no hydration blob — so the
rows come from its HTML, which is unusually clean and semantically classed.

Two things about that HTML would quietly corrupt a dataset and are handled
explicitly:

- **Every posting URL carries a session id.** Links look like
  `/jobsearch/jobposting/50209079;jsessionid=F2A25A…`, where the path parameter
  is *this crawl's* server session. Left in, it would rot immediately and make
  the same job look like a new URL on every run. It is stripped.
- **Screen-reader-only labels sit inside the data elements.** Job Bank is built
  on the Web Experience Toolkit, whose `wb-inv` class marks text meant only for
  assistive technology — and those spans live inside the very `<li>` elements
  holding the location and salary. Flattening tags naively yields a city of
  "Location Surrey". They are removed before any field is read.

No WAF was measured: six different TLS fingerprints all received identical
pages.

# Actor input Schema

## `queries` (type: `array`):

Job titles or keywords, one run each with its own summary row. A term matching nothing honestly reports zero rather than falling back to everything.

## `location` (type: `string`):

A Canadian city or postal code (Toronto, M5V). Leave empty to search all of Canada, or use the province list below.

## `provinces` (type: `array`):

Restrict to these provinces and territories. Combines with the location field.

## `sortBy` (type: `string`):

Result ordering.

## `maxPages` (type: `integer`):

Pages of 25 postings per search.

## `maxItems` (type: `integer`):

Optional overall cap on job rows across every search. Summary rows are always emitted and do not count against it.

## `lmia` (type: `string`):

A Labour Market Impact Assessment is what a Canadian employer needs before hiring a foreign worker, so this is the single most consequential filter for anyone applying from outside Canada. Note it shares an upstream parameter with Work location, so only one of the two can be applied per search.

## `postedWithin` (type: `string`):

Note the last option means jobs OLDER than 30 days, not within them - that is Job Bank's own wording.

## `workHours` (type: `string`):

Full or part time.

## `workTerm` (type: `string`):

How long the job runs for.

## `workLanguage` (type: `string`):

The language the role is performed in.

## `workLocation` (type: `string`):

Remote, hybrid or on-the-road roles. Shares an upstream parameter with LMIA status, so only one of the two can be applied per search.

## `salaryBand` (type: `string`):

For roles quoted per year.

## `hourlyBand` (type: `string`):

For roles quoted per hour, which is most trades and service work on this board.

## `jobSource` (type: `string`):

Whether the employer posted directly on Job Bank or the posting was syndicated in from an external board.

## `equityGroups` (type: `array`):

Job Bank tags postings that explicitly welcome these groups, which commercial boards do not publish. Selecting several requires a posting to match all of them.

## `maxConcurrency` (type: `integer`):

Parallel requests. Kept at 1 by default out of respect for the crawl delay this public service asks for.

## `minRequestInterval` (type: `integer`):

Seconds between request starts. 5 is Job Bank's own requested crawl delay and is enforced as a floor - a lower value is raised back to 5.

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

Apify Proxy on the shared datacenter pool. This is the default because it is included in your plan at no extra cost and this target works through it. If you start seeing blocks, challenges or empty results, switch the group here to Residential -- it uses real consumer IPs and gets through more, but Apify bills residential traffic per gigabyte, so leave it off unless you need it.

## Actor input object example

```json
{
  "queries": [
    "developer"
  ],
  "sortBy": "relevance",
  "maxPages": 5,
  "lmia": "any",
  "postedWithin": "any",
  "workHours": "any",
  "workTerm": "any",
  "workLanguage": "any",
  "workLocation": "any",
  "salaryBand": "any",
  "hourlyBand": "any",
  "jobSource": "any",
  "maxConcurrency": 1,
  "minRequestInterval": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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("scrapyx/jobbank-canada-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("scrapyx/jobbank-canada-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 scrapyx/jobbank-canada-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/jobbank-canada-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/eOZg9U73VMhBdaQr7/builds/AysqaHXlWxLNrwZzs/openapi.json
