# Detect Price & Stock Changes | Dataset Diff (`panda_studio/dataset-diff-change-detector`) Actor

Spot price drops, stock changes, and catalog updates with Dataset Diff. Compare two JSON arrays or Apify datasets for field-level changes, before/after values, and numeric deltas. No scraping.

- **URL**: https://apify.com/panda\_studio/dataset-diff-change-detector.md
- **Developed by:** [panda studio](https://apify.com/panda_studio) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

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

## Detect Price & Stock Changes | Dataset Diff

**Spot price drops, stock changes, and catalog updates** — compare two dataset snapshots and get exactly what changed as clean, field-level JSON. Point it at yesterday's scrape and today's scrape and it tells you which records were **added**, **modified** (with before / after values and numeric deltas), **missing**, and **removed**. Ideal for recurring e-commerce price monitoring and change detection — and it processes only user-supplied data, with **no scraping**.

Built for recurring monitoring: e-commerce price & availability tracking, catalog audits, lead-list change detection, and any workflow where you run a scraper on a schedule and only care about *what moved*.

- **No scraping, no browser, no anti-bot games.** It only processes data **you** supply — inline JSON, or two Apify datasets your own token can read. Zero third-party Terms-of-Service risk.
- **Dependency-free & fast.** Pure Python standard library. Typical runs finish in seconds.
- **Truthful by default.** It won't call a record "removed" from a half-finished scrape — see *Missing vs. removed* below.

***

### What you get

Every run writes one **summary** row plus one **change** row per affected record:

```jsonc
// summary row
{ "recordType": "summary", "oldCount": 1200, "newCount": 1198,
  "added": 5, "modified": 41, "missing": 0, "removed": 7,
  "unchanged": 1152, "evaluatedKeys": 1205, "errors": [] }

// a modified record
{ "recordType": "change", "changeType": "modified", "key": "sku-1",
  "changes": [
    { "field": "price", "before": 19.99, "after": 17.99, "numericDelta": -2.0,
      "beforePresent": true, "afterPresent": true }
  ] }

// an added record (full record included)
{ "recordType": "change", "changeType": "added", "key": "sku-4",
  "record": { "id": "sku-4", "title": "Mechanical Keyboard", "price": 79.0 } }
```

***

### Inputs

| Field | Type | Default | What it does |
|---|---|---|---|
| `oldDatasetId` | string | — | Apify dataset ID of the **previous** snapshot. Your token must read it. |
| `newDatasetId` | string | — | Apify dataset ID of the **current** snapshot. |
| `oldItems` | array | — | Previous snapshot as inline JSON (used when `oldDatasetId` is empty). |
| `newItems` | array | — | Current snapshot as inline JSON. |
| `keyFields` | array | `["id"]` | Field(s) that identify a record across snapshots. Use several for a composite key (e.g. `sku` + `variant`). |
| `compareFields` | array | `[]` | Only check these fields. Empty = compare **all** fields except keys and ignored fields. |
| `ignoreFields` | array | `["scrapedAt","fetchedAt","crawledAt","#debug"]` | Volatile fields to skip so they don't create noise. |
| `numericFields` | array | `["price"]` | Fields compared as numbers; a `numericDelta` is computed and the tolerance applies. Currency symbols and thousands separators are tolerated (`"$1,299.00"` → `1299.0`). |
| `numericTolerance` | integer | `0` | Ignore numeric changes at or below this absolute difference. |
| `snapshotsComplete` | boolean | `false` | See *Missing vs. removed*. |
| `includeUnchanged` | boolean | `false` | Also emit a row for records that didn't change. |
| `maxRowsPerSnapshot` | integer | `50000` | Safety cap; the run errors out instead of silently truncating. |

> **Run it with no input** and it compares a small built-in demo snapshot, so you can see the exact output shape before wiring up your own data.

### Outputs

| Field | Present on | Meaning |
|---|---|---|
| `recordType` | all | `summary` or `change`. |
| `changeType` | change rows | `added` / `modified` / `missing` / `removed` / `unchanged`. |
| `key` | change rows | The composite key of the record. |
| `changes` | modified rows | Array of `{field, before, after, numericDelta, beforePresent, afterPresent}`. |
| `record` | added / missing / removed rows | The full record. |
| `oldCount`,`newCount`,`added`,`modified`,`missing`,`removed`,`unchanged`,`evaluatedKeys` | summary | Counts. |
| `errors` | summary | Non-fatal issues (duplicate keys, missing key fields, skipped non-objects). |

***

### Missing vs. removed — why it matters

If a record is in the old snapshot but not the new one, that can mean two very different things:

- The product was genuinely **removed**, or
- Your new scrape was **incomplete** (a timeout, a blocked page, pagination that stopped early).

Treating an incomplete scrape as mass deletions is how monitoring pipelines send false "everything is gone" alerts. So by default this Actor labels those records **`missing`** (a soft signal). Set **`snapshotsComplete: true`** only when you are confident both snapshots are complete crawls — then they are labelled **`removed`**.

***

### Example: daily price monitoring

1. Schedule your product scraper to run every morning; each run produces a dataset.
2. Schedule this Actor right after, with:
   - `oldDatasetId` = yesterday's dataset ID
   - `newDatasetId` = today's dataset ID
   - `keyFields` = `["sku"]`, `numericFields` = `["price"]`, `numericTolerance` = `0`
3. Wire the output to a webhook / integration. Every `modified` row with a `price` change and a negative `numericDelta` is a price drop worth acting on.

### Notes

- Matching is by exact key equality; pick keys that are stable across runs (an internal `id` or `sku`, not a position or a timestamp).
- Records with duplicate keys keep the last occurrence and are reported in `errors`.
- The Actor reads and writes datasets only through the documented Apify REST API and never logs your input values.

# Actor input Schema

## `oldDatasetId` (type: `string`):

Apify dataset ID of the PREVIOUS snapshot (e.g. yesterday's scrape). Your token must be able to read it. Leave empty to use the inline 'Old items' below.

## `newDatasetId` (type: `string`):

Apify dataset ID of the CURRENT snapshot (e.g. today's scrape). Leave empty to use the inline 'New items' below.

## `oldItems` (type: `array`):

Previous snapshot as an inline JSON array of objects. Ignored when 'Old dataset ID' is set. If you provide neither datasets nor inline items, a built-in demo snapshot is used so the run always returns a result.

## `newItems` (type: `array`):

Current snapshot as an inline JSON array of objects. Ignored when 'New dataset ID' is set.

## `keyFields` (type: `array`):

Field name(s) that uniquely identify a record across snapshots (e.g. <code>id</code>, or <code>sku</code>+<code>variant</code> for a composite key). Records are matched by these.

## `compareFields` (type: `array`):

Only these fields are checked for changes. Leave empty to compare ALL fields except the key fields and the ignored fields.

## `ignoreFields` (type: `array`):

Volatile fields to exclude from comparison so they don't create noise (timestamps, debug metadata, etc.).

## `numericFields` (type: `array`):

Fields treated as numbers so a delta is computed and the tolerance below applies (e.g. <code>price</code>, <code>stock</code>). Currency symbols and thousands separators are tolerated.

## `numericTolerance` (type: `integer`):

Ignore numeric changes whose absolute difference is at or below this value (e.g. 1 hides a price change of 1 or less). 0 reports every change.

## `snapshotsComplete` (type: `boolean`):

Enable ONLY if both snapshots are guaranteed complete crawls. When on, records present in the old snapshot but absent from the new are reported as 'removed'. When off (safer), they are reported as 'missing' to avoid mistaking a partial scrape for deletions.

## `includeUnchanged` (type: `boolean`):

Also emit a row for every record that did not change. Off by default to keep the output focused on changes.

## `maxRowsPerSnapshot` (type: `integer`):

Safety cap. If either snapshot exceeds this, the run stops with an error instead of silently truncating.

## Actor input object example

```json
{
  "keyFields": [
    "id"
  ],
  "compareFields": [],
  "ignoreFields": [
    "scrapedAt",
    "fetchedAt",
    "crawledAt",
    "#debug"
  ],
  "numericFields": [
    "price"
  ],
  "numericTolerance": 0,
  "snapshotsComplete": false,
  "includeUnchanged": false,
  "maxRowsPerSnapshot": 50000
}
```

# Actor output Schema

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

All change and summary rows produced by this run, stored in the default dataset.

# 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 = {
    "keyFields": [
        "id"
    ],
    "ignoreFields": [
        "scrapedAt",
        "fetchedAt",
        "crawledAt",
        "#debug"
    ],
    "numericFields": [
        "price"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("panda_studio/dataset-diff-change-detector").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 = {
    "keyFields": ["id"],
    "ignoreFields": [
        "scrapedAt",
        "fetchedAt",
        "crawledAt",
        "#debug",
    ],
    "numericFields": ["price"],
}

# Run the Actor and wait for it to finish
run = client.actor("panda_studio/dataset-diff-change-detector").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 '{
  "keyFields": [
    "id"
  ],
  "ignoreFields": [
    "scrapedAt",
    "fetchedAt",
    "crawledAt",
    "#debug"
  ],
  "numericFields": [
    "price"
  ]
}' |
apify call panda_studio/dataset-diff-change-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,panda_studio/dataset-diff-change-detector"
        }
    }
}
```

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/X4IY57cvtRi7DNfI4/builds/NF2LF4KweicK2hYUa/openapi.json
