Skip to content

About

Breezy HR scraper for Node.js: pull open positions from company.breezy.hr boards into your service as objects with title, job type, pay text, location codes, remote flag and apply URL. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

1 Commit

Folders and files

Repository files navigation

Breezy HR Jobs Scraper β€” JavaScript example

Breezy HR Scraper for JavaScript and Node.js: open positions as JSON objects

Run on Apify JavaScript MIT license CI

If your product lists jobs, alerts candidates or enriches company records, Breezy HR boards are a source you will keep running into: many small and mid-market employers publish their openings at https://{company}.breezy.hr. This repository shows how to fetch them from Node.js through the Breezy HR Jobs Scraper, an Apify Actor that reads each board and returns plain objects you can store or forward. The example is one ES module on top of apify-client. It accepts a list of employer subdomains, waits for the cloud run, then writes results.json and results.csv. Lift the core call into an API route, a queue consumer or a scheduled function and the rest of this repo is optional.

Actor: Breezy HR Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in Python: breezy-hr-scraper-python

πŸ”₯ What you get

  • A whole board per request: each employer's careers site hands over its complete list of open positions at once, so there is no paging and no half-read board when a run ends early.
  • Subdomain or link, your choice: rhynocare and https://rhynocare.breezy.hr/ resolve to the same employer. Vanity domains that merely embed a Breezy board are refused with a clear validation message.
  • Dead subdomains are skipped, not fatal: a name Breezy does not recognise is logged and counted, and every other employer in the list still delivers.
  • A status line you can trust: the run ends with postings scraped, employers that answered, slugs that were invalid and any that failed after five attempts.
  • Locations as codes: region comes through as an abbreviation such as ON and country as an ISO alpha-2 code, which makes grouping and joining straightforward.
  • Pay left exactly as posted: the salary string is whatever the employer typed, and an empty one becomes null so disclosure rates are easy to count.
  • Billing tied to rows: the per-posting charge only fires for rows written to the dataset; an employer with no openings, or a mistyped one, adds nothing beyond the run's start fee.

🎯 Use cases

Ingest Breezy employers into a job board

Keep a table of partner employers and their slugs, then let a worker pass the whole list to one run every few hours. Upsert on posting_id and mark anything that disappeared as closed. Because every board arrives with an identical object shape, the same mapper serves every employer.

Push new-role alerts to a webhook

Diff today's rows against the last snapshot and send the new ones to Slack, Discord, Teams or an n8n webhook. Recruiters watching competitors and candidates waiting on a favourite clinic or restaurant group both want the same thing: a message when a matching role opens.

Enrich CRM accounts with live open roles

A small serverless function can run on a timer, count reqs per company_name and write the number to a custom field in HubSpot, Salesforce or Pipedrive. Reps then sort their territory by who is actively hiring.

Back a candidate-facing search API

Location arrives pre-split into city, region abbreviation and ISO country, and is_remote is a boolean, so filters like "remote roles in CA" are simple equality checks in your database query rather than string parsing.

⚑ Quick start

You need an Apify account and an API token from Console β†’ Settings β†’ Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.

git clone https://github.com/devilscrapes/breezy-hr-scraper-javascript.git
cd breezy-hr-scraper-javascript
npm install
export APIFY_TOKEN=your_token_here
node index.js                       # uses input.json
node index.js my-input.json --out data --max-charge 2

The script starts the Breezy HR Jobs Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.

🧩 The core call

Here is the integration with nothing extra around it. call() resolves once the run finishes; listItems() returns the postings from the run's default dataset as an array.

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('DevilScrapes/breezy-hr-jobs-scraper').call({"companySlugs": ["rhynocare", "barloworldequipment"], "maxResultsPerCompany": 5, "proxyConfiguration": {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);

πŸ“₯ Input

index.js reads input.json and forwards it unchanged. The sample targets two live Breezy boards with a limit of five postings each, so you see at most ten objects for roughly twenty-two cents. Put your own employers in companySlugs, either as bare subdomains or as full board links.

{
  "companySlugs": [
    "rhynocare",
    "barloworldequipment"
  ],
  "maxResultsPerCompany": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
Field Type Required Default Allowed values
companySlugs array yes ["rhynocare", "barloworldequipment"] β€”
maxResultsPerCompany integer no 200 β€”
proxyConfiguration object no {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} β€”

πŸ“€ Output

Each object is one posting. Required keys are always set; the optional ones arrive as null when the employer left them empty, so check for null rather than undefined. published_date and scraped_at are ISO-8601 strings that new Date() parses directly.

Field Type Always present
posting_id string yes
company_slug string yes
company_name string yes
title string yes
apply_url string yes
published_date string yes
job_type string yes
department string or null no
salary string or null no
location_city string or null no
location_state string or null no
location_country string or null no
is_remote boolean yes
scraped_at string yes

πŸ’° Pricing

Event Price (USD)
Actor start $0.2
Result emitted $0.0015

Prices as configured on 2026-08-20. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.

πŸ› οΈ Working with the data in JavaScript

The script below compares results.json with the previous snapshot, prints opened and closed reqs, and saves the current rows for next time. It needs only Node.js built-ins. Keep maxResultsPerCompany above each board's real size when you diff, otherwise the cap itself can look like a closure.

import { readFile, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';

const current = JSON.parse(await readFile('results.json', 'utf8'));
const previous = existsSync('previous.json') ? JSON.parse(await readFile('previous.json', 'utf8')) : [];

const before = new Set(previous.map((job) => job.posting_id));
const now = new Set(current.map((job) => job.posting_id));
const opened = current.filter((job) => !before.has(job.posting_id));
const closed = previous.filter((job) => !now.has(job.posting_id));

const lines = opened.map((job) => {
    const where = job.is_remote ? 'remote' : (job.location_city ?? 'no city');
    return `NEW  ${job.company_name}: ${job.title} (${where}) ${job.apply_url}`;
});
for (const job of closed) lines.push(`GONE ${job.company_name}: ${job.title}`);

console.log(lines.length ? lines.join('\n') : 'No change since the last snapshot.');
await writeFile('previous.json', JSON.stringify(current));

❓ FAQ

Do I need a Breezy HR login or credentials from the employer?

Neither. The Actor reads the same openings each careers page already shows to job seekers, so you can follow companies you have no relationship with. The only secret your service holds is its Apify token.

Can I start a run from a serverless function without waiting?

Yes. Use client.actor(...).start() instead of call(), return immediately, and attach an Apify webhook for the ACTOR.RUN.SUCCEEDED event that points at a second function. That function reads the dataset by its ID and does the upsert.

What does the run do with a custom careers domain?

It rejects it during input validation, before any request is made. Point it at the something.breezy.hr address behind the custom domain instead; that is the host Breezy actually serves.

Is the TypeScript experience any different?

No. apify-client ships type definitions, so the core call works in a .ts file as-is. Declare an interface for the posting fields listed above and cast the items array to it.

🧯 Troubleshooting

  • Set APIFY_TOKEN first: export the token in the same shell, or put it in a local .env file your shell loads. Never commit it.
  • Run finished with FAILED or ABORTED: open the printed Console link; the run log says why. Input validation errors name the field.
  • Fewer rows than expected: a --max-charge cap also limits how many results can be charged. Raise the cap or lower the limits in your input.
  • Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.

πŸ”— More from Devil Scrapes

Related Actors:


MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Breezy HR.

About

Breezy HR scraper for Node.js: pull open positions from company.breezy.hr boards into your service as objects with title, job type, pay text, location codes, remote flag and apply URL. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages