# Job Bank Scraper (`harpoon/jobbank-scraper`) Actor

Scrape Canadian job postings from Job Bank with every search filter.

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

## Pricing

from $0.50 / 1,000 job postings

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### Job Bank Scraper — Canadian job postings, with every search filter

Pull live job postings from Job Bank, Canada's official job board, into a clean dataset you can filter, chart, or load into your own app. Search by keyword, or skip the keyword entirely and browse by filters alone — every facet the site offers is on the form: province, economic region, city, date posted, full/part time, term, shift, language, education, experience, salary band, remote/hybrid, government employers, LMIA status, apprenticeship level, internships, green jobs, and the intended-applicant programs.

Open each posting to collect the full description, requirements, tasks, benefits and application link — or switch that off for a fast, cheap run of listing rows.

### What can it do?

- 🔎 **Keyword search** across one or many terms at once, each with its own result cap.
- 🗺️ **Location filtering** by province, economic region, or a specific city or postal code.
- 🧰 **Filter-only browsing** — leave the keyword empty to pull e.g. every remote job in British Columbia.
- 🌍 **Work arrangement** — on site, remote, hybrid, on the road, or relocation-heavy roles.
- 🎓 **Requirements** — education level, years of experience, and working language.
- 💰 **Salary bands** — annual and hourly facets, separately.
- 🏛️ **Employer and source** — federal / provincial / municipal government, verified postings vs. partner boards, and an option to drop staffing agencies.
- 🛂 **Immigration filters** — LMIA requested or approved (and their exclusions), plus every intended-applicant program including newcomers and international candidates.
- 📄 **Full posting details** — description, tasks, work conditions, benefits, vacancies, expiry date, who can apply, and the application link.
- 🔁 **Resumable runs** — an interrupted run continues where it stopped instead of starting over.

### What data can I extract?

| Field | Example |
| --- | --- |
| `id` | `50618828` |
| `url` | `https://www.jobbank.gc.ca/jobsearch/jobposting/50618828` |
| `title` | `welder-fitter` |
| `employer` | `Hahn and Sons` |
| `location.city` / `location.province` | `Burlington` / `ON` |
| `location.postal_code` | `P9N 1L9` |
| `salary.text` | `$ 25.00 to $ 38.00 hourly (To be negotiated)` |
| `salary.min` / `salary.max` / `salary.unit` | `25` / `38` / `hourly` |
| `work_setting` | `On site`, `Remote`, `Hybrid` |
| `employment_type` | `Permanent employment` |
| `work_hours` | `Full time` |
| `vacancies` | `1` |
| `date_posted` | `2026-10-30` |
| `expires_at` | `2026-11-19` |
| `job_number` | `3709116` |
| `source` | `Job Bank`, `indeed.com`, `Jobillico`, `CareerBeacon` |
| `source_url` | the partner's own posting link, when there is one |
| `is_new`, `direct_apply`, `lmia_requested` | `true` / `true` / `false` |
| `description` | full posting summary |
| `languages` | `English` |
| `education` | `["College/CEGEP", "or equivalent experience"]` |
| `experience` | `5 years or more` |
| `tasks` | `["Interpret welding process specifications", ...]` |
| `work_settings` | `["Dusty", "Hot", "Noisy"]` |
| `work_conditions` | `["Fast-paced environment", "Attention to detail", ...]` |
| `benefits` | `["Free parking available", "Team building opportunities"]` |
| `who_can_apply` | `["a Canadian citizen", "a permanent resident of Canada", ...]` |
| `apply_url` | where to actually apply |
| `search_query` | which keyword produced this row |

Fields under `location` and `salary` are the only two nested objects; everything else is flat. When **Fetch full job details** is off, the detail-only fields come back empty or as empty arrays — the listing fields are always filled.

### How to use it

1. Open the **Input** tab. The form is prefilled with a working search, so you can press **Start** straight away.
2. Set **Search queries** — one keyword per line. Or leave it empty to browse by filters alone.
3. Set **Max results per query**. 100 is a good default; 25 is about one page.
4. Optionally narrow with the filter sections: location, date, employment, requirements, salary, work arrangement, job type, and intended applicants.
5. Leave **Fetch full job details** on for the complete record, or turn it off for a quick, inexpensive listing run.
6. Press **Start** and watch the run log. Results stream into the **Output** tab as they are collected.

### Input

Two things drive a run: **what to search for** and **how to narrow it**. That is the whole mental model.

- `search_queries` is the list of keywords. Each one is its own search with its own cap.
- The filter fields apply to every keyword in the run.
- If `search_queries` is empty, the filters are the search — every matching job is returned.

```json
{
  "search_queries": ["welder", "millwright"],
  "max_results": 200,
  "fetch_details": true,
  "sort": "date_posted",
  "provinces": ["ON"],
  "regions": ["9219"],
  "date_posted": "last_7_days",
  "employment_type": ["full_time"],
  "work_term": ["permanent"],
  "workplace": ["on_site"],
  "experience": ["3_years_or_more"],
  "salary_hourly": ["30_to_40"],
  "employment_groups": ["newcomers"]
}
```

The **Input** tab documents every field, with the accepted values for each filter.

### Output

Each job is one row in the dataset. This is a real row, with the long description trimmed:

```json
{
  "id": "50618828",
  "url": "https://www.jobbank.gc.ca/jobsearch/jobposting/50618828",
  "title": "welder-fitter",
  "employer": "Hahn and Sons",
  "location": {
    "city": "Burlington",
    "province": "ON",
    "postal_code": "",
    "country": "Canada"
  },
  "salary": {
    "text": "$ 25.00 to $ 38.00 hourly (To be negotiated)",
    "min": 25,
    "max": 38,
    "currency": "CAD",
    "unit": "hourly"
  },
  "work_setting": "On site",
  "employment_type": "Permanent employment",
  "work_hours": "Full time",
  "vacancies": 1,
  "date_posted": "2026-10-30",
  "expires_at": "2026-11-19",
  "job_number": "3709116",
  "source": "Job Bank",
  "source_url": "",
  "is_new": true,
  "direct_apply": true,
  "lmia_requested": false,
  "description": "Education: College/CEGEP. or equivalent experience. Work site environment: Dusty. Hot. Noisy. Tasks: Interpret welding process specifications...",
  "languages": "English",
  "education": ["College/CEGEP", "or equivalent experience"],
  "experience": "5 years or more",
  "tasks": [
    "Interpret welding process specifications",
    "Operate manual or semi-automatic, fully automated welding equipment",
    "Read and interpret welding blueprints, drawings specifications, manuals and processes"
  ],
  "work_settings": [],
  "work_conditions": [
    "Fast-paced environment",
    "Attention to detail",
    "Standing for extended periods"
  ],
  "benefits": ["Free parking available", "Team building opportunities"],
  "who_can_apply": [
    "a Canadian citizen",
    "a permanent resident of Canada",
    "a temporary resident of Canada with a valid work permit"
  ],
  "apply_url": "https://www.jobbank.gc.ca/jobsearch/jobposting/50618828",
  "search_query": "welder"
}
```

Rows land in the run's default dataset. Pick a view in the **Output** tab — **Overview** for the quick scan, **Requirements** for education and tasks, **By search** when you ran several keywords — and export as JSON, CSV, Excel or XML. Each run also writes a small report to the key-value store with the filters used, the total, and the per-keyword counts.

### What can you do with the data?

1. **Build a job feed.** Run daily with `sort: date_posted`, `date_posted: last_48_hours` and a saved task, then diff against yesterday's rows to publish only what is new.
2. **Salary benchmarking.** Pull a role across provinces, group by `location.province` and `salary.unit`, and compare advertised pay bands.
3. **Hiring-market research.** Count postings by `employer`, `employment_type` and `work_setting` to see who is hiring and whether remote work is actually growing in a sector.
4. **Job-board or aggregator.** Feed the dataset into your own site: `title`, `employer`, `location`, `salary.text` and `apply_url` are enough to render a listing page.
5. **Candidate tooling.** Filter to `employment_groups: ["newcomers"]` plus `lmia: ["approved_lmia"]` to surface roles open to applicants from abroad.

### How much does it cost?

This Actor bills **per result** (pay-per-event):

- **`job-posting`** — charged once per job posting added to the dataset. This is the base price of a row.
- **`job-detail`** — charged in addition for each row that carries the full posting details.

The rates are on this Actor's **Pricing** tab. In plain terms:

- A listing-only run of 500 jobs = 500 × `job-posting`.
- A run of 500 jobs with details = 500 × `job-posting` + 500 × `job-detail`.
- Turning **Fetch full job details** off is the cheapest way to run at scale; you give up the description, requirements, tasks, benefits and expiry date.

Nothing is charged for a run that finds no results, and nothing is charged for rows that were never saved.

### FAQ

**Do I need an account, cookies, or a login?**
No. Everything this Actor returns is publicly visible job-posting data. There is nothing to sign into and no credentials to supply.

**Can I search more than one keyword in a single run?**
Yes — put one keyword per line in **Search queries**. Each keyword gets its own `max_results` cap, and every row records which keyword found it in `search_query`.

**Can I search without a keyword?**
Yes. Leave **Search queries** empty and the filters become the search — for example every permanent, full-time, remote job in Ontario. If you leave both the keyword and the filters empty you get a clear message instead of an empty run.

**Why do some rows have empty `description` or `tasks`?**
Two reasons. Either **Fetch full job details** is off, or the posting itself is a short one collected from a partner board — those often carry only a title, employer, salary and a link out. `source` tells you which board it came from.

**Why is `vacancies` 0 on some rows?**
The posting did not state a number of openings. Treat 0 as "not stated".

**Why do `salary.min` / `salary.max` both read 0?**
The posting states no figure, or only a note such as "to be negotiated". `salary.text` always shows exactly what the employer wrote, so trust it over the numbers.

**How many results can I get?**
`max_results` goes up to 10,000 per keyword, and a run can carry many keywords. Large runs are collected a page at a time, so deep result sets stay quick.

**What happens on a free plan?**
There is no result cap of our own: a free Apify account gets the same results as a paid one, up to whatever `max_results` you set. The only limits are Apify's own — your plan's monthly usage credit and its concurrent-run allowance.

**A run was interrupted. Can I continue it?**
Yes. Start the next run with the interrupted run's id as the `resume_from_run_id` input property and the keywords it already finished are skipped, so only the remainder is scraped. `node scripts/resurrect.mjs <run-id>` does exactly that for you. It is an API-only property with no form field, because it is run bookkeeping rather than a search setting — the Console's **Resurrect** button reuses the original input and starts over from the beginning.

**Can I use it from the API or an AI agent?**
Yes. It is a normal Actor: start it through the Apify API or MCP, then read the dataset. The `.md` version of this page is machine-readable, and the **Input** and **Output** tabs document every field.

**It returned nothing. What now?**
Check the run log — it states the reason per keyword. The usual causes are a filter combination that matches nothing (try fewer filters) and a city that could not be matched, which the log reports while the run continues.

### Notes and limitations

- **Data comes from Canada's official job board.** Coverage, wording and which filters exist are the site's, not this Actor's. If a filter returns nothing, that is a real result.
- **Ordering by date and keeping the cap modest is the fastest way to run.** Very deep result sets are slower per row than the first few pages.
- **`salary` is as advertised.** Many Canadian postings quote hourly pay, some quote annual, some quote "to be negotiated". The `unit` field tells you which, and `salary.text` is always the employer's own wording.
- **Aggregated postings are thinner.** Job Bank republishes postings from partner boards; those carry fewer detail fields and link out via `apply_url`.
- **`expires_at` is the advertised-until date**, not a promise the posting is still live.
- **Job postings change and disappear.** Ids are stable, so a re-run is the right way to refresh a row.
- **The content belongs to the employers and the site.** Use the data in line with their terms and your local rules on personal data; do not republish in a way that misrepresents the original posting.

### Support

Found a bug or a field that looks wrong? Open an issue on this Actor's page with the run id and the posting URL — that is enough to reproduce it.

Built for the Apify platform. Runs on the Actor's own compute; no setup on your side.

# Actor input Schema

## `search_queries` (type: `array`):

One or more keywords to search for, e.g. <code>teacher</code>. Add one per line, or paste a list with <b>Bulk edit</b>. Each keyword is one scrape target with its own result cap, and duplicates are removed automatically.<br><br>Leave this empty to search the filters alone and return <b>every</b> matching job.

## `max_results` (type: `integer`):

How many job postings to return for each keyword, e.g. <code>100</code>. The board shows 25 per page, so this is rounded up to whole pages and trimmed to the exact number before saving.

## `fetch_details` (type: `boolean`):

Open every posting to collect the full job description, requirements, tasks, benefits, vacancies, expiry date and application link.<br><br><b>Costs one extra request per job</b> and is charged at the higher detail rate. Turn it off for a large, cheap run of listing rows only.

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

Keep only jobs in these provinces or territories. Leave empty for all of Canada, and select several to combine them, e.g. <code>Ontario</code> + <code>Quebec</code>.

## `regions` (type: `array`):

Keep only jobs in these economic regions, which are finer than a province. Selecting a region narrows further still - leave empty and use <b>Provinces</b> instead for a wider net.

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

Keep only jobs in or around one place, e.g. <code>Toronto</code>, <code>Montreal, QC</code> or <code>K1A 0A1</code>. A trailing province code picks the right match when several cities share a name.<br><br>If the place cannot be matched, the run continues without it and says so in the log - it never fails.

## `sort` (type: `string`):

Order of the results. <b>Newest first</b> is what you want for a recurring feed; <b>Best match</b> is better for exploring a keyword.

## `date_posted` (type: `string`):

Only return jobs posted in this window. Leave empty for any age; choose <b>More than 30 days old</b> to look at older postings that are still open.

## `employment_type` (type: `array`):

Restrict to full-time or part-time work. Select both to include either.

## `work_term` (type: `array`):

The length of the engagement, e.g. <code>Permanent</code> for a steady job or <code>Seasonal</code> for summer work.

## `work_hours` (type: `array`):

The shift pattern the employer advertises. Select several to include any of them, e.g. <code>Day</code> + <code>Evening</code>.

## `language_at_work` (type: `array`):

The language the job is performed in. Leave empty for both languages.

## `education` (type: `array`):

The minimum schooling the posting asks for. <b>No education required</b> is the useful one when you want entry-level jobs.

## `experience` (type: `array`):

How much experience the posting requires. <b>No experience</b> is the entry-level filter.

## `salary_annual` (type: `array`):

Yearly pay bands. This is the annual facet - use <b>Hourly salary</b> for postings that quote an hourly rate.

## `salary_hourly` (type: `array`):

Hourly pay bands, for postings that quote an hourly wage rather than a salary.

## `workplace` (type: `array`):

Where the work happens. <b>Remote</b> is the telework filter; <b>Exclude remote jobs</b> is its inverse.

## `job_source` (type: `array`):

Jobs posted directly on the board versus collected from partner job boards such as indeed.com or Jobillico.

## `employer_type` (type: `array`):

Limit to public-sector employers, or drop staffing agencies from the results.

## `job_type` (type: `array`):

Special posting categories. Each pair is mutually exclusive: pick <b>Student jobs</b> or <b>Exclude student jobs</b>, not both.

## `lmia` (type: `array`):

A requested or approved LMIA is what lets an employer hire a temporary foreign worker. Useful when you are searching from abroad.<br><br>Each pair is mutually exclusive.

## `apprenticeship_level` (type: `array`):

Training level for apprenticeship postings.

## `employment_groups` (type: `array`):

Jobs that actively encourage applications from a specific group. <b>Canadians and authorized workers</b> and <b>Canadians and international candidates</b> are the useful ones when you are looking from outside Canada.

## Actor input object example

```json
{
  "search_queries": [
    "welder",
    "heavy equipment operator"
  ],
  "max_results": 100,
  "fetch_details": true,
  "provinces": [
    "ON"
  ],
  "regions": [
    "9219"
  ],
  "location": "Toronto, ON",
  "sort": "best_match",
  "date_posted": "",
  "employment_type": [
    "full_time"
  ],
  "work_term": [
    "permanent"
  ],
  "work_hours": [
    "day"
  ],
  "language_at_work": [
    "english"
  ],
  "education": [
    "no_education_required"
  ],
  "experience": [
    "no_experience"
  ],
  "salary_annual": [
    "60k_to_80k"
  ],
  "salary_hourly": [
    "20_to_30"
  ],
  "workplace": [
    "remote"
  ],
  "job_source": [
    "verified_jobs"
  ],
  "employer_type": [
    "federal_government"
  ],
  "job_type": [
    "green_jobs"
  ],
  "lmia": [
    "approved_lmia"
  ],
  "apprenticeship_level": [
    "level_one"
  ],
  "employment_groups": [
    "newcomers"
  ]
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per scraped job posting: title, employer, location, salary, dates, source board, and - when details are enabled - the full description, requirements, tasks and benefits. Export as JSON, CSV, Excel, or XML.

## `requirements` (type: `string`):

The same rows, opened on the requirements view: languages, education, experience, tasks, work conditions, benefits and who can apply.

## `run_report` (type: `string`):

A summary of the run: when it started and finished, the filters used, the total item count and the per-query breakdown.

# 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 = {
    "search_queries": [
        "teacher"
    ],
    "max_results": 100,
    "fetch_details": true,
    "sort": "best_match",
    "date_posted": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("harpoon/jobbank-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 = {
    "search_queries": ["teacher"],
    "max_results": 100,
    "fetch_details": True,
    "sort": "best_match",
    "date_posted": "",
}

# Run the Actor and wait for it to finish
run = client.actor("harpoon/jobbank-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 '{
  "search_queries": [
    "teacher"
  ],
  "max_results": 100,
  "fetch_details": true,
  "sort": "best_match",
  "date_posted": ""
}' |
apify call harpoon/jobbank-scraper --silent --output-dataset

```

## MCP server setup

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