# Officeworks Australia Scraper Lite (`abotapi/officeworks-scraper-lite`) Actor

Scrape Officeworks (officeworks.com.au) products: name, brand, title, price, GST, was/original price, deals and clearance specials, stock by state, images, variants, specifications, ratings and reviews. Search by keyword, specials or filters, or paste product, search or category URLs.

- **URL**: https://apify.com/abotapi/officeworks-scraper-lite.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Officeworks Scraper Lite: Products, Prices, Stock & Reviews

Officeworks Scraper Lite turns officeworks.com.au into a clean product data feed. Search by keyword with brand, price and rating filters, browse the Deals and Clearance collections, or paste product, search or category links directly. Get the current price plus any genuine was-price, savings and discount, the canonical product title, the variant matrix, availability by Australian state, and optionally the full product description, specifications, image gallery and customer reviews with an aggregate rating summary and star-rating distribution. Export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Two ways to find products.** Search by keyword with brand, price and rating filters, or paste product, search, category or specials links directly. No need to build URLs by hand.
- **Deals and Clearance, covered natively.** Browse Officeworks' own promotional collections instead of (or alongside) a keyword search, with the same filters and sort still applying.
- **Genuine reductions only.** Officeworks runs an every-day-low-price model; `wasPrice`, `savingsAmount` and `savingsPercent` are populated only when a product is genuinely reduced below its everyday price, never fabricated.
- **Full product detail on demand.** Switch on detail enrichment for the complete description, feature bullets, specifications, identifiers, image gallery and variant matrix, or leave it off for a faster, cheaper listing-only run.
- **Ratings and reviews.** Every product already carries the aggregate rating and review count; detail enrichment adds the star-rating distribution and individual customer reviews.
- **Availability by state.** See which Australian states a product ships to and whether it is available in-store or online.
- **Built for schedules.** Incremental mode returns only new and changed products on recurring runs, and Resume skips products a previous run already collected (see the limits under **Resume and recurring updates**).

### Use Cases

- **Price monitoring and repricing tools:** track price, was-price, savings and specials on a schedule to react to Officeworks' pricing moves.
- **Comparison shopping and catalog aggregators:** build a structured Officeworks product catalog with identifiers, categories and images.
- **Review and reputation analytics:** collect ratings, review counts and individual reviews to gauge customer sentiment per product or category.
- **Merchandising and market research:** monitor the Deals and Clearance collections to track what Officeworks is promoting and when.
- **Availability tracking:** watch stock by state and online availability for a specific set of SKUs.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `sku` | `"SAMPLE0001"` |
| `name` | `"Sample A4 Display Book 20 Pocket"` |
| `title` (canonical, brand-prefixed) | `"Sample Brand A4 Display Book 20 Pocket"` |
| `brand` | `"Sample Brand"` |
| `url` | Officeworks link to the product |
| `price` | `12.5` |
| `edlpPrice` | `12.5` (the everyday price) |
| `wasPrice` / `savingsAmount` / `savingsPercent` | `null` unless the product is genuinely reduced below `edlpPrice` |
| `isOnSpecial` / `promoLabel` | `true` / `"Deal"` (also `"Clearance"`, `"Low Price"`, `"New"`, `"Planet Positive"`) |
| `isNew` / `isClearance` | `false` / `false` |
| `currency` / `gstRate` | `"AUD"` / `10.0` |
| `gtin` / `manufacturerPartNumber` / `productType` | product identifiers |
| `availableStates` | `["NSW", "VIC", "QLD"]` (an index flag from the product listing, not a delivery answer) |
| `isAvailableInStore` / `availableOnline` | `true` / `true` (index flags too; use the home delivery check below to see whether a product can be delivered to your postcode). The three index flags come from the product listing only: a pasted product link (`/p/...`) is read from its product page, so its rows (full and compact) do not carry them |
| `deliveryPostcode` / `deliveryZone` / `deliveryType` / `deliveryAvailable` / `deliveryQuantity` / `deliveryEtaFrom` / `deliveryEtaTo` / `clickAndCollectQuantity` / `inStoreQuantity` / `deliveryStoreId` / `deliveryStoreName` / `deliveryChanges` | home delivery check only: `"4209"` / `"Regional"` / `"standard"` / `true` / `6` / `"2026-11-03"` / `"2026-11-03"`, then the click & collect and in-store counts at the store named in the same row; `deliveryChanges` appears with incremental mode only |
| `variants` / `variantAxes` / `variantCount` | the variant matrix (per-variant sku, url, attributes, thumbnail) |
| `category` / `categories` / `categoryHierarchy` | `"Display Books"` / breadcrumb path / full category chains |
| `shortDescription` / `longDescription` / `features` | product copy and feature bullets (detail enrichment) |
| `specifications` / `attributes` | name/value pairs (detail enrichment) |
| `imageUrl` / `images` | primary image / full gallery |
| `rating` / `reviewCount` | `4.3` / `42`, present on every product |
| `ratingDistribution` / `recommendedCount` | star breakdown and recommended count (detail enrichment) |
| `reviews[]` | per-review rating, title, body, author, ISO-8601 date, helpful count, pros/cons and purchase location (detail enrichment) |
| `reviewsCollected` | how many reviews were actually collected for this product |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` / `previousPrice` / `priceChanges` / `variantChanges` | incremental mode only: `"UPDATED"` / `["price"]` / dates / `12.5`; `priceChanges` and `variantChanges` on compact rows only, see below |

Every record also carries `seoPath`, `urlKeyword`, `colour`, `multipackSize`, `unitsPerPack`, `brandUrl`, `itemsPerUnit`, `totalUnits`, `categoryPath`, `metaDescription`, `hasBusinessPrice`, `ratingScale`, `searchMode` (`"search"` or `"url"`), `source` (`"officeworks.com.au"`) and `country` (`"AU"`). Detail enrichment also adds `description` (long description, falling back to the short one) and `breadcrumbs` (the category trail as a list of titles). `rangedOnline`, `rangedRetail` and `published` are availability flags that appear as `true`/`false` when the site reports them and stay `null` otherwise. Products with no reviews leave `rating`, `reviewCount`, `ratingDistribution` and `reviews` absent rather than faked; `reviewsCollected` then reads `0`.

**Dataset views.** The dataset has four views: an overview of every product, a detail view (description, specifications, gallery, variants), a reviews view, and a changes view for incremental runs (status, changed fields and the compact change sections). Every run also stores an `OUTPUT` summary in its key-value store (summary only, never extra dataset fields): `productsReturned`, `scanned`, `reachedEndOfResults`, `scanComplete` (no cap, cost limit, time limit, Resume or restart cut the scan short, so a missing product could be marked `EXPIRED`; it does not guarantee that every listed product was read: a product page that could not be read, or a search that stopped early on overlapping results, can still be missed), `stateCommitted` (`true` when the saved state was updated, `false` when another run had saved it first, `null` outside incremental mode), `stoppedEarlyReason` (`costLimit`, `timeLimit`, `maxProducts`, `halted` or `null`), `expiredReturned`, and with the delivery check on `delivery`: `productsLookedUp`, `answered`, `noAnswer` by cause (`noResponse`, `refused`, `notFound`, `serverError`, `unreadable`, `unknownProduct`), `skippedByBreaker` and `attemptsByConnection` (requests per connection type; residential requests are billed to your proxy plan). The log ends with the same delivery line.

### How to Use

1. Pick a **mode**: `search` (keyword and filters) or `url` (paste product, search, category or specials links).
2. In search mode, add one or more keywords, or pick a **Specials / offers collection** (Deals or Clearance) instead of (or alongside) your keywords. Apply Brand, price, rating and sort filters as needed.
3. Turn **Fetch full product detail + reviews** on (the default) for the complete description, specifications, gallery and customer reviews, or off for a faster, listing-only run.
4. Set **Max products** to control run size and cost, then click **Start**.

**Search by keyword with filters:**

```json
{
  "mode": "search",
  "queries": ["notebook"],
  "brand": "Sample Brand",
  "minPrice": 20,
  "maxPrice": 200,
  "minRating": 4,
  "detailEnrichment": true,
  "maxItems": 50
}
```

**Browse the Clearance collection, cheapest first:**

```json
{
  "mode": "search",
  "queries": [],
  "specialsCategory": "clearance",
  "sortBy": "priceAsc",
  "maxItems": 100
}
```

Accepted link shapes in url mode:

| Link shape | Example | What you get |
| --- | --- | --- |
| Keyword search | `/shop/officeworks/search?q=laptop` | every page of results |
| Search starting at a page | `/shop/officeworks/search?q=laptop&page=2` | results from that page onward |
| Category (nested path) | `/shop/officeworks/c/office-supplies/pens/ballpoint-pens` | every page of the category |
| Category starting at a page | `.../ballpoint-pens?page=2` | results from that page onward |
| Top-level category | `/shop/officeworks/c/art-supplies` | every page of the category |
| Product page | `/shop/officeworks/p/<product-slug>` | the one product |
| SKU as keyword | `/shop/officeworks/search?q=IMMM6162` | products matching that SKU |
| Deals / clearance | `/shop/officeworks/deals`, `/shop/officeworks/clearance` | the deals or clearance collection (these links redirect home on the site, so the collection is browsed directly) |
| SKU-only product link | `/shop/officeworks/p/IMMM6162` | a link that ends in just the SKU is resolved through SKU search (the same product is returned), with a warning in the log if nothing matches |
| Host without `www` | `https://officeworks.com.au/...` | treated as the `www` host |

A link that is none of these is reported with a warning in the log and returns nothing for that link.

**The default URLs list holds one example of each shape above** (ten links). Max products is a cap on the whole run, so with the default of 20 only the first link or two are reached; raise Max products, or replace the list with your own links, before running at scale.

**Paste a single product link:**

```json
{
  "mode": "url",
  "urls": ["https://www.officeworks.com.au/shop/officeworks/p/sample-product-sku"],
  "detailEnrichment": true,
  "maxReviewsPerProduct": 30
}
```

**Track a category daily: selling price, stock and delivery to 4209, compact changes only:**

```json
{
  "mode": "url",
  "urls": ["https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens"],
  "incrementalMode": true,
  "changeScope": "sellingPriceAndStock",
  "compactUpdates": true,
  "deliveryPostcode": "4209",
  "verifyHomeDelivery": true,
  "emitExpired": true,
  "expireAfterMissedScans": 2,
  "maxItems": 0,
  "maxPages": 0
}
```

The first run returns every product in full as `NEW`, with its delivery answer for 4209. Later runs return a product only when its selling price moved, its stock flags changed, a size or colour joined or left its family, or the answer to "can it be delivered to 4209" flipped; each comes back as a short `UPDATED` row with the old and new values in `priceChanges`, `variantChanges` and `deliveryChanges`. Unchanged products are not returned or billed. A product missing from two complete scans in a row comes back once as a short `EXPIRED` row.

**Which field holds what (daily monitoring with delivery):**

| You need | Field | Notes |
| --- | --- | --- |
| Product ID | `sku` | Present on every row, full and compact. |
| Variant IDs | `variants[].sku` | On `NEW` rows. On a compact `UPDATED` row, a size or colour that joined or left the family is listed in `variantChanges` with its `sku` and `change` (`ADDED` / `REMOVED` / `CHANGED`). |
| Current payable price | `price` | The selling price, on every row. `previousPrice` and `priceChanges.price` hold the old value when it moved. |
| Delivery availability | `deliveryAvailable` | `true` / `false` is a confirmed answer for `deliveryPostcode`; `null` means the answer could not be read this run. |
| Barcode | `gtin` | Text with leading zeros kept. On `NEW` rows and on compact `UPDATED` / `EXPIRED` rows. A few products have none. |

Both samples below are illustrative placeholders, abridged.

A `NEW` row (first run, complete record):

```json
{
  "sku": "SAMPLE0001",
  "name": "Sample A4 Display Book 20 Pocket",
  "brand": "Sample Brand",
  "url": "https://www.officeworks.com.au/shop/officeworks/p/sample-a4-display-book-sample0001",
  "gtin": "9300000012345",
  "price": 12.5,
  "currency": "AUD",
  "variants": [{ "sku": "SAMPLE0001", "attributes": { "Colour": "Blue" } }],
  "deliveryPostcode": "4209",
  "deliveryZone": "Regional",
  "deliveryType": "standard",
  "deliveryAvailable": true,
  "deliveryQuantity": 6,
  "changeType": "NEW",
  "changedFields": [],
  "firstSeenAt": "2026-11-04T01:00:00Z",
  "lastSeenAt": "2026-11-04T01:00:00Z"
}
```

An `UPDATED` compact row (a later run: the price dropped and delivery flipped):

```json
{
  "sku": "SAMPLE0001",
  "name": "Sample A4 Display Book 20 Pocket",
  "brand": "Sample Brand",
  "url": "https://www.officeworks.com.au/shop/officeworks/p/sample-a4-display-book-sample0001",
  "gtin": "9300000012345",
  "currency": "AUD",
  "price": 9.0,
  "changeType": "UPDATED",
  "changedFields": ["price", "deliveryChanges"],
  "firstSeenAt": "2026-11-04T01:00:00Z",
  "lastSeenAt": "2026-11-05T01:00:00Z",
  "previousPrice": 12.5,
  "priceChanges": { "price": { "old": 12.5, "new": 9.0 } },
  "variantChanges": null,
  "otherChanges": null,
  "deliveryPostcode": "4209",
  "deliveryAvailable": false,
  "deliveryChanges": { "deliveryAvailable": { "from": true, "to": false } }
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/officeworks-scraper-lite").call(run_input={"mode": "search", "queries": ["notebook"]})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["name"], product["price"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/officeworks-scraper-lite').call({ mode: 'search', queries: ['notebook'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Multiple keywords and specials in one run

Every keyword (and a specials collection, if chosen) is searched as its own target within the run, one after another, sharing the same overall dedup set. Pasted search and category links in url mode work the same way. That shared set has three real, confirmed side effects worth knowing:

- **Overlap is returned once.** A product returned by an earlier target is never returned again by a later one in the same run, so the run total can be lower than the sum of each target searched on its own.
- **A later target can stop on its very first page.** A target stops walking as soon as it reaches a page on which every product has already been handled. That check uses the run-wide set, so if a later target's first page is made up entirely of products an earlier target already handled (two near-synonymous keywords such as "pen" and "pens", for example), that target stops right there and its later pages, including products no earlier target returned, are never read in that run.
- **Products skipped by a cap still count as handled.** When a page has more candidates than the remaining room (see below), the ones beyond the room are marked as handled without being returned. A later target in the same run then silently skips those products too, and can stop early on them as described above.

In incremental mode with `emitExpired` on, a target that stopped early this way still counts as fully scanned, so tracked products on its unread pages can be returned (and billed) as `EXPIRED` even though they are still listed; they come back as `REAPPEARED` on a later run that reads them. If you need every target's own complete result set, or you use `emitExpired`, run overlapping searches as separate actor runs (each with its own automatic state) instead of combining them into one `queries` or `urls` list.

#### How Max products and Max pages shape a run

**Max products** (`maxItems`) stops the whole run once that many products have been returned; **Max pages** (`maxPages`) caps how many result pages are walked per keyword or URL (`0` = walk every page). On a capped run, a page's worth of candidates beyond the remaining room is not inspected in this run. If the platform restarts a run (server migration or Resurrect), the products it saved before the restart count toward **Max products**, so the run as a whole never returns more than that many products.

**Incremental-mode tip:** the remaining room for a page is computed before incremental mode knows which candidates will turn out `UNCHANGED` and get suppressed. Suppressed products still use up slots, so with a result page of up to 48 products and a `maxItems` below that (the default is `20`), a new or changed product later on the same page can be skipped in this run even though the run ends well under `maxItems`. Worse, with `emitExpired` on: because suppressed products are not returned, such a run can still finish under `maxItems` and count as a complete scan, and then every tracked product that was skipped this way is returned (and billed) as `EXPIRED` even though it is still listed. Those products come back as `REAPPEARED` (billed again) on a later run that reaches them. Set `maxItems` to `0` (and leave `maxPages` at `0`) for incremental runs, especially with `emitExpired` on.

#### Check home delivery to a postcode

Set **Delivery postcode** (four digits, for example `4209`) and turn on **Check home delivery live**. For every returned product the actor asks Officeworks, at that moment, whether the product can be delivered to that postcode, and adds:

- `deliveryType`: the delivery speed label the site reports (for example `standard`, `extended`, `nextDay`), `nearest` when only store stock exists and nothing can be delivered, or `outOfStock`.
- `deliveryAvailable`: `true` only when the type is not `outOfStock` and the delivery quantity is above zero.
- `deliveryQuantity`, `deliveryEtaFrom`, `deliveryEtaTo` (ISO dates), and `deliveryZone` for the postcode (`Metro`, `Regional`, `Country`). A `deliveryQuantity` of 1,000,000,000 or more is the site's marker for "no stock limit" (seen on some items), not a real count: it is passed through unchanged, and `deliveryAvailable` is still `true` for it.
- `clickAndCollectQuantity` and `inStoreQuantity` at the store shown in `deliveryStoreId` / `deliveryStoreName`. By default that is the store whose own postcode is closest to yours; set **Pickup store** (for example `W444`) to choose another. Delivery type, quantity and arrival dates did not depend on the store in our checks of in-stock items.

A product whose answer could not be read gets `null` delivery fields and is never reported as unavailable: the site returned nothing usable, an error or a page for another product, kept refusing the request, or answered for a SKU it does not know (the site answers an unknown SKU as "out of stock" without the product details a real answer always carries, so such an answer is treated as not read, not as out of stock). An answer that could not be read is retried a few times on a fresh connection before it is given up. Each product is looked up once per run, with a small number of lookups in flight at a time; if lookups keep failing, most of the remaining ones are skipped for that run and an occasional one still tries, so the check resumes if the service recovers. An answer about a SKU the site does not know is not a failure for that purpose: the service is answering, so the check carries on for the products after it. The check is off by default, and a run without it is unchanged. In incremental mode only the yes/no answer (`deliveryAvailable`) is compared, for the same postcode: stock counts, the speed label (which changes with the time of day), arrival dates and the store-specific counts move without the answer changing, so they never make a product count as changed. When the answer flips, an otherwise unchanged product is returned as UPDATED with the flip listed in `deliveryChanges`. The first answer that can be read for an already tracked product (its earlier lookups could not be read, the check was just turned on, or the postcode changed) is returned once as UPDATED with `deliveryChanges` `{"deliveryAvailable": {"from": null, "to": true}}` (or `false`), so you always receive every product's first known answer; an answer that was known and then could not be read for a run is not reported again when it is read again. Changing only the pickup store starts no new comparison. A delivery postcode needs the check turned on, and the run stops with a message otherwise. A pickup store without the check is ignored (a warning is logged). The lookups consume additional platform usage; there is no enrichment event fee. Turning the check on for a search that is already tracked, or changing the delivery postcode, therefore returns every tracked product once (one product row each, see **How much does it cost?**).

#### Using the rows in your own system

| You see | It means |
| --- | --- |
| A full row (`changeType` `NEW`, `UPDATED`, `REAPPEARED` or `UNCHANGED`) | The whole product record as read this run. |
| A compact row (Compact update rows on) | Only identifiers, current price and stock, and the change sections; keep your stored copy of the other fields. |
| `deliveryAvailable` `true` / `false` | The site answered this run: deliverable / not deliverable to the postcode. |
| `deliveryAvailable` `null` | The answer could not be read this run (it does not mean unavailable). `deliveryPostcode`, `deliveryZone` and the store fields are filled even then, so do not use them to tell whether the answer was read. |
| No `gstRate` / `ratingScale` on a row | The product page could not be read (or Detail enrichment is off): the row holds the search result only. |

**Joining with Kmart data.** The Kmart Australia Scraper returns `apn` as a number (leading zeros dropped), while this Actor returns `gtin` as text with its leading zeros kept (for example `"071641171725"`). Join on the number (`int(gtin) == apn`), or pad both to 14 digits. A few Officeworks products have no `gtin`.

#### Resume and recurring updates

There are two different things here, pick the one that matches what you're doing:

| Need | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` |
| Run the same search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous crawl: paste a run ID or dataset ID and this run skips products already collected there, returning only the remaining new products. Two real, confirmed limits:

- **It can return nothing if the earlier run collected a whole first page.** The resumed walk starts again from page 1, and a page on which every product was already collected ends that search or link. So if the earlier run returned every product on the first result page of a keyword or link (up to 48 products), the resumed run stops there and returns nothing for it, with a "No products matched" status, even though later pages were never collected. Resume works as intended only when the earlier run stopped part-way through the first page; otherwise re-run the search with a larger **Max products** instead.
- **Pasted product links are not skipped.** In url mode, a pasted product link (`/p/...`) is matched by its link text rather than by its SKU, so resuming a run that already returned it fetches and returns (and bills) that product again. Remove already-collected product links from `urls` before resuming.

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the same search by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED` and `REAPPEARED` products by default; unchanged products are suppressed unless `emitUnchanged` is on. Change detection (`UPDATED`) compares the product's own data: price, specials and promo label, stock and availability, name, brand, category, images and the other catalogue fields. The order of `availableStates` and of the size/colour family (`variants`) is not compared (the site returns the same states in varying order): only which states and which sizes/colours, and the rows you receive keep the site's order. `rating`, `reviewCount`, `ratingDistribution`, `recommendedCount`, `reviewsCollected` and `reviews` are returned but never cause `UPDATED`, because other customers change them without the product changing. Use `ignoreFieldsForChanges` to leave out further fields; changing that list does not turn the whole baseline into `UPDATED`. A run that started from a pasted `?page=N` URL with N above 1, or where Max products trimmed a page, does not count as a full scan and never returns `EXPIRED` rows. Turn on `emitExpired` to also get a tombstone row for a product that was tracked before but is no longer found; this only happens once a run counts as having fully scanned the tracked search (not when Max products or Max pages capped it, and not on a Resume run). A run can count as complete while still having skipped some listed products, so read the tips below and under **Multiple keywords and specials in one run** and **How Max products and Max pages shape a run** before relying on `EXPIRED`.

State is isolated per mode/search/URL/specials/brand/sort/price/rating/stock setup automatically; set `stateKey` to name a campaign, or to deliberately share state across differently-configured runs. Switching **Detail enrichment** on or off for a search with no `stateKey` continues from the newest saved baseline of that search under either setting (also when you switch back and forth), so the search is not returned as `NEW` again and a change already returned under one setting is not returned again under the other. The search result and the product page are compared source by source: the price shown in search results can differ from the product page's price, so a search-result price is only ever compared with an earlier search-result price, and a product-page price with an earlier product-page price.

**What counts as a change** (`changeScope`): `all` (the default) compares the whole product record as described above. `priceAndStock` compares only the price fields (`price`, `edlpPrice`, `wasPrice`, `savingsAmount`, `savingsPercent`, `isOnSpecial`, `isClearance`), the stock flags (`availableOnline`, `availableStates`, `isAvailableInStore`) and the sizes and colours in the product family (`variants`, by SKU). `sellingPriceAndStock` is the same but counts a price change only when `price`, the price a buyer pays today, moves. At Officeworks every size or colour is its own product with its own SKU, price, stock and delivery answer, so its changes come back on its own row; within a product, `variantChanges` reports a size or colour that joined (`ADDED`) or left (`REMOVED`) the family. With **Check home delivery live** on, a flip of `deliveryAvailable` returns an `UPDATED` row in every option (`changedFields` then names `deliveryChanges`); arrival dates, quantities and the speed label never do. The option never changes the automatic state key: switching it keeps your saved baseline, and nothing is returned again because of the switch.

**Compact update rows** (`compactUpdates`): `UPDATED` and `EXPIRED` rows carry only `sku`, `name`, `brand`, `url`, `gtin` (when the product has one), `currency`, the current `price` and stock flags (none for a pasted product link, see the output table), `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`, `previousPrice`, `priceChanges` (old and new value of each moved field, for example `{"price": {"old": 12.5, "new": 9.0}}`), `variantChanges` (a size or colour `ADDED` to or `REMOVED` from the family and, with "All fields", one whose label, link or image moved, as `CHANGED` with its old and new values), `otherChanges` (old and new value of every other moved field, for example the description, so a compact `UPDATED` row always says what changed) and, with the delivery check on, the delivery fields and `deliveryChanges`. `NEW` rows always carry the complete record. Full `UPDATED` rows also carry `previousPrice` (the price before this run, empty when the price did not move). With "All fields" and compact rows, the full record of each compact `UPDATED` row is also saved in the run's own key-value store (`INCREMENTAL_RECOVERY_<sku>`) just before the row is saved, so a run restarted by the platform does not report the same change again on the next run; only a restart of that run reads it.

**Missing products** (`emitExpired`, `expireAfterMissedScans`): a product is returned as `EXPIRED` only after it has been missing from `expireAfterMissedScans` complete scans in a row (default 1); a product that comes back resets the count. Scans that were not complete (capped by Max products or Max pages, a Resume, a restarted run, a product link whose page could not be read) never count as a miss. A product that is still listed but out of stock is not `EXPIRED`: that shows in the stock and delivery fields.

**Checks that could not be read**: a delivery answer that could not be read leaves the delivery fields `null`, is never reported as unavailable, never counts as a change; the last known answer is kept for the next comparison. A product page that could not be read on a run returns the search result only. That row is compared only on the fields that come from the search result on both runs (`isClearance`, `availableOnline`, `availableStates`, `isAvailableInStore`); the saved product-page values, including its price, are kept untouched, and a price or detail change is reported on the next run that can read the page.

**Start from an existing dataset** (`baselineDatasetId`): if you already scraped the search with this Actor, pick that run's dataset and your first incremental run returns only products that changed since then. Pick it unedited (rows that do not look like this Actor's products stop the run before scraping, with a message saying why) and from a run with the same search and detail setting. Compact rows and `EXPIRED` rows in it are skipped. It is only used while the state key has no saved state, and cannot be combined with Resume.

**Coming from the Officeworks Australia Scraper?** If you tracked the same State key (or the same search settings) there, pick that scraper's history store (`officeworks-com-au-incremental-state`) in "Continue from the Officeworks Australia Scraper's history" and the first Lite run continues from that saved history, so you are not given the whole search as `NEW` again. With no State key, the history saved there under either Detail enrichment setting of the same search is used. Your earlier history is only read, never changed, and nothing needs to be copied or run. It is used only on the first Lite run for that search (not once Lite already tracks it, and not with a starting dataset or Resume). If the picked store cannot be read, is not that scraper's history store, or holds something that is not its saved state, the run stops before scraping with a message saying why, instead of returning everything as `NEW`; if it simply has nothing saved for this search, the run says so and starts fresh.

**Tips (real, confirmed behavior worth knowing before you rely on this):**

- **`emitExpired` is not capped by Max products.** Once a run proves it scanned the tracked search completely, every previously-tracked product that is no longer found is returned as `EXPIRED` in one batch, regardless of `maxItems`. If a monitored search shrinks a lot between runs (a big Clearance collection selling out, for example), the `EXPIRED` batch on that run can be much larger than your usual `maxItems`, and is billed accordingly. Turn `emitExpired` off, or expect and budget for an occasional larger run, if this matters to your costs.
- **Combining `resumeFromRunId` with a brand-new `incrementalMode` campaign bills one phantom `UPDATED` per resumed product on the very next scheduled run.** Resuming into a fresh (first-ever) incremental baseline seeds that baseline from the resumed dataset without re-fetching those products, so their stored fingerprint is not yet known. The next scheduled run then reports every one of those products as `UPDATED` with every field listed under `changedFields`, even if nothing actually changed. It happens exactly once per product, right after that bootstrap; from then on comparisons are against a real fingerprint. If you don't want this, run a plain (non-incremental) resume first, then start incremental mode fresh from a normal full run.
- **Reusing the same `stateKey` for a materially different search is a real feature, but it can bill unrelated products as `EXPIRED`.** If you deliberately point the same `stateKey` at a new keyword, URL or filter set, every product tracked under the old configuration that isn't rediscovered by the new one looks exactly like a product that disappeared, and (with `emitExpired` on) is billed as `EXPIRED` the moment a run under the new configuration completes a full scan. Only share a `stateKey` across scopes that are meant to be tracked together; leave it empty for anything else so each distinct search gets its own isolated, auto-derived state.
- **A run restarted by the platform (server migration or Resurrect) never returns `EXPIRED` rows.** The restarted run continues from its saved position and skips the keywords, links and pages it had already finished, so it cannot tell which tracked products are really gone. It therefore skips `EXPIRED` detection for that run, and the products it had already delivered before the restart are kept in the saved state, so they are not returned as `NEW` or `UPDATED` again.
- **A run that is stopped outright (it hit its timeout or ran out of memory) and is NOT resurrected.** About 75 seconds before its time limit (or, for a run with a short time limit, a quarter of it) a run stops starting new products, saves what it delivered and ends with a status message, so the next run continues normally (it is not a complete scan, so it returns no `EXPIRED` rows). If a run is killed anyway before it could save, the next run of the same search adds the products that run had already returned to the saved state before it starts, so they are not returned or billed again. This needs to read that run's dataset and storage: when it has been deleted, or this run is not allowed to read it, the earlier run is skipped with a log line and those products can come back once; if reading it keeps failing for another reason, the run stops without saving or billing anything, and after three such runs the earlier run is given up the same way. This also holds when an older, already finished run is resurrected after newer runs: the saved state records which run wrote each product, so a product that a newer run has since saved (changed, unchanged or `EXPIRED`) keeps that newer answer and is not returned or billed again. To do this, an incremental run that returns at least one product writes one small `INCREMENTAL_REPLAY_BASES` record to its own key-value store. Products last saved by a version of this Actor from before this ordering existed have no such record, and are treated as the oldest. If that record cannot be saved (the run's storage keeps refusing the write after retries), the run stops before saving any further product and fails with a message saying so: nothing it could not order is saved or billed, and those products are returned by the next run. Likewise, if the saved state or the run's restart records cannot be read (storage keeps failing after retries, or returns something unreadable), the run stops before it saves, bills or changes anything, keeps its restart point and fails with a message; resurrect it (or run again) once storage is reachable. A saved state that cannot be read is never treated as empty, so it never returns the whole search as `NEW`. A product returned as `EXPIRED` just before a restart stays expired after it, so it is not returned as `EXPIRED` a second time, and comes back as `REAPPEARED` if it is listed again.
- **Do not run the same search with different Detail enrichment settings at the same time.** Two simultaneous runs of one search, one with Detail enrichment on and one with it off, keep separate saved states that are reconciled afterwards. When their saves happen at the same moment, the run with product pages wins a tie, so if the other run saw a later change, that one change can be returned (and billed) once more by the next run. Runs that follow one another, in any order and with either setting, are not affected.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (keyword + filters) or `url` (scrape pasted URLs). |
| `queries` | array | `["laptop"]` | One or more keywords to search (search mode). Each keyword is searched separately. |
| `specialsCategory` | string | (none) | Browse `deals` or `clearance` instead of (or alongside) your keywords. |
| `urls` | array | one example of each link shape (see table below) | Product, search, category, deals or clearance URLs to scrape (url mode). |
| `brand` | string | (none) | Keep only products whose brand matches this value. |
| `sortBy` | string | `relevance` | `relevance`, `newest`, `nameAsc`, `priceAsc`, `priceDesc`, `ratingAsc`, or `ratingDesc`. |
| `minPrice` | integer | (none) | Only keep products priced at or above this amount (AUD). |
| `maxPrice` | integer | (none) | Only keep products priced at or below this amount (AUD). |
| `minRating` | integer | (none) | Only keep products with an average rating at or above this value (1-5). |
| `includeOutOfStock` | boolean | `true` | Include products not currently available in-store or online. |
| `detailEnrichment` | boolean | `true` | Fetch full product detail, specifications, gallery and reviews (one extra request per product). |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews collected per product when detail enrichment is on (`0` = all available, capped at 1000 per product; values above 1000 are treated as 1000). |
| `maxItems` | integer | `20` | Maximum number of products to return across the whole run (`0` = unlimited). |
| `maxPages` | integer | `0` | Safety cap on result pages walked per keyword / URL (`0` = walk every page). |
| `resumeFromRunId` | string | (none) | ID of a previous run (or dataset) to continue from; already-collected products are skipped. |
| `incrementalMode` | boolean | `false` | Return only new and changed products on scheduled runs. |
| `stateKey` | string | (none) | Name or share an incremental-mode state. Left empty, a key is derived automatically from the search/filter setup. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) unchanged products (incremental mode only). |
| `emitExpired` | boolean | `false` | Also return (and bill) products no longer found (incremental mode only). |
| `baselineDatasetId` | string | (none) | Incremental mode only. Pick the dataset of an earlier run of this Actor as the starting point, so the first incremental run returns only changes. Not combinable with Resume. |
| `earlierHistoryStoreId` | string | (none) | Incremental mode only. Pick the Officeworks Australia Scraper's history store (`officeworks-com-au-incremental-state`) so the first Lite run for a search continues from that history. Read only. |
| `changeScope` | string | `all` | Incremental mode only. `all`, `priceAndStock` (price fields, stock flags, family sizes and colours) or `sellingPriceAndStock` (only `price` among the price fields). Switching keeps the saved baseline. |
| `compactUpdates` | boolean | `false` | Incremental mode only. Short `UPDATED` and `EXPIRED` rows with `priceChanges`, `variantChanges` and `deliveryChanges`; `NEW` rows stay complete. |
| `expireAfterMissedScans` | integer | `1` | Incremental mode only, with `emitExpired`. Complete scans in a row a product must be missing before it is `EXPIRED` (1 to 30). |
| `ignoreFieldsForChanges` | array | `[]` | Extra field names (or dotted paths such as `images.url`) left out of change detection (incremental mode only). They are still returned, just not compared. |
| `deliveryPostcode` | string | (none) | Four-digit postcode for the home delivery check. Required when the check is on. |
| `verifyHomeDelivery` | boolean | `false` | Check live whether each returned product can be delivered to the delivery postcode. |
| `pickupStoreId` | string | (none) | Optional store code (for example `W444`) for the click and collect and in-store quantities. Ignored unless Check home delivery live is on. |
| `proxy` | object | Apify Proxy | Connection settings. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "sku": "SAMPLE0001",
  "name": "Sample A4 Display Book 20 Pocket",
  "title": "Sample Brand A4 Display Book 20 Pocket",
  "brand": "Sample Brand",
  "url": "https://www.officeworks.com.au/shop/officeworks/p/sample-a4-display-book-sample0001",
  "price": 12.5,
  "edlpPrice": 12.5,
  "wasPrice": null,
  "savingsAmount": null,
  "savingsPercent": null,
  "isOnSpecial": true,
  "promoLabel": "Deal",
  "isNew": false,
  "currency": "AUD",
  "gstRate": 10.0,
  "gtin": "9300000012345",
  "manufacturerPartNumber": "SB-DB20",
  "productType": "Display Books",
  "availableStates": ["NSW", "VIC", "QLD"],
  "isAvailableInStore": true,
  "availableOnline": true,
  "isClearance": false,
  "category": "Display Books",
  "categories": ["Office Supplies", "Folders & Filing", "Display Books"],
  "categoryHierarchy": [["Office Supplies", "Folders & Filing", "Display Books"]],
  "variantAxes": { "Colour": ["Blue", "Black"] },
  "variantCount": 2,
  "variants": [
    {
      "sku": "SAMPLE0001",
      "url": "https://www.officeworks.com.au/shop/officeworks/p/sample-a4-display-book-sample0001",
      "attributes": { "Colour": "Blue" },
      "imageUrl": "https://www.officeworks.com.au/images/sample-thumb.jpg"
    }
  ],
  "shortDescription": "A durable display book for everyday filing.",
  "features": ["20 pockets", "A4 size", "Reinforced spine"],
  "specifications": { "Brand": "Sample Brand", "Pockets": "20" },
  "imageUrl": "https://www.officeworks.com.au/images/sample.jpg",
  "images": ["https://www.officeworks.com.au/images/sample.jpg"],
  "rating": 4.3,
  "reviewCount": 42,
  "ratingDistribution": { "5": 20, "4": 12, "3": 6, "2": 2, "1": 2 },
  "recommendedCount": 35,
  "reviews": [
    {
      "reviewId": "SAMPLE-REVIEW-1",
      "rating": 5,
      "title": "Handy for the office",
      "body": "Good quality and holds up well.",
      "author": "SampleReviewer",
      "date": "2026-03-01T09:00:00.000+00:00",
      "helpfulCount": 2
    }
  ],
  "reviewsCollected": 1
}
```

### Plan Requirement

The default proxy setting works out of the box on datacenter connections. Officeworks occasionally refuses a single request from one exit (a plain "request blocked" page); the connection rotates to a fresh exit and retries automatically, and the same pages load on the retry. Residential gives more headroom for large or frequent runs; pick it under Connection. If no page can be read at all, the run fails with a clear message instead of an empty dataset: a refused proxy connection for the account points to a plan without proxy access, and otherwise the message says the requests failed and to try again later.

### FAQ

#### How much does it cost?

You pay one product event per returned dataset row and one run-start event per run, plus Apify platform usage for compute, proxies, storage and data transfer. The Pricing tab shows the current rates for your plan. This Lite version has no detail enrichment or delivery enrichment event fees. Product pages, reviews and the home delivery check still consume platform resources. Use **Max products**, **Max pages**, **Max reviews per product** and the platform run limits to bound work. If you also set a maximum cost per run, the scraper stops as soon as that budget is used up and keeps every product already saved. A delivery lookup for a SKU the site does not know is treated as not read. The first readable delivery answer of an already tracked product (also when you turn the check on for a tracked search, or change the postcode) returns that product once, as one product row. A product whose states with stock or sizes/colours only changed order is no longer returned.

#### Is it legal to scrape Officeworks?

This actor collects only publicly available product data. You are responsible for how you use it: follow Officeworks' terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Prices and specifications are generally facts, but product photos and review text may be subject to third-party rights.

#### Can I get only new or changed products on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared products, and unchanged ones are suppressed by default.

#### Why did my run return fewer products than Max products, or a very large batch when Emit expired is on?

Max products stops a run once that many products have been returned; a capped page or a capped run does not go back for more. A run with several keywords or links can also stop a later one early when its first page overlaps products already handled, and an incremental run spends slots on unchanged products it then suppresses; see **Multiple keywords and specials in one run** and **How Max products and Max pages shape a run**. Separately, `emitExpired` is not limited by Max products: once a run fully scans the tracked search, every previously-tracked product no longer found is returned as `EXPIRED` in one batch, which can be larger than your usual Max products if the tracked search shrank a lot since the last run. See the tips under **Resume and recurring updates**.

#### Why does a product I resumed show up as UPDATED on the very next incremental run, even though nothing changed?

This happens exactly once, only when `resumeFromRunId` is combined with a brand-new `incrementalMode` campaign (no prior state for that `stateKey`). The resumed products are added to the new baseline without a real fingerprint yet, so the first comparison against them reports every field as changed. It does not repeat on later runs. See the tips under **Resume and recurring updates**.

#### Why did my run fail instead of returning an empty dataset?

If the connection is refused before any data could be read, the run stops with a clear message so "no products found" is never confused with "nothing could be read". Run it again in a few minutes. Separately, starting **Incremental mode** with **Resume** when that state key already has tracked products also fails on purpose, with a message telling you to drop Resume or pick a different **State key**, rather than silently mixing the two.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more Officeworks data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/officeworks-scraper"><b>Officeworks Scraper</b></a><br>Scrape Officeworks (officeworks.com.au) products: name, brand, title, price, GST...</td><td>🛒 <a href="https://apify.com/abotapi/myer-au-scraper"><b>Myer Scraper</b></a><br>Scrape Myer (myer.com.au) department-store products: name, brand, price, was-price...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/bigw-marketplace-scraper"><b>BIG W Marketplace Scraper</b></a><br>Scrape BIG W Marketplace seller listings from bigw.com.au: name, brand, price, was-price...</td><td>🛒 <a href="https://apify.com/abotapi/harvey-norman-scraper"><b>Harvey Norman Scraper</b></a><br>Scrape Harvey Norman Australia products: name, brand, price, was-price / discount...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/amazon-au-scraper"><b>Amazon Australia Product &amp; Reviews Scraper</b></a><br>Scrape Amazon Australia (amazon.com.au) products and customer reviews. Search by keyword...</td><td>🛒 <a href="https://apify.com/abotapi/costco-au-scraper"><b>Costco AU Scraper</b></a><br>Scrape Costco.com.au products by keyword or product, search and category URLs. Extract...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/officeworks-scraper-lite/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Choose 'search' to use keywords and filters, or 'url' to scrape specific product, search or category URLs you paste below.

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

One or more keywords to search, for example 'laptop' or 'printer ink'. Each keyword is searched separately.

## `specialsCategory` (type: `string`):

Optional. Scrape one of Officeworks' promotional collections instead of (or alongside) your keywords: Deals (curated featured offers) or Clearance (reduced-to-clear stock). Brand, price, rating filters and sort still apply. Leave empty to skip. Note: Officeworks uses an every-day-low-price model, so most products do not carry a strike-through 'was' price.

## `urls` (type: `array`):

Product, search or category URLs to scrape, for example https://www.officeworks.com.au/shop/officeworks/p/some-product-sku, https://www.officeworks.com.au/shop/officeworks/search?q=laptop or https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens. Multiple URLs supported.

## `brand` (type: `string`):

Optional. Keep only products whose brand matches this value, for example 'HP' or 'Keji'.

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

Ordering applied to the results by the store's product-search service.

## `minPrice` (type: `integer`):

Optional. Only keep products priced at or above this amount (AUD).

## `maxPrice` (type: `integer`):

Optional. Only keep products priced at or below this amount (AUD).

## `minRating` (type: `integer`):

Optional. Only keep products with an average customer rating at or above this value (1 to 5).

## `includeOutOfStock` (type: `boolean`):

Include products that are not currently available in-store or online. On by default; turn off to keep only available products.

## `detailEnrichment` (type: `boolean`):

For each product, also fetch the product page for the full description, feature bullets, specifications, identifiers and image gallery, plus the star-rating distribution and customer reviews. Adds one request per product, which consumes additional platform usage; there is no enrichment event fee. Turn off for a faster, lower-cost run that returns the listing card fields only (name, price, brand, aggregate rating and review count) with no reviews.

## `maxReviewsPerProduct` (type: `integer`):

Cap on reviews collected per product when detail enrichment is on. Use 0 for all available reviews, up to a hard cap of 1000 per product. Products with no reviews leave the review fields absent.

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

Maximum number of products to return across the whole run. Use 0 for unlimited.

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

Optional safety cap on result pages walked per keyword / URL. A pasted URL that names its own start page counts from that page (maxPages=2 on a page=5 URL walks pages 5-6). A resumed run counts from page 1 of the original run, so the cap is cumulative across resumes, not reset per run. Use 0 to walk every page (the run still stops automatically once Max products is reached, or once a page repeats or the store's own result total is reached).

## `deliveryPostcode` (type: `string`):

Four-digit Australian postcode to check home delivery to, for example 4209. Needed when Check home delivery is on. Leave empty otherwise.

## `verifyHomeDelivery` (type: `boolean`):

For every returned product, ask the store whether it can be delivered to the delivery postcode right now, and add the delivery type, quantity, estimated arrival dates, click and collect and in-store quantities, and the postcode's delivery zone. The listing fields availableStates / availableOnline are index flags, not a delivery answer. A product whose answer cannot be read gets empty (null) delivery fields, never 'unavailable'. These lookups consume additional platform usage; there is no enrichment event fee. Requires a delivery postcode.

## `pickupStoreId` (type: `string`):

Optional store code, for example W444, whose click and collect and in-store quantities are reported. Ignored unless Check home delivery live is on. Leave empty to use the store nearest the delivery postcode. Delivery type, quantity and estimated arrival do not depend on the store.

## `proxy` (type: `object`):

Apify Proxy is recommended for reliable results. On the free plan, coverage may be limited and some runs may return no results.

## `resumeFromRunId` (type: `string`):

Optional. ID of a previous run of this actor (or a dataset ID). Products already in that dataset are skipped, so this run returns only NEW products (a delta). Combine both runs' datasets for the full set. Max products then counts only the new products. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. UPDATED is driven by the product's own data (price, specials and promo label, stock and availability, name, brand, category, images and the other catalogue fields); rating, reviewCount, ratingDistribution, recommendedCount, reviewsCollected and reviews are excluded and never cause UPDATED. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned. State is kept separately for each distinct search/filter setup (or by State key below).

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

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from your search/filter/detail settings (switching Detail enrichment continues from the same search's saved baseline) — different searches then never mix state with each other.

## `baselineDatasetId` (type: `string`):

Optional. Pick the dataset of an earlier run of this Actor (same search and settings) to use as your starting point, so your first incremental run returns only products that changed since then instead of the whole search again. Pick the dataset unedited: it must be this Actor's own product output. Only used on the first run for a state key; ignored once that state key already tracks products. Cannot be combined with Resume from a previous run. If the dataset is old, everything that changed since is returned as UPDATED or NEW.

## `earlierHistoryStoreId` (type: `string`):

Optional. Pick the saved-history store of the Officeworks Australia Scraper (named officeworks-com-au-incremental-state) so your first Lite run for a State key, or for the same search settings, continues from the history that scraper already tracked, instead of returning the whole search as NEW. With an automatic key, the history saved under either Detail enrichment setting of the same search is used. The store is only read, never changed. Only used on the first run for a state key; ignored once Lite already tracks products for that key (or, with an automatic key, for the same search under the other Detail enrichment setting), or when Start from an existing dataset or Resume is set.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max products or Max pages capped it, or when Resume was used. This returns extra synthetic rows.

## `changeScope` (type: `string`):

All fields (default): any change to the product record, except the always-ignored review fields, returns an UPDATED row. Price and availability only: only price changes (price, everyday price, was price, savings, special and clearance flags), stock changes (available online, states with stock, in-store availability) and a size or colour added to or removed from the product family return an UPDATED row; descriptions, images, specifications and other details never do. Selling price and availability only: like Price and availability only, but a price change counts only when the selling price (price) moves; the everyday price, was price, savings and special flags never return a row on their own. Every size or colour is its own product at Officeworks, so its own price and stock are compared on its own row. With Check home delivery live on, a change in whether the product can be delivered to your postcode returns an UPDATED row in every option. Changing this option keeps the saved baseline of your search: it never returns the whole search as NEW again.

## `compactUpdates` (type: `boolean`):

Off by default. When on, UPDATED rows (and EXPIRED rows) are short: sku, url, name, brand, current price and stock, previousPrice, plus priceChanges (old and new value of each moved price or stock field), variantChanges (a size or colour added to or removed from the family) and, with Check home delivery live on, the delivery answer and deliveryChanges. NEW rows always carry the complete product record. Compact rows carry no product-page content (description, specifications, images, reviews).

## `expireAfterMissedScans` (type: `integer`):

Only used with Emit expired products. A product is marked EXPIRED only after it has been missing from this many full scans in a row (default 1). Raise it (e.g. 3) so a product that briefly drops out of a search or category listing is not reported as gone. A product that comes back resets the count. Stock and delivery status are reported through the availability and delivery fields, not through EXPIRED.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names to leave out of UPDATED change detection, on top of the built-in handling (rating, reviewCount, ratingDistribution, recommendedCount, reviewsCollected and reviews never trigger UPDATED, since other customers change them without the product changing). Use a top-level name such as colour, or a dotted path such as images.url to ignore a nested field. Ignored fields are still returned, just not compared.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "laptop"
  ],
  "urls": [
    "https://www.officeworks.com.au/shop/officeworks/search?q=laptop",
    "https://www.officeworks.com.au/shop/officeworks/search?q=laptop&page=2",
    "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens",
    "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens?page=2",
    "https://www.officeworks.com.au/shop/officeworks/c/art-supplies",
    "https://www.officeworks.com.au/shop/officeworks/p/mac-mini-m6-12-core-cpu-12-core-gpu-16-256gb-immm6162",
    "https://www.officeworks.com.au/shop/officeworks/search?q=IMMM6162",
    "https://www.officeworks.com.au/shop/officeworks/c/deals",
    "https://www.officeworks.com.au/shop/officeworks/c/clearance",
    "https://www.officeworks.com.au/shop/officeworks/p/JBCNCPA480"
  ],
  "sortBy": "relevance",
  "includeOutOfStock": true,
  "detailEnrichment": true,
  "maxReviewsPerProduct": 20,
  "maxItems": 20,
  "maxPages": 0,
  "verifyHomeDelivery": false,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No 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 = {
    "mode": "search",
    "queries": [
        "laptop"
    ],
    "urls": [
        "https://www.officeworks.com.au/shop/officeworks/search?q=laptop",
        "https://www.officeworks.com.au/shop/officeworks/search?q=laptop&page=2",
        "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens",
        "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens?page=2",
        "https://www.officeworks.com.au/shop/officeworks/c/art-supplies",
        "https://www.officeworks.com.au/shop/officeworks/p/mac-mini-m6-12-core-cpu-12-core-gpu-16-256gb-immm6162",
        "https://www.officeworks.com.au/shop/officeworks/search?q=IMMM6162",
        "https://www.officeworks.com.au/shop/officeworks/c/deals",
        "https://www.officeworks.com.au/shop/officeworks/c/clearance",
        "https://www.officeworks.com.au/shop/officeworks/p/JBCNCPA480"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "changeScope": "all",
    "compactUpdates": false,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/officeworks-scraper-lite").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 = {
    "mode": "search",
    "queries": ["laptop"],
    "urls": [
        "https://www.officeworks.com.au/shop/officeworks/search?q=laptop",
        "https://www.officeworks.com.au/shop/officeworks/search?q=laptop&page=2",
        "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens",
        "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens?page=2",
        "https://www.officeworks.com.au/shop/officeworks/c/art-supplies",
        "https://www.officeworks.com.au/shop/officeworks/p/mac-mini-m6-12-core-cpu-12-core-gpu-16-256gb-immm6162",
        "https://www.officeworks.com.au/shop/officeworks/search?q=IMMM6162",
        "https://www.officeworks.com.au/shop/officeworks/c/deals",
        "https://www.officeworks.com.au/shop/officeworks/c/clearance",
        "https://www.officeworks.com.au/shop/officeworks/p/JBCNCPA480",
    ],
    "proxy": { "useApifyProxy": True },
    "changeScope": "all",
    "compactUpdates": False,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/officeworks-scraper-lite").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 '{
  "mode": "search",
  "queries": [
    "laptop"
  ],
  "urls": [
    "https://www.officeworks.com.au/shop/officeworks/search?q=laptop",
    "https://www.officeworks.com.au/shop/officeworks/search?q=laptop&page=2",
    "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens",
    "https://www.officeworks.com.au/shop/officeworks/c/office-supplies/pens/ballpoint-pens?page=2",
    "https://www.officeworks.com.au/shop/officeworks/c/art-supplies",
    "https://www.officeworks.com.au/shop/officeworks/p/mac-mini-m6-12-core-cpu-12-core-gpu-16-256gb-immm6162",
    "https://www.officeworks.com.au/shop/officeworks/search?q=IMMM6162",
    "https://www.officeworks.com.au/shop/officeworks/c/deals",
    "https://www.officeworks.com.au/shop/officeworks/c/clearance",
    "https://www.officeworks.com.au/shop/officeworks/p/JBCNCPA480"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/officeworks-scraper-lite --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/officeworks-scraper-lite"
        }
    }
}
```

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/FjeNDEdU4pUXNmWOh/builds/6iswGqnqJICcZqeWw/openapi.json
