# Dataset Diff: See What Changed & Why (`hiredhandai/dataset-diff-explainer`) Actor

Compare two datasets by key and get only what changed: new, removed and changed records with before/after values, numeric deltas and alert rules. Pure compute, no scraping.

- **URL**: https://apify.com/hiredhandai/dataset-diff-explainer.md
- **Developed by:** [Logan Bishop](https://apify.com/hiredhandai) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 reported changes

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?

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

## Dataset Diff: See What Changed & Why

**Compare two Apify datasets (or today's data against the last saved snapshot) by key, and get back only what changed: new records, removed records and changed records, each explained in plain English.**

Re-running a scraper every day gives you two big datasets and a diff that is mostly noise: whitespace, timestamps, `19.99` versus `"19.99"`, reordered lists. This Actor ignores that noise, tells you what it ignored, and reports the real changes as ready-to-filter records such as `price decreased 20.0% (19.99 -> 15.99)`. It never scrapes anything, and it keeps nothing after the run except a snapshot in your own key-value store, if you ask for one.

### What you get

- **Only the real changes.** New, removed and changed records, with before/after values, numeric deltas (`delta`, `deltaPct`, `direction`), text similarity and list additions/removals.
- **Noise control with receipts.** Whitespace, case (optional), numeric strings (`"$1,299.00"` equals `1299`), ISO dates, empty-versus-missing, reordered lists, timestamps and run metadata (`scrapedAt`, `crawledAt`, `#debug`, ...) are normalized or ignored. The run summary counts what was suppressed and lists the fields it ignored.
- **Alert rules.** Say what matters ("price dropped 10% or more", "stock became 0", "title changed") and matching records get `alerts[]` and a `severity` you can filter on in Zapier, Make, n8n or a Slack integration.
- **A key you do not have to guess.** Leave the key empty and the Actor picks one (and says why), or stops with an explanation. URL keys ignore `http`/`https`, `www`, tracking parameters, trailing slashes and parameter order. Composite keys, duplicate and missing-key policies are supported.
- **Snapshot mode.** Give a snapshot name and the Actor compares today's data with the last run and saves today's data as the new snapshot. The first run only creates the snapshot and is free.
- **A free report.** `REPORT.html` and `REPORT.md` in the key-value store: counts, top alerts, biggest numeric movers, what was ignored.

### How to use it

1. **Pick your two datasets** (current and baseline), or give a **snapshot name** and one current dataset.
2. **Set the key** (for example `url` or `sku`), or leave it empty for auto-detection.
3. **Run it.** Read the changes from the Dataset tab, or connect the run to a Slack, Zapier, Make or n8n integration and filter on `severity` or `alerts`.

Prefilled example input (try it as is):

```json
{
  "keyFields": ["sku"],
  "currentRecords": [
    { "sku": "A-100", "title": "Acme Mug", "price": 15.99, "stock": 0, "tags": ["new", "sale"], "scrapedAt": "2026-10-24T08:00:00Z" },
    { "sku": "B-200", "title": "Blue Pen", "price": 2.5, "stock": 40, "tags": [], "scrapedAt": "2026-10-24T08:00:00Z" },
    { "sku": "D-400", "title": "Desk Lamp", "price": 30, "stock": 7, "tags": ["eco"], "scrapedAt": "2026-10-24T08:00:00Z" }
  ],
  "baselineRecords": [
    { "sku": "A-100", "title": "Acme Mug", "price": 19.99, "stock": 12, "tags": ["new"], "scrapedAt": "2026-10-23T08:00:00Z" },
    { "sku": "B-200", "title": "Blue Pen", "price": 2.5, "stock": 40, "tags": [], "scrapedAt": "2026-10-23T08:00:00Z" },
    { "sku": "C-300", "title": "Cable", "price": 5, "stock": 9, "tags": [], "scrapedAt": "2026-10-23T08:00:00Z" }
  ],
  "rules": [
    { "id": "price-drop-10", "field": "price", "when": "decreased", "minPercent": 10, "severity": "high", "label": "Price dropped 10%+" },
    { "id": "oos", "field": "stock", "when": "became", "value": 0, "severity": "high" }
  ]
}
```

That run returns three items (a new record, a removed record and one changed record). The changed one, from a real local run:

```json
{
  "changeType": "changed",
  "key": "[\"a-100\"]",
  "keyParts": { "sku": "A-100" },
  "matchedBy": "key",
  "matchConfidence": 1,
  "severity": "high",
  "summary": "price decreased 20.0% (19.99 -> 15.99); stock 12 -> 0; tags: +1 (sale)",
  "alerts": [
    { "rule": "price-drop-10", "field": "price", "severity": "high", "message": "Price dropped 10%+: price decreased 20.0% (19.99 -> 15.99)" },
    { "rule": "oos", "field": "stock", "severity": "high", "message": "stock became 0 (was 12)" }
  ],
  "changedFieldNames": ["price", "stock", "tags"],
  "fieldChanges": [
    { "field": "price", "op": "changed", "type": "number", "from": 19.99, "to": 15.99, "delta": -4, "deltaPct": -20.01, "direction": "decreased", "explanation": "price decreased 20.0% (19.99 -> 15.99)" },
    { "field": "stock", "op": "changed", "type": "number", "from": 12, "to": 0, "delta": -12, "deltaPct": -100, "direction": "decreased", "explanation": "stock 12 -> 0" },
    { "field": "tags", "op": "changed", "type": "set", "added": ["sale"], "removed": [], "explanation": "tags: +1 (sale)" }
  ],
  "before": { "price": 19.99, "stock": 12, "tags": ["new"] },
  "after": { "price": 15.99, "stock": 0, "tags": ["new", "sale"] },
  "baselineIndex": 0,
  "currentIndex": 0
}
```

Note that `scrapedAt` differs on every record and produced no change: it is on the built-in ignore list.

### Monitor a scraper every day (snapshot mode)

1. Run your scraper on a schedule.
2. Schedule this Actor right after it with `currentDatasetId` set to the scraper's dataset (in an integration or through the API) and a `snapshotName` such as `daily-prices`.
3. The first run saves the snapshot and returns nothing. Every later run returns only what changed since the previous run, then rolls the snapshot forward.

Snapshots are stored in a key-value store named `dataset-diff-snapshots` in your own account. You can delete the store at any time. A run that stops early (for example at your maximum cost per run) does not update the snapshot, so no change is lost. `updateSnapshot: false` gives a dry run.

To stop a failed scrape from showing every record as removed, set `abortIfShrinksPercent` (for example `50`). Without it you still get a warning when the new dataset has fewer than half the records of the old one.

### Chain it after other Actors

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

// 1. Run your scraper (replace with the Actor and input you use)
const scraperRun = await client.actor('<your-scraper>').call({ /* scraper input */ });

// 2. Diff its dataset against the last snapshot
const diffRun = await client.actor('hiredhandai/dataset-diff-explainer').call({
  currentDatasetId: scraperRun.defaultDatasetId,
  snapshotName: 'competitor-prices',
  keyFields: ['url'],
  rules: [{ field: 'price', when: 'decreased', minPercent: 10, severity: 'high' }],
});

// 3. Read only the high-severity changes
const { items } = await client.dataset(diffRun.defaultDatasetId).listItems();
console.log(items.filter((c) => c.severity === 'high'));
```

It works with any Actor that writes records to a dataset. Put `hiredhandai/lead-data-cleaner` in front of it for lead lists, so that cleaning differences do not show up as changes.

### Input

| Field | What it does |
|---|---|
| `currentDatasetId` / `currentRecords` | The newer data: an Apify dataset, or up to 10,000 inline records. |
| `baselineDatasetId` / `baselineRecords` / `snapshotName` | The older data. Use exactly one of the three. |
| `updateSnapshot` | Save today's data as the new snapshot after a complete run (default on). |
| `keyFields` | Field(s) that identify a record. Empty = auto-detect, with the reason in the summary. |
| `keyNormalization`, `keyCaseSensitive`, `fallbackKeyFields` | How keys are compared, and a backup key for records missing a part. |
| `duplicateKeyPolicy`, `missingKeyPolicy` | What to do with repeated keys and with records that have no key. |
| `fuzzyMatch` | Optional re-pairing of a removed and a new record with almost the same text. |
| `compareFields`, `ignoreFields`, `ignoreVolatileFields` | Which fields count. Wildcards `*` and `**` are supported. |
| `arrayKeys`, `arrayMode` | Match list items by a key (`variants[sku=B2].stock`), or compare lists as sets or in order. |
| `normalize`, `numericTolerance` | Noise control: trimming, case, numeric strings, dates, tiny numeric movements. |
| `rules`, `watchFields` | Alert rules and the fields listed first in each summary. |
| `emit`, `alertsOnly`, `outputMode`, `includeRecords`, `includeSuppressed` | What is returned (and charged), and in which shape. |
| `abortIfShrinksPercent`, `strategy`, `maxRecordsPerSide`, `maxChangesReported` | Safety limits and memory strategy. |

Rule `when` values: `changed`, `increased`, `decreased`, `became` (with `value`), `became_empty`, `became_nonempty`, `added`, `removed`, `matches` (with `pattern`), `crossed` (with `threshold` and `direction`). `minPercent` and `minAbsolute` apply to numeric changes. Use the field `$record` with `added` or `removed` to flag new and disappeared records.

### Output

- **Dataset:** one item per added, removed or changed record, as shown above. With `outputMode: "fieldChanges"` you get one item per changed field, which is easier to put in a spreadsheet.
- **`OUTPUT` (key-value store):** status, counts, the key and why it was chosen, top changed fields, numeric movement, what was suppressed and ignored, the strategy used, warnings, and the charges of the run.
- **`REPORT.html` and `REPORT.md`:** the same in a human-readable form.

### How it decides

- **Keys.** Key values are compared after normalization: text keys ignore case and spacing; URL keys ignore scheme, `www`, tracking parameters, fragments, trailing slashes and parameter order. The number `123` and the text `"123"` are the same key; `"007"` stays `"007"`. Composite keys are collision-safe.
- **Auto-detect.** Candidates are fields present in at least 99% of records on both sides. Common identifier names (`url`, `id`, `sku`, `asin`, `slug`, ...) are preferred, and a candidate needs a score of 0.98 or more (presence times uniqueness). Fields on the built-in ignore list (`scrapedAt`, `timestamp`, ...) are never chosen. If nothing qualifies, the run stops and lists the best candidates instead of guessing.
- **Values.** Text is trimmed and whitespace-collapsed, NFKC-normalized and repaired when it was garbled by encoding errors (`CafÃ©`). Numeric strings with currency symbols, thousands separators and percent signs are read as numbers; `"1,234"` is 1234 and `"12,5"` is 12.5; ids with leading zeros are left alone. ISO dates are compared as instants; a date-only value never equals a date-time value. `null`, empty text, `[]`, `{}` and missing are equal by default.
- **Tolerance.** `numericTolerance` ignores movements below an absolute and/or relative size, per field if you like. The summary counts how many were ignored.
- **Lists.** By default lists are compared as sets, so reordering is not a change. With `arrayKeys`, items are matched by key and reported as `variants[sku=B2].stock`.
- **Renamed records.** With `fuzzyMatch`, a removed and a new record are paired only if their text similarity clears your threshold, they are each other's best match, they beat every runner-up by a margin, and all numbers inside the text are identical (`Store 12` is never paired with `Store 21`).
- **Large datasets.** Small datasets are compared in memory. Larger ones use a multi-pass method that keeps one short fingerprint per record, so memory stays flat. Both methods return identical results (checked on 200 random datasets in the test suite). On a development machine, 100,000 against 100,000 records took about 9 to 12 seconds with the low-memory method. Timings on the platform depend on the memory you give the run.
- **Limits.** 500,000 records per dataset by default (`maxRecordsPerSide`), 100,000 reported changes per run (`maxChangesReported`), records larger than 100,000 characters are skipped and counted (their key is excluded from both sides, so they never appear as fake additions or removals). No Excel, CSV or Google Sheets input: use a dataset or inline JSON.

### Pricing

Pay per event. Prices:

| Event | Price |
|---|---|
| Actor start | $0.00005 (Apify default) |
| Compared record (each record in the current dataset) | $0.0001 ($0.10 per 1,000) |
| Reported change (each added, removed or changed record returned) | $0.002 ($2.00 per 1,000) |

Worked examples per 1,000 records compared: 1% churn costs about $0.12, 5% churn about $0.20, 20% churn about $0.50, and if every record changed $2.10. The first run of a snapshot compares nothing and is free. Changes you filter out with `emit` or `alertsOnly` are not returned and not charged. The report is free.

Your **maximum cost per run** is respected: the Actor stops before spending more, returns exactly what it could afford, says so in the run status, and does not update a snapshot after a partial run. If the limit cannot even cover comparing the current dataset, the run stops at the start and charges nothing.

### FAQ

**Does it scrape anything?** No. It only reads the datasets you point it at.

**Does it store my data?** No. The only thing that persists is a snapshot, in your own key-value store named `dataset-diff-snapshots`, and only if you use a snapshot name.

**Why was a record I expected not reported?** Most likely it differed only by noise: whitespace, case (if `caseInsensitive`), a numeric movement within `numericTolerance`, list order, or an ignored field. Check the `suppressed` block and `ignoredFields` in `OUTPUT`, and use `includeSuppressed` for notes on changed records.

**The first run returned nothing.** In snapshot mode the first run only saves the snapshot. Run it again later.

**How do I reset a snapshot?** Delete the records starting with `MANIFEST-<name>` and `SNAP-<name>-` from the `dataset-diff-snapshots` store, or use a new name.

**What if the key changed for some records?** Turn on `fuzzyMatch` to pair a removed and a new record by similar text, or choose a more stable key.

### Support

Found a bug or a case that is not handled? Open an issue from the Actor page; issues get a reply within 48 hours. See CHANGELOG.md for what changed in each version. If the Actor saves you time, an honest rating on the Actor page helps others find it.

Built by hiredhandai. Related Actors by hiredhandai: [Lead Data Cleaner, Deduper & Scorer](https://apify.com/hiredhandai/lead-data-cleaner).

# Changelog

This Actor's version history is a separate document: https://apify.com/hiredhandai/dataset-diff-explainer/changelog.md

# Actor input Schema

## `currentDatasetId` (type: `string`):

The newer dataset, for example the output of today's scraper run. Pick it from your datasets, or pass its ID through the API. Leave empty if you paste records below.

## `currentRecords` (type: `array`):

Up to 10,000 records as a JSON array. Handy for trying the Actor or for small API calls. Use the dataset field for bigger jobs.

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

The older dataset to compare against, for example yesterday's run. Use exactly one of: baseline dataset, baseline records or snapshot name.

## `baselineRecords` (type: `array`):

Up to 10,000 records as a JSON array. Use exactly one of: baseline dataset, baseline records or snapshot name.

## `snapshotName` (type: `string`):

Compare the current data with the snapshot saved by the last run under this name, then save the current data as the new snapshot. The first run only creates the snapshot and costs nothing. Schedule the Actor and you get a change feed. Snapshots are stored in your own key-value store named dataset-diff-snapshots.

## `updateSnapshot` (type: `boolean`):

Save the current data as the new snapshot when the run completes. Turn off for a dry run. Ignored when you do not use a snapshot name.

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

Field(s) that identify a record, for example \["url"] or \["shopId","sku"]. Leave empty to let the Actor choose a key; it tells you why in the run summary, and stops with an explanation if it cannot find a reliable one.

## `keyNormalization` (type: `string`):

How key values are compared. Auto treats web addresses as URLs (ignores http/https, www, tracking parameters, trailing slash) and other values as text (ignores case and spacing).

## `keyCaseSensitive` (type: `boolean`):

Treat "ABC" and "abc" as different keys.

## `fallbackKeyFields` (type: `array`):

Used for records that are missing a part of the key, for example \["url"] when some records have no sku.

## `duplicateKeyPolicy` (type: `string`):

What to do when several records share a key. Occurrence matches the first with the first, the second with the second (reordered duplicates can look like changes).

## `missingKeyPolicy` (type: `string`):

Whole record: identical records still match, but a record that changed shows up as removed plus added. Skip: ignore them. Stop: fail the run.

## `fuzzyMatch` (type: `object`):

Example: {"enabled": true, "fields": \["title"], "threshold": 0.92}. Pairs a removed and a new record that are almost the same text ("same product, new URL") and reports them as one changed record with the confidence. Numbers inside the text must match exactly.

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

Dot paths, for example \["price","stock"]. Empty means every field. Wildcards: \* is one level, \*\* is any depth.

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

Dot paths or wildcards, for example \["meta.\*","\*\*.updatedAt"]. Ignored fields never create changes.

## `ignoreVolatileFields` (type: `boolean`):

Skips fields that change on every scrape without meaning anything, such as scrapedAt, crawledAt, timestamp, requestId and #debug. The list used is echoed in the run summary.

## `arrayKeys` (type: `object`):

Example: {"variants": "sku", "offers": \["seller","currency"]}. Items in those lists are matched by the key instead of by position, so changes are reported like variants\[sku=B2].stock.

## `arrayMode` (type: `string`):

Set: ignore order, report added and removed items. Ordered: compare position by position.

## `normalize` (type: `object`):

All default to on except caseInsensitive: trim, collapseWhitespace, unicodeNfkc, caseInsensitive, emptyEqualsMissing (null, empty text, \[] and {} equal missing), parseNumericStrings ("$1,299.00" equals 1299), parseDates. Example: {"caseInsensitive": true}.

## `numericTolerance` (type: `object`):

Ignore tiny numeric movements. Example: {"rel": 0.01, "perField": {"rating": {"abs": 0.05}}} ignores moves under 1% and rating moves under 0.05. The summary says how many were ignored.

## `rules` (type: `array`):

Example: \[{"id":"price-drop","field":"price","when":"decreased","minPercent":10,"severity":"high"},{"field":"stock","when":"became","value":0}]. when: changed, increased, decreased, became, became_empty, became_nonempty, added, removed, matches (pattern), crossed (threshold, direction). Use field $record with when added or removed for new and gone records. Matching records get alerts and a severity you can filter on in Zapier, Make or n8n.

## `watchFields` (type: `array`):

Fields listed first in each summary line, for example \["price","stock"]. Other changed fields are still reported.

## `emit` (type: `array`):

Any of: added, removed, changed. Default is all three. You are charged for the changes returned, so this also controls cost.

## `alertsOnly` (type: `boolean`):

Return only records that triggered at least one alert rule. Detected-but-skipped changes are not charged.

## `outputMode` (type: `string`):

One item per changed record, or one item per changed field (easier for spreadsheets). You are charged per changed record either way.

## `includeRecords` (type: `string`):

Changed fields only (default), the full records, or none.

## `includeSuppressed` (type: `boolean`):

Adds a note to changed records about differences that were suppressed, such as numeric moves within tolerance.

## `abortIfShrinksPercent` (type: `integer`):

Protects against a failed scrape showing every record as removed. Example: 50 stops the run if the current dataset has more than 50% fewer records than the baseline. 0 turns it off; a warning is always given below 50%.

## `strategy` (type: `string`):

Auto uses memory for small datasets and a multi-pass low-memory method for large ones. Results are identical.

## `maxRecordsPerSide` (type: `integer`):

Safety limit for each side. Default 500,000.

## `maxChangesReported` (type: `integer`):

Hard cap on the changes returned (and charged) in one run. Default 100,000.

## Actor input object example

```json
{
  "currentRecords": [
    {
      "sku": "A-100",
      "title": "Acme Mug",
      "price": 15.99,
      "stock": 0,
      "tags": [
        "new",
        "sale"
      ],
      "scrapedAt": "2026-10-24T08:00:00Z"
    },
    {
      "sku": "B-200",
      "title": "Blue Pen",
      "price": 2.5,
      "stock": 40,
      "tags": [],
      "scrapedAt": "2026-10-24T08:00:00Z"
    },
    {
      "sku": "D-400",
      "title": "Desk Lamp",
      "price": 30,
      "stock": 7,
      "tags": [
        "eco"
      ],
      "scrapedAt": "2026-10-24T08:00:00Z"
    }
  ],
  "baselineRecords": [
    {
      "sku": "A-100",
      "title": "Acme Mug",
      "price": 19.99,
      "stock": 12,
      "tags": [
        "new"
      ],
      "scrapedAt": "2026-10-23T08:00:00Z"
    },
    {
      "sku": "B-200",
      "title": "Blue Pen",
      "price": 2.5,
      "stock": 40,
      "tags": [],
      "scrapedAt": "2026-10-23T08:00:00Z"
    },
    {
      "sku": "C-300",
      "title": "Cable",
      "price": 5,
      "stock": 9,
      "tags": [],
      "scrapedAt": "2026-10-23T08:00:00Z"
    }
  ],
  "updateSnapshot": true,
  "keyNormalization": "auto",
  "keyCaseSensitive": false,
  "duplicateKeyPolicy": "occurrence",
  "missingKeyPolicy": "wholeRecord",
  "ignoreVolatileFields": true,
  "arrayMode": "set",
  "emit": [
    "added",
    "removed",
    "changed"
  ],
  "alertsOnly": false,
  "outputMode": "records",
  "includeRecords": "changedFields",
  "includeSuppressed": false,
  "abortIfShrinksPercent": 0,
  "strategy": "auto",
  "maxRecordsPerSide": 500000,
  "maxChangesReported": 100000
}
```

# Actor output Schema

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

One item per added, removed or changed record, with before/after values, numeric deltas, alerts and a plain-English summary.

## `summary` (type: `string`):

Counts, the key that was used and why, what was ignored as noise, and the charges for this run.

## `reportHtml` (type: `string`):

A self-contained report: counts, top alerts, biggest numeric movers and what was ignored. Free of charge.

## `reportMd` (type: `string`):

The same report as Markdown, handy for Slack or e-mail.

# 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 = {
    "currentRecords": [
        {
            "sku": "A-100",
            "title": "Acme Mug",
            "price": 15.99,
            "stock": 0,
            "tags": [
                "new",
                "sale"
            ],
            "scrapedAt": "2026-10-24T08:00:00Z"
        },
        {
            "sku": "B-200",
            "title": "Blue Pen",
            "price": 2.5,
            "stock": 40,
            "tags": [],
            "scrapedAt": "2026-10-24T08:00:00Z"
        },
        {
            "sku": "D-400",
            "title": "Desk Lamp",
            "price": 30,
            "stock": 7,
            "tags": [
                "eco"
            ],
            "scrapedAt": "2026-10-24T08:00:00Z"
        }
    ],
    "baselineRecords": [
        {
            "sku": "A-100",
            "title": "Acme Mug",
            "price": 19.99,
            "stock": 12,
            "tags": [
                "new"
            ],
            "scrapedAt": "2026-10-23T08:00:00Z"
        },
        {
            "sku": "B-200",
            "title": "Blue Pen",
            "price": 2.5,
            "stock": 40,
            "tags": [],
            "scrapedAt": "2026-10-23T08:00:00Z"
        },
        {
            "sku": "C-300",
            "title": "Cable",
            "price": 5,
            "stock": 9,
            "tags": [],
            "scrapedAt": "2026-10-23T08:00:00Z"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("hiredhandai/dataset-diff-explainer").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 = {
    "currentRecords": [
        {
            "sku": "A-100",
            "title": "Acme Mug",
            "price": 15.99,
            "stock": 0,
            "tags": [
                "new",
                "sale",
            ],
            "scrapedAt": "2026-10-24T08:00:00Z",
        },
        {
            "sku": "B-200",
            "title": "Blue Pen",
            "price": 2.5,
            "stock": 40,
            "tags": [],
            "scrapedAt": "2026-10-24T08:00:00Z",
        },
        {
            "sku": "D-400",
            "title": "Desk Lamp",
            "price": 30,
            "stock": 7,
            "tags": ["eco"],
            "scrapedAt": "2026-10-24T08:00:00Z",
        },
    ],
    "baselineRecords": [
        {
            "sku": "A-100",
            "title": "Acme Mug",
            "price": 19.99,
            "stock": 12,
            "tags": ["new"],
            "scrapedAt": "2026-10-23T08:00:00Z",
        },
        {
            "sku": "B-200",
            "title": "Blue Pen",
            "price": 2.5,
            "stock": 40,
            "tags": [],
            "scrapedAt": "2026-10-23T08:00:00Z",
        },
        {
            "sku": "C-300",
            "title": "Cable",
            "price": 5,
            "stock": 9,
            "tags": [],
            "scrapedAt": "2026-10-23T08:00:00Z",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("hiredhandai/dataset-diff-explainer").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 '{
  "currentRecords": [
    {
      "sku": "A-100",
      "title": "Acme Mug",
      "price": 15.99,
      "stock": 0,
      "tags": [
        "new",
        "sale"
      ],
      "scrapedAt": "2026-10-24T08:00:00Z"
    },
    {
      "sku": "B-200",
      "title": "Blue Pen",
      "price": 2.5,
      "stock": 40,
      "tags": [],
      "scrapedAt": "2026-10-24T08:00:00Z"
    },
    {
      "sku": "D-400",
      "title": "Desk Lamp",
      "price": 30,
      "stock": 7,
      "tags": [
        "eco"
      ],
      "scrapedAt": "2026-10-24T08:00:00Z"
    }
  ],
  "baselineRecords": [
    {
      "sku": "A-100",
      "title": "Acme Mug",
      "price": 19.99,
      "stock": 12,
      "tags": [
        "new"
      ],
      "scrapedAt": "2026-10-23T08:00:00Z"
    },
    {
      "sku": "B-200",
      "title": "Blue Pen",
      "price": 2.5,
      "stock": 40,
      "tags": [],
      "scrapedAt": "2026-10-23T08:00:00Z"
    },
    {
      "sku": "C-300",
      "title": "Cable",
      "price": 5,
      "stock": 9,
      "tags": [],
      "scrapedAt": "2026-10-23T08:00:00Z"
    }
  ]
}' |
apify call hiredhandai/dataset-diff-explainer --silent --output-dataset

```

## MCP server setup

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

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/kfj1ZT4us9E3oaszO/builds/CJVfyIfJ4UpMAdPjp/openapi.json
