# Jobs.ie Scraper - Ireland Job Listings, Salary & Employer (`scrapersdelight/jobs-ie-jobs-scraper`) Actor

Scrape jobs.ie, Ireland's national job board: title, employer, county, salary, work type, industry, posted & expiry dates, GPS and the full description. Filter by direct employer vs agency, contract type, salary and posted-within. Daily incremental mode.

- **URL**: https://apify.com/scrapersdelight/jobs-ie-jobs-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 per job returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## 🇮🇪 Jobs.ie Scraper — Ireland Job Listings, Salary & Employer Data

**Scrape [jobs.ie](https://www.jobs.ie), Ireland's national job board, by keyword, county, employer and contract type. Every row comes back with the hiring employer, their jobs.ie company id and company page, the county, the salary exactly as advertised *plus* a parsed min/max/period/currency, and — with the detail add-on — the exact posted and expiry dates, work type, industry taxonomy, GPS coordinates and the complete job description. $1.00 per 1,000 jobs, with no per-run start fee.**

***

### Why this one?

| | **This actor** | Typical jobs.ie scraper on the Store |
|---|---|---|
| Price per 1,000 jobs | **$1.00** | $1.10 + **$5.00 per run start** |
| Charge to start a run | **$0** | $0.005 every run, even a run that returns nothing |
| Direct employer **vs** recruitment agency filter | ✅ server-side (measured 2026-09-06 on `/jobs/dublin`: 649 direct / 476 agency) | ❌ |
| Employer's jobs.ie **company id** on every row | ✅ 100% fill — feed it straight back in to scrape that employer's whole board | ❌ |
| Salary parsed to min / max / period / currency **+ annualised** | ✅ and an explicit `salary_disclosed` flag | raw text |
| Work type (Permanent / Temporary / Contract) | ✅ — it exists **nowhere** in the page's structured data, only on the posting page | ❌ |
| GPS coordinates | ✅ 100% fill (30/30, run `IPNR6YmGafvdyp17e`) | ❌ |
| Protects you from jobs.ie's "similar jobs" padding | ✅ **and never bills you for it** unless you switch *Include similar jobs* on — see below | ❌ |
| Incremental / never-seen-before modes for daily monitors | ✅ two independent modes, state in named stores | ❌ |
| Honest about sorting | ✅ says client-side, because jobs.ie has no sort parameter | a rival exposes a `sort` input that cannot work |

No login, no cookies, no account, no browser automation.

***

### ⚠️ The padding trap this actor exists to fix

When a jobs.ie search matches **fewer than 25 jobs**, jobs.ie pads the *same* results container with unrelated "similar" jobs and gives **no markup boundary** between the real matches and the padding. Re-measured 2026-09-06, each row a real run of this actor with `includeRelatedJobs: true` so every rendered card is counted:

| Search | jobs.ie heading | Cards rendered | Actually in scope | Run |
|---|---|---|---|---|
| `/jobs/nurse/in-galway` | "**1** Nurse job in County Galway + 20 km" | 12 | **1** — the other 11 are padding | `PJJnVsN1mmHgUnaiq` |
| `/jobs/chef/in-waterford` | "**5** Chef jobs in County Waterford + 20 km" | 25 | **5** — cards 6–25 are padding | `0RBRvhVBEr55ql5wR` |
| `/jobs/chef/in-cork` | "**9** Chef jobs in County Cork + 20 km" | 25 | **9** | `L4GMalANxfdN5O2z6` |

A naive scraper hands you 12 rows for a 1-job search and bills you for all 12. This actor reads the real match count structurally from the page heading, emits only cards `1..min(25, N)`, stops paging at `ceil(N/25)`, and **drops the padding before the billing call** — so you are never charged for a row you did not ask for. If you *want* the padding, `includeRelatedJobs` turns it on and flags every such row `is_related_match: true`.

Deep pages are not padded: a complete `/jobs/dublin` sweep on 2026-09-06 (run `jqd1RNkfFDR5TAwjJ`) walked 46 pages, page 46 returned exactly 1 card, and 45 × 25 + 1 = **1,126** = the heading count, exact — with **0** duplicate job ids across all 46 pages.

***

### What you get — every field, measured

Measured **2026-09-06**. Listing fields: **1,126 rows / 1,126 unique postings** — a complete `/jobs/dublin` sweep, run `jqd1RNkfFDR5TAwjJ`. Detail fields: **30 of 30 enriched**, run `IPNR6YmGafvdyp17e`.

#### Always (from the search results, 1 request per 25 jobs)

| Field | Fill | Example |
|---|---|---|
| `job_id` | 100% | `107900425` — the stable jobs.ie id, used for all dedupe |
| `title` | 100% | `Field Sales Representative Dublin` |
| `company` | 100% | `Yuno Limited t/a Yuno Energy` |
| `company_id` | 100% | `1383052` — feed back in as **Company ID** to scrape that employer's whole board |
| `company_url` | 100% | the employer's jobs.ie page |
| `company_logo` | 100% | real logo URL (pulled from the `<noscript>` fallback, not the 1×1 lazy-load placeholder) |
| `location` | 100% | `Dublin, County Dublin` |
| `salary_raw` | 100% | `€32,000 per annum` · `€15.15 - €30.30 per hour` · `€ Not Disclosed` |
| `salary_disclosed` | 100% | `true` on **50%** of postings (564 of 1,126) — the rest literally say "€ Not Disclosed" |
| `salary_min` / `salary_max` / `salary_period` / `salary_currency` | 50% | parsed by this actor from `salary_raw`; every disclosed salary parsed — 564 of 564 |
| `salary_annualised_min` | 50% | hourly/daily/weekly rates normalised to a yearly figure so you can compare like for like |
| `posted_relative` | 100% | `1 week ago` |
| `snippet` | 100% | ~300-character description preview |
| `label` | **under 1%** | `NEW` is the only badge jobs.ie renders — 9 of 1,126 cards (0.8%). Everything else is `null`. **Exclude promoted listings** does not key off this field — it reads jobs.ie's structured sponsored / top-job / highlighted flags, which measured **0 of 1,126** on 2026-09-06 (run `2q1I1b8I3yFMpNzQC`), so it normally drops nothing |
| `job_url`, `search_query`, `search_url`, `search_total_matches`, `is_related_match`, `scraped_at` | 100% | provenance on every row |

#### With **Fetch full job details** on (1 extra request per job)

Measured on a 30-job Dublin run, 30/30 enriched — run `IPNR6YmGafvdyp17e`:

| Field | Fill | Example |
|---|---|---|
| `work_type` | 100% | `Permanent` / `Part Time` / `Temporary` — **this exists only on the posting page**, not in its structured data and not on the search card |
| `employment_type` | 100% | `["FULL_TIME"]`, `["PART_TIME"]`, `["FULL_TIME","CONTRACTOR"]`, `["TEMPORARY","FULL_TIME","CONTRACTOR"]` |
| `industry` | 100% | `Logistics, Logistics-Driving` — a two-level taxonomy |
| `date_posted` | 100% | `2026-09-02T05:35:57.283Z` — the exact timestamp |
| `valid_through` | 100% | expiry; posted + 30 days is the usual (this sample: 30, 28 and one 14) |
| `direct_apply` | 100% | boolean |
| `latitude` / `longitude` | 100% | `53.38667`, `-6.21191` |
| `country` | 100% | `IE` |
| `region` | 83% (25/30) | the county |
| `town` | 17% (5/30) | jobs.ie usually leaves `addressLocality` out and puts the whole address in `location` |
| `postcode` | 17% (5/30) | Irish postings rarely publish Eircodes |
| `description_text` / `description_html` | 100% | 1,304 – 13,309 characters, median 6,263 |

> **On defaults (`includeDetails: false`) 14 of the 37 columns are null on every row** — `town`, `region`, `postcode`, `country`, `latitude`, `longitude`, `work_type`, `employment_type`, `industry`, `direct_apply`, `date_posted`, `valid_through`, `description_text`, `description_html`. Verified on the 45-row control run `FrlXsVHESixWU0Dm2`, 2026-09-06. Those 14 columns *are* the detail add-on's product: turn **Fetch full job details** on to fill them, or set **Drop empty fields** to keep the rows tight.
>
> **If a row ends up with no fields left, you do not get it and you are not charged for it.** **Drop empty fields** removes null keys *after* **Output columns** has already narrowed the row, so asking for detail-only columns with the detail add-on off can strip a row down to a literal `{}`. Those rows are dropped before the push: never delivered, never billed, and the run log names the column that emptied them. Measured: on build 0.1.13 the input `outputFields: ["date_posted","work_type","industry"] + excludeEmptyFields: true` with `includeDetails: false` delivered **6 rows, all six `{}`, and charged 6 events** (run `yyr1a0fgplXWG7yh0`, 2026-09-07). On build 0.1.14 the identical input delivers **0 rows and charges 0 events** (run `eg6KjLGnF8BojKenu`, 2026-09-07) and the log says why: which column emptied the rows, and that they were never billed. The realistic version behaves the same way — `outputFields: ["salary_min","salary_max"] + excludeEmptyFields: true, maxItems: 20` returns **15 rows, both columns 100% filled, 15 events charged**, with the 5 "€ Not Disclosed" postings dropped rather than delivered empty (run `7p7rkmbbmeXS3oPle`).

**Two fields we deliberately do NOT ship**, because shipping them would be a lie:

- **`apply_url`.** The apply button on jobs.ie has no href — it is JavaScript-gated. `job_url` is the only honest link.
- **`radius`.** `?radius=20` AND `?radius=5` on `/jobs/dublin` both returned the identical match count and the identical first result — it is a no-op on this path. A location search *already* applies a +20 km radius; jobs.ie's own heading says so ("5 Chef jobs in County Waterford **+ 20 km**").

***

### Filters — all re-measured 2026-09-06 against the same `/jobs/dublin` baseline (1,125 matches)

> **The two Dublins.** A **keyword** and a **location** are different corpora on jobs.ie, and the table below is measured on the keyword one. Typing `dublin` into **Keywords** builds `/jobs/dublin` — "1,129 Dublin jobs" when re-checked on 2026-09-06 (run `MSkDGZRjDeoxG4LHk`). Typing `dublin` into **Location** builds `/jobs/in-dublin` — "1,618 jobs in County Dublin + 20 km" the same hour (run `zUY8UfxGvRlh9hwff`). Both are correct; the location path is the geographic one and is larger. Percentages below are shares of the keyword path.

Everything in the Filters section is a **real jobs.ie server-side filter**, so narrowing costs you nothing and cuts your bill.

| Input | jobs.ie result |
|---|---|
| **Company type** = direct employer | 649 |
| **Company type** = agency | 476 |
| **Posted within** 1 / 3 / 7 / 14 days | 95 / 458 / 841 / 1,105 |
| **Contract type** permanent / part-time / contract / temporary / work-from-home | 931 / 119 / 54 / 84 / 107 |
| **Min salary** €40,000/yr · €80,000/yr | 423 · 73 |
| **Min salary** €15/hr · €50/hr | 740 · 41 |
| **Location** narrowing — `/jobs/chef` (221 nationally) -> `/jobs/chef/in-cork` | 9 |
| **Company ID** (one employer's whole board) | run `u2Tmigturk6jVq1ra`: 2 rows, that employer's live postings only |

The contract-type figures **overlap** — a role can be both permanent and work-from-home — so they do not sum to the baseline.

**Sorting is client-side and this actor says so.** `?sortby=2` and `?sort=date` were measured returning the identical match count AND the identical first job id as the unsorted page — jobs.ie has no server-side sort parameter. `sortBy` reorders the rows this run collected; it cannot change which rows jobs.ie hands over.

***

### Who buys this

- **Recruitment agencies and RPOs in Ireland** — `companyType: direct-employer` is a cold-call roster of Irish companies that are hiring *right now*, with the role, the county, the salary band and the employer's own jobs.ie page.
- **Staffing/HR-tech vendors** — `companyType: agency` is the live list of every recruitment agency active on the Irish market, ranked by how much they are posting.
- **Compensation and market analysts** — `salary_min`/`salary_max`/`salary_period`/`salary_annualised_min` plus `industry` and `region`, with `salary_disclosed` so you never mistake "€ Not Disclosed" for zero.
- **Job aggregators and boards** — the full description, JSON-LD-grade dates, employment type and GPS.

#### The recurring use: a daily new-jobs monitor

jobs.ie carried **3,385 live postings nationally** on 2026-09-06 (per contract-type filter, and these overlap: permanent 2,771 · part-time 440 · contract 232 · temporary 182 · work-from-home 180), and postings expire ~30 days after they are posted. So the value here is **flow, not stock** — 95 brand-new Dublin postings in the 24 hours to 2026-09-06, 458 in the last 3 days. It is a small board that turns over fast, so every total on this page moves day to day; read them as a scale, not a constant.

The monitor recipe:

```json
{
  "keywords": "dublin",
  "postedWithin": "1",
  "companyType": "direct-employer",
  "incrementalMode": true,
  "stateKey": "dublin-daily",
  "maxItems": 0
}
```

Verified on 2026-09-06 — run `UzUFbdBm5mhYq4hLb` seeded the baseline with the 76 direct-employer Dublin postings from the previous 24 hours. Schedule it daily. The first run seeds a baseline; every run after returns only job ids that were not there before. Use `dedupeAcrossRuns` instead (or as well) if you want "never send me this job again, ever" — it keeps a permanent delivered-ids ledger in its own named key-value store.

***

### Example inputs

**A lead list of Irish employers hiring care staff**

```json
{ "keywords": "care assistant", "location": "dublin", "companyType": "direct-employer", "maxItems": 200 }
```

**Several searches in one run** (jobs.ie is a small board — stacking searches is how you fill a dataset)

```json
{
  "searchTerms": ["chef in cork", "chef in galway", "care assistant in dublin", "hgv driver"],
  "maxItems": 500
}
```

**Full detail, for salary benchmarking**

```json
{ "keywords": "accountant", "includeDetails": true, "minSalary": 40000, "salaryPeriod": "year", "maxItems": 300 }
```

**One employer's entire board**

```json
{ "companyId": 1257178, "includeDetails": true, "maxItems": 0 }
```

**A URL you already have**

```json
{ "startUrls": [{ "url": "https://www.jobs.ie/jobs/permanent/nurse/in-cork?postedWithin=7" }] }
```

***

### Pricing

| Event | Price | When it fires |
|---|---|---|
| **Per job returned** | **$0.001** ($1.00 / 1,000) | once for every job actually delivered to your dataset |
| **Per detail fetch** | $0.0005 ($0.50 / 1,000) | only when *Fetch full job details* is on, and only for jobs whose posting page actually returned data |

There is **no actor-start charge**. Duplicates, collapsed reposts and jobs.ie's out-of-scope "similar" padding are removed *before* the billing call, so delivered always equals billed. Rows dropped by your own client-side filters (keywords, dates, sponsored) are not billed either — even when this actor already paid to fetch their detail page, and even when the row came from a single-posting URL you pasted in (runs `a08URQfSoptruM4JZ` / `Qf9U8YUqYKafcExXM`, 2026-09-06: 1 detail page fetched, 0 rows delivered, **$0** charged).

The one way to pay for padding is to ask for it: with **Include similar jobs** on, those out-of-scope cards are delivered flagged `is_related_match: true` and charged like any other row. It is off by default.

A one-shot scrape of every live job on jobs.ie (3,385 rows on 2026-09-06) costs **$3.39** listing-only, or **$5.08** with full details. That is not an estimate: run `XAFKevooCzJSUGwCa` walked the whole national board that day — 136 pages, 18.6 minutes, **3,385 rows delivered and 3,385 billed**, exactly the count jobs.ie's own heading reports.

***

### Reliability, measured

Everything below was measured on **2026-09-06** and names the run it came from.

| | Result |
|---|---|
| Sustained load — 46 consecutive search pages, a complete `/jobs/dublin` sweep incl. pages 5, 9, 20, 40, 46 (run `jqd1RNkfFDR5TAwjJ`) | **46 / 46 usable (100%)**, **0** retries, median page-to-page **2.49 s**, slowest **5.99 s** |
| Detail pages, with the actor's built-in retries | **38 / 38** across today's detail runs — 30 + 6 + 2 (`IPNR6YmGafvdyp17e`, `RWFn7duL9bxJqlVxE`, `u2Tmigturk6jVq1ra`); two proxy `UPSTREAM504`s were absorbed by a retry |
| Pagination overlap | **0** duplicate job ids across all 46 pages — 1,126 rows, 1,126 unique ids |
| Deep-page count arithmetic | page 46 returned exactly 1 card: 45 × 25 + 1 = 1,126 = the heading count, exact |
| Charge cap honoured atomically | run `cZahlv6s5795SzEqT` at a $0.02 cap: the log said 20 delivered, the dataset held 20, the platform billed 20 |
| The cap fills past **every** filter, the exact posted-date one included | capped run `M8LzQE7nASSap2mQ2` (`maxItems: 25`, details on, `postedAfter: 2026-08-25`) returned **24** — and its uncapped control `IfQC82v9qWlRAE1jd`, byte-identical filter, returned **the same 24**. Both read all 45 in-scope postings; the board held no 25th match |
| Client-side filters bind on single-posting URLs too, and unbilled | `bezKfHZTGnAhfP7dD`: `excludeKeywords: ["care assistant"]` against the posting titled *Care Assistant Support Worker* → **0 rows, $0**, after the detail page was fetched to test it. `vCmBYUoeiv3GEVBn4`, the matching include-list on the same URL → 1 row, 1 billed |
| jobs.ie's own match-count hook cross-checked every page | `[data-at="search-jobs-count"]` was present on **2 of 2** SERPs read and agreed with the `<h1>` parse on both (`IfQC82v9qWlRAE1jd`). The h1 count is the padding boundary billing depends on, so a disagreement now raises a named warning |
| Whole national board, one run | run `XAFKevooCzJSUGwCa`: 136 pages, 18.6 min, **3,385 rows = the heading count exactly**, 16 transport stalls all absorbed by retries, 3,385 billed |
| Log vs dataset vs invoice | on **all 17** verification runs on 2026-09-06 the "N delivered" log line, the dataset row count and the platform's `job-scraped` count were the same number |
| Proxy tiers on `/jobs/dublin`, 3 tries each | Irish **RESIDENTIAL 3 / 3** (200 + 25 cards, 2.0–5.2 s) · **datacenter 0 / 3** (one 403, two 45-second read timeouts) — the same datacenter session reached a neutral host fine, so this is jobs.ie, not the proxy |

**Transport:** plain HTTP through an **Apify RESIDENTIAL** proxy pinned to **Ireland**, a fresh proxy session per request, and a same-site `Referer` header. Apify's **datacenter** tier is unusable on jobs.ie (re-measured 2026-09-06: 0 of 3 — one 403 and two 45-second read timeouts), so residential is not a preference here, it is the requirement. GB and US residential also work if you need to override the country.

**Honest limits:**

- Search pages were clean today (46/46, no stalls), but jobs.ie and the residential pool do stall: a whole-board sweep on 2026-09-06 hit a handful of `UPSTREAM504`s, socket hang-ups and 40-second timeouts spread across ~136 pages, every one of which a retry absorbed. The 40-second timeout and 5 retries are why; do not lower them.
- `town` and `postcode` fill on only ~17% of postings and `region` on ~83% — that is what jobs.ie publishes, not a parser gap.
- **Half** of Irish postings advertise no salary at all ("€ Not Disclosed") — 562 of 1,126 on 2026-09-06. Those rows carry `salary_disclosed: false` and null figures rather than a misleading zero.
- **Memory.** The default run memory is **1,024 MB** and it is not decoration: a whole-board `/jobs/dublin` sweep with **Exclude promoted listings** on peaked at 645 MB (run `2q1I1b8I3yFMpNzQC`), and the identical run at 512 MB was killed by the container at 486 MB (run `YQzafVcprCNykx8fQ`). Leave the memory setting alone unless you are only pulling a few hundred rows.
- `postedAfter` / `postedBefore` are exact only with **Fetch full job details** on. Without it they fall back to the card's relative "3 weeks ago" text and are approximate — use **Posted within** for a precise server-side cut.
- jobs.ie's match count can drift by one or two between requests as postings go live and expire. Row counts on a large run are therefore ±1, not to-the-row reproducible.

***

### FAQ

**Do I need a jobs.ie account or login?**
No. Everything this actor reads is on public pages that jobs.ie serves to anyone.

**Why does it need a residential proxy?**
Measured, not assumed. Re-run on 2026-09-06 against `/jobs/dublin`, 3 tries per tier: Apify **datacenter** got one 403 and two 45-second read timeouts (0 of 3), while Irish **residential** returned 200 + 25 cards every time in 2.0–5.2 s (3 of 3). The same datacenter proxy session reached an unrelated host without trouble, so the block is jobs.ie's, not the proxy's. The default is already set correctly — you do not need to change it.

**Why did my 1-job search return 1 row when the page shows 12?**
Because 11 of those 12 cards are jobs.ie's "similar jobs" padding from other counties, and you should not pay for them. Set `includeRelatedJobs: true` if you want them; they arrive flagged `is_related_match: true`.

**How many jobs are there in total?**
3,385 live postings nationally on 2026-09-06. It is a small, high-turnover board and that figure moves every day — the product here is the daily flow of new postings, not a one-off dump.

**Can I get an apply link?**
No, and no honest scraper can: jobs.ie's apply button carries no href, it is JavaScript-gated. `job_url` takes you to the posting, where the apply flow lives.

**I set a keyword / date / dedupe filter and a `maxItems` — do I get fewer rows than I asked for?**
Not because of the cap. Every filter is applied *before* a row counts toward `maxItems`, so the crawl keeps paging until the cap is full instead of truncating at the first 25 cards and filtering them away afterwards. That now includes the **exact posted-date** test, which can only be answered from the posting page: with **Fetch full job details** on, the crawl enriches each page and applies the date test there, page by page, rather than after the cap. Proved with a control on the same 45-match board within the same hour on 2026-09-06 — capped run `80fgRz66cIo7x0x2e` (`maxItems: 25`, details on, `postedAfter: 2026-08-25`) returned **24**, and the uncapped run `m3aJ4j6dCx9rYcNF9` on the byte-identical filter returned the same **24**. The board simply held no 25th job posted after that date; both runs read all 45 in-scope postings.

If a run *does* come back under the cap, the last log line names which of the three reasons it was: your own filters plus an exhausted board, a crawl cut short by `maxPages`, or Apify's max-total-charge cap stopping the push. The price of the exact-date test is one detail fetch per candidate card, including cards it then drops — those are never billed (run `UNly7pVXDQJ0LTgCV`: 9 detail pages fetched, 9 dropped on the exact date, **0** rows charged).

**Can I sort by newest or by salary?**
Yes, client-side. jobs.ie itself has no sort parameter (measured — `?sortby=2` and `?sort=date` return the same match count and the same first job id as the unsorted page), so sorting reorders the rows the run collected. To bias *which* jobs you get toward recent ones, use **Posted within**, which is a real server-side filter.

**Do the keyword filters work on a single posting URL?**
Yes. A `/job/…` start URL has no search card to read, so the include/exclude test re-runs after that posting's detail page is fetched — over its title, company and full description — and it runs *before* the push, so a row you filtered out is neither delivered nor billed. Measured 2026-09-06: `excludeKeywords: ["care assistant"]` against the posting titled *Care Assistant Support Worker* returned 0 rows and charged $0 (run `a08URQfSoptruM4JZ`), `includeKeywords: ["zzzznotpresent"]` likewise (run `Qf9U8YUqYKafcExXM`), and `includeKeywords: ["care assistant"]` on the same URL returned the 1 row (run `vCmBYUoeiv3GEVBn4`) — so it filters, it does not just drop.

**How do I scrape one company's whole board?**
Run any search, take that employer's `company_id` from the output, and pass it back as **Company ID**. Verified live.

**How do I run this daily without paying for the same jobs twice?**
Turn on **Incremental mode** with a `stateKey`, or **Never return the same job twice** with a `dedupeStoreName`. Both keep their state in named key-value stores so it survives between runs.

**What happens if my search matches nothing?**
The run finishes successfully with 0 rows and a log line that says **which** kind of nothing it was, measured on 2026-09-06:

| The run | What the log says |
|---|---|
| jobs.ie genuinely matched nothing (`keywords: zzzqqxnotarealjobtitle`, run `H0KNLVDxpboBZiruu`) | "this is the source, not a filter and not the cap: no row of ours was dropped" |
| Your own settings removed everything (`postedAfter: 2026-09-10`, run `UNly7pVXDQJ0LTgCV`) | "NOT because jobs.ie was empty … 45 in-scope posting(s) were read and 45 row(s) were removed by settings in YOUR input (posted-date (approximate, from the card) 36, posted-date (exact, from the detail page) 9)", plus which setting was narrowest |
| A keyword filter removed a single posting URL (run `a08URQfSoptruM4JZ`) | the same itemisation, and **nothing charged** |

The itemised counts sum to the postings read — reposts and the exact-date drop are both in that total. Where a limit rather than the source ended the crawl (`maxPages`, the page safety cap, transport, the run-time budget) the message says so too, so an incomplete answer is never presented as a complete one. It only fails loudly when something is genuinely broken — a transport failure or a markup change — because a silently-empty run is worse than an obviously failed one.

**Can I limit what I spend?**
Yes, two ways: **Number of jobs** is a hard cap on rows returned *and* billed, and Apify's own "max total charge" cap is honoured atomically — this actor pushes and bills in the same call, so a run that hits your cap can never hand you rows you were not billed for or bill you for rows you did not get. Verified 2026-09-06 at a $0.02 cap (run `cZahlv6s5795SzEqT`): the log reported 20 delivered, the dataset held 20, and the platform billed `job-scraped` × 20.

***

### Legal & fair use

- This actor reads **public job listings** on jobs.ie. It uses no login, no cookies, no account, and it does not solve, forge or bypass any anti-abuse control. It sends the same `Referer` header any browser sends when it follows a next-page link.
- **You are responsible for complying with jobs.ie's Terms of Service** and for using the output lawfully.
- **Personal data:** job postings can name individual recruiters or contacts inside the description. Under GDPR that is personal data of an identifiable person, and handling it lawfully — legal basis, retention, deletion, and the individual's rights — is your responsibility as the operator, not Apify's and not ours.
- **robots.txt, quoted verbatim** (fetched 2026-09-04). jobs.ie's `User-agent: *` group has **no** blanket `Disallow: /` — that line appears only in the two named-bot groups (adidxbot/Twitterbot, and Applebot/Bingbot/BingPreview/MSNBot/Slurp). The lines binding `*` that touch paths this actor can build are:

  ```
  Allow: /jobs/work-from-home # GA 29/05/23  RD 25/10/23
  Allow: /jobs/temporary # GA 29/05/23  RD 25/10/23
  Disallow: /jobs/permanent # GA 29/05/23
  Allow: /jobs/contract # GA 29/05/23  RD 25/10/23
  Allow: /jobs/part-time # GA 29/05/23  RD 25/10/23

  Disallow: /jobs/work-from-home/*/in-* #  RD 31/10/23
  Disallow: /jobs/temporary/*/in-* #  RD 31/10/23
  Disallow: /jobs/contract/*/in-* #  RD 31/10/23
  Disallow: /jobs/part-time/*/in-* #  RD 31/10/23

  Allow: /*?q*&page=  # 8/9/2017
  Disallow: /*&page=  # this is where there is a page parameter on a facited link - its a catch all
  Disallow: /*Radius=
  Disallow: /*&radius=
  Disallow: /*?radius=
  Disallow: /*salary=
  Disallow: /*Salary=
  Disallow: /*postedwithin=
  Disallow: /*PostedWithin=
  Disallow: /*companytype=  # 5/10/18 RD
  Disallow: /*CompanyType=
  Disallow: /*?action=
  Disallow: /*&action=
  ```

  Read plainly: the paths this actor uses by default — `/jobs/<keyword>`, `/jobs/<keyword>/in-<county>`, `/job/<posting>`, and `?page=N` emitted as the **first** query parameter — are not disallowed for `*`. This actor deliberately puts `page` first for that reason, and deliberately does not ship a `radius` input or send `action=`. The optional inputs that *do* land on a `Disallow`-ed pattern are **Contract type = permanent** (`Disallow: /jobs/permanent`) and the contract-type + keyword + county combination, plus **Min salary** (`Disallow: /*salary=`). Those inputs are off by default. Whether to use them is your call as the operator, and you should make it deliberately.
- jobs.ie / StepStone own their content. Republishing scraped listings verbatim is a copyright question, not a scraping one — resolve it before you publish anything downstream.

# Actor input Schema

## `keywords` (type: `string`):

Job title, skill or town, exactly as you would type it into jobs.ie — e.g. 'care assistant', 'python developer', 'dublin', 'hgv driver'. Multi-word terms work ('care assistant' becomes /jobs/care-assistant). Leave empty to sweep every live job in Ireland.

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

Town, city, county, province or region — e.g. 'dublin', 'cork', 'county-kildare', 'galway', 'leinster', 'munster', 'connacht', 'ulster', 'northern-ireland'. jobs.ie always applies a +20 km radius to a location (its own heading says so), so there is no separate radius setting. Leave empty to search the whole country. NOTE: a location and a keyword are two different corpora on jobs.ie. Location 'dublin' builds /jobs/in-dublin — 1,618 matches on 2026-09-06 (run zUY8UfxGvRlh9hwff), 'jobs in County Dublin + 20 km'. Typing dublin as the KEYWORD builds /jobs/dublin — 1,129 that same hour (run MSkDGZRjDeoxG4LHk), jobs whose text mentions Dublin. The filter breakdowns in the README are measured on the keyword path.

## `searchTerms` (type: `array`):

Run several searches in ONE run. Each line is either a keyword ('chef') — which reuses the Location above — or 'keyword in location' ('care assistant in cork'). jobs.ie carried 3,385 live postings nationally on 2026-09-06, so stacking searches is how you fill a dataset. Results are deduplicated on the jobs.ie job id across every search.

## `startUrls` (type: `array`):

Paste jobs.ie URLs directly and skip the builder above. A search URL (https://www.jobs.ie/jobs/chef/in-cork?postedWithin=7) is paginated; a single posting URL (https://www.jobs.ie/job/chef/compass-group-job107812345) is fetched on its own with full detail. Anything you paste here overrides Keywords/Location.

## `contractType` (type: `string`):

Server-side filter, applied as a jobs.ie path prefix. MEASURED 2026-09-06 on /jobs/dublin (1,125 matches): permanent 931 · part-time 119 · contract 54 · temporary 84 · work-from-home 107. These overlap - a role can be both permanent and work-from-home.

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

Only jobs posted in the last N days. MEASURED 2026-09-06 on /jobs/dublin (1,125 matches): 1 day 95 · 3 days 458 · 7 days 841 · 14 days 1,105. This is the lever for a recurring feed — pair it with Incremental mode.

## `minSalary` (type: `integer`):

Only jobs paying at least this much, in the period chosen below. 0 = no salary filter. MEASURED 2026-09-06 on /jobs/dublin (1,125 matches): €40,000/year 423 · €80,000/year 73 · €15/hour 740 · €50/hour 41. Note that many Irish postings say '€ Not Disclosed' — those are excluded by jobs.ie when a salary filter is on.

## `salaryPeriod` (type: `string`):

How to read the minimum salary above — per year, per day (contractor rates) or per hour.

## `companyType` (type: `string`):

THE lead-gen lever. 'Direct employer' = companies hiring for themselves — your cold-call roster. 'Agency' = recruitment agencies — competitor intel, or your prospect list if you sell TO recruiters. MEASURED 2026-09-06 on /jobs/dublin (1,125): direct employers 649 · agencies 476.

## `companyId` (type: `integer`):

Scrape every live posting from ONE employer. The id is the `cmpId=` number in any jobs.ie company link, and it is returned on every row as `company_id` — so you can discover it with one broad run, then monitor that employer. 0 = off.

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

Stop after this many jobs — this is also the hard cap on what you are billed. 0 = every job jobs.ie serves for your query (the whole national board was 3,385 live postings on 2026-09-06). The cap counts rows you will actually RECEIVE: every filter — cross-run dedupe, incremental, keywords, promoted, reposts AND the posted-date filter, including the exact date read from the detail page — is applied BEFORE a row counts toward the cap, and the crawl keeps paging until the cap is full or jobs.ie runs out. Proof, both on 2026-09-06 over the same 45-match board: capped run 80fgRz66cIo7x0x2e (maxItems 25, details on, postedAfter 2026-08-25) returned 24, and the uncapped control m3aJ4j6dCx9rYcNF9 on the identical filter returned the same 24 — the board held no 25th matching job. When a run does come in under the cap the log says which of the three reasons it was: your filters plus an exhausted board, a truncated crawl (maxPages), or your Apify max-total-charge cap.

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

Belt-and-braces cap on how many 25-job result pages to walk per search. 0 = no page cap (the job count above still applies).

## `includeRelatedJobs` (type: `boolean`):

OFF by default, and you almost certainly want it off. When a search matches fewer than 25 jobs, jobs.ie pads the same results list with unrelated 'similar' jobs and gives no visual break — MEASURED 2026-09-06: /jobs/nurse/in-galway says '1 Nurse job in County Galway + 20 km' and renders 12 cards, 11 of them out of scope. This scraper reads the real match count from the page heading and drops the padding, so you are never billed for it. Turn this on only if you want that padding too; those rows are flagged `is_related_match: true`.

## `includeDetails` (type: `boolean`):

Opens each posting for the exact posted date, expiry date, employment type, industry taxonomy, work type (Permanent/Temporary/Contract — this is the ONLY place it exists), full address, GPS coordinates and the complete job description. Costs one extra request per job and is billed as its own cheaper event, so leave it off when you only need the lead list. With it OFF these 14 of the 37 columns are null on every row (measured 2026-09-06, run FrlXsVHESixWU0Dm2): town, region, postcode, country, latitude, longitude, work\_type, employment\_type, industry, direct\_apply, date\_posted, valid\_through, description\_text, description\_html.

## `descriptionFormat` (type: `string`):

How to return the full description when details are fetched. 'None' keeps the row small when you only want the structured fields.

## `descriptionMaxLength` (type: `integer`):

Truncate the description to this many characters, ellipsis included - set 200 and you get exactly 200. 0 = keep the whole thing (measured 2026-09-06 over 30 postings: 1,304-13,309 characters, median 6,263).

## `postedAfter` (type: `string`):

Client-side date filter, e.g. 2026-08-01. Exact when 'Fetch full job details' is on (it compares the posting's real datePosted); with details off it falls back to the card's relative text ('3 weeks ago') and is approximate — use 'Posted within' above for a precise server-side cut. With details ON the exact test runs INSIDE the crawl, page by page, so it never decides on a truncated sample and the cap still fills; the cost is one detail fetch per candidate card, including cards the date test then drops. Dropped rows are never billed (run UNly7pVXDQJ0LTgCV, 2026-09-06: 9 detail pages fetched, 9 dropped on the exact date, 0 rows charged).

## `postedBefore` (type: `string`):

Client-side date filter, e.g. 2026-08-31. Same accuracy, same in-crawl ordering and the same billing note as 'Posted on or after' above.

## `includeKeywords` (type: `array`):

Keep only jobs whose title, company or snippet contains at least one of these words. Case-insensitive. On a single-posting start URL there is no card to read, so the test is re-run after that posting's detail page is fetched, over its title, company and full description — and a row that fails it is never delivered and never billed (run Qf9U8YUqYKafcExXM, 2026-09-06: 0 rows, 0 charged).

## `excludeKeywords` (type: `array`):

Drop jobs whose title, company or snippet contains any of these words — e.g. 'apprentice', 'unpaid', 'internship'. Case-insensitive. On a single-posting start URL there is no card to read, so the test is re-run after that posting's detail page is fetched, over its title, company and full description — and a row that fails it is never delivered and never billed (run a08URQfSoptruM4JZ, 2026-09-06: 0 rows, 0 charged).

## `excludeSponsored` (type: `boolean`):

Drop paid placements, using jobs.ie's own flags for the card (sponsored, top job, highlighted, partnership, or a FEATURED / PREMIUM / TOP JOB badge) rather than the visible label. Honest measurement, 2026-09-06: jobs.ie currently runs NO promoted inventory - 0 of 1,126 cards on a complete /jobs/dublin sweep were flagged - so this normally drops nothing. Turning it on makes every search page parse jobs.ie's hydration blob, which roughly doubles the run's peak memory. The 'NEW' badge means recently posted, not promoted, and is never dropped.

## `skipReposts` (type: `boolean`):

Keep only the first posting for each identical job title + company pair. Irish agencies repost the same role under new ids; this collapses them so you do not pay twice for one opening. A collapsed repost still counts as a page's 'new id' for end-of-results detection, so a page full of reposts can never end the crawl early (run zHeEhTKBV8YtM8RI3, 2026-09-06: page 2 read 25 cards, kept 23, and the seen-id count still advanced by the full 25).

## `dedupeAcrossRuns` (type: `boolean`):

Remembers every job id already delivered and drops it in all future runs, forever. Ideal for topping up a CRM. State lives in a named key-value store, so it survives between runs.

## `dedupeStoreName` (type: `string`):

Name of the key-value store holding the 'already delivered' job ids. Use different names to keep separate campaigns apart.

## `incrementalMode` (type: `boolean`):

Compares this run against the ids this state key has already seen and returns only job ids that were not there before. The first run seeds the baseline and returns everything; every later run ADDS what it saw to the baseline (a capped run only walks part of the board, so replacing the baseline with one run's ids would re-deliver everything it did not reach). This plus 'Posted within = 24 hours' plus a daily schedule is the new-jobs monitor: jobs.ie postings expire exactly 30 days after they are posted, so the value here is the flow, not the stock.

## `stateKey` (type: `string`):

Name this monitor. Different searches on a schedule should each use their own key so their baselines do not collide.

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

CLIENT-SIDE only, and deliberately honest about it: jobs.ie has NO server-side sort parameter — ?sortby=2 and ?sort=date were measured returning the same match count AND the same first job id as the unsorted page. So this reorders the rows this run collected; it does not change which rows jobs.ie hands over. To bias which jobs you get toward recent ones, use 'Posted within', which is a real server-side filter.

## `outputFields` (type: `array`):

Keep only these columns, in this order — e.g. company, title, location, salary\_raw, job\_url. Leave empty for all 37 fields. 14 columns (town, region, postcode, country, latitude, longitude, work\_type, employment\_type, industry, direct\_apply, date\_posted, valid\_through, description\_text, description\_html) are null unless 'Fetch full job details' is on, so asking for only those with the add-on off gives you rows of nulls — and, with 'Drop empty fields' also on, no rows at all (see that setting).

## `excludeEmptyFields` (type: `boolean`):

Omit null/empty keys from each row. Makes JSON output smaller; CSV exports stay aligned either way. This runs AFTER 'Output columns' has narrowed the row, so it can strip a row down to nothing: any row left with no fields at all is dropped before delivery and is never charged (measured: outputFields \[date\_posted, work\_type, industry] + this switch, with the detail add-on off, delivered 6 empty {} rows and charged 6 events on build 0.1.13 — the same input now delivers 0 rows and charges $0). The run log names the column that emptied them.

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

jobs.ie is unusable from Apify datacenter IPs (re-measured 2026-09-06 on /jobs/dublin: datacenter 0 of 3 - one 403 and two 45-second read timeouts - while Irish RESIDENTIAL was 3 of 3, 200 + 25 cards in 2.0-5.2s), so this defaults to an Irish RESIDENTIAL proxy. GB and US residential also work if you need them. Leave it as-is unless you know what you are doing.

## `requestConcurrency` (type: `integer`):

Parallel requests for the detail add-on. jobs.ie answers in about 2 seconds; keep this modest to stay polite and avoid resets.

## `maxRequestRetries` (type: `integer`):

Each retry uses a brand-new proxy session. jobs.ie occasionally stalls a request against a ~2.5s median (measured 2026-09-06: 46 consecutive search pages, 0 stalls, median 2.49s, slowest 5.99s; detail pages did throw two proxy UPSTREAM504s that retries absorbed), so do not lower this.

## Actor input object example

```json
{
  "keywords": "care assistant",
  "location": "dublin",
  "searchTerms": [],
  "startUrls": [],
  "contractType": "any",
  "postedWithin": "any",
  "minSalary": 0,
  "salaryPeriod": "year",
  "companyType": "any",
  "companyId": 0,
  "maxItems": 25,
  "maxPages": 0,
  "includeRelatedJobs": false,
  "includeDetails": false,
  "descriptionFormat": "text",
  "descriptionMaxLength": 0,
  "includeKeywords": [],
  "excludeKeywords": [],
  "excludeSponsored": false,
  "skipReposts": false,
  "dedupeAcrossRuns": false,
  "dedupeStoreName": "jobs-ie-seen",
  "incrementalMode": false,
  "stateKey": "default",
  "sortBy": "relevance",
  "outputFields": [],
  "excludeEmptyFields": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "IE"
  },
  "requestConcurrency": 3,
  "maxRequestRetries": 5
}
```

# Actor output Schema

## `jobs` (type: `string`):

Title, employer, company id, county, salary (raw + parsed min/max/period/currency), work type, industry, posted & expiry dates, GPS coordinates and the full job description.

# 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 = {
    "keywords": "care assistant",
    "location": "dublin",
    "maxItems": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/jobs-ie-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 = {
    "keywords": "care assistant",
    "location": "dublin",
    "maxItems": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/jobs-ie-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 '{
  "keywords": "care assistant",
  "location": "dublin",
  "maxItems": 25
}' |
apify call scrapersdelight/jobs-ie-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/jobs-ie-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/EAfY7U9mLe3UnGoqj/builds/7aqtFVgQ9jP3W1yiC/openapi.json
