Bosco Labs

Tools › Data tools

Change Monitor: new, changed & removed items + Slack alerts

Coming soon to the Store

Run any scraper on a schedule, get only what changed, and an alert. This Actor compares each run of your scraper with the previous one and outputs only the new, changed and (optionally) removed items, matched by your key fields such as url, placeId or sku. One summary per run goes to Slack, Discord, Telegram, email or any webhook. For price tracking, new listings, job boards, stock and competitor monitoring.

Price$0.001 per change reported
DomainData tools
Call itApify API or MCP

Pricing (pay per event): $0.001 per change reported ($1 per 1,000); unchanged items, the baseline run and alerts are free; no start fee. The run stops at your max charge limit, and changes it couldn't report are reported on the next run.

Coming soon to the Apify StoreAsk for early access

Questions agents ask

  • “Compare today's run of my shop scraper with the previous one by url and tell me which prices changed.”

Example

Input

{"datasetId": "<the run's dataset ID>", "monitorName": "shop", "keyFields": ["url"], "watchFields": ["price"]}

Output

[{"change_type": "changed", "key": {"url": "https://shop.example/p/1"},
  "changed_fields": ["price"], "previous": {"price": 19.99},
  "item": {"url": "https://shop.example/p/1", "title": "Desk lamp", "price": 17.49, "stock": 12},
  "monitor_name": "shop-prices", "checked_at": "2026-11-05T06:00:12+00:00", "schema_version": "1"},
 {"change_type": "new", "key": {"url": "https://shop.example/p/9"}, "changed_fields": [],
  "previous": null, "item": {"url": "https://shop.example/p/9", "title": "Floor lamp", "price": 49, "stock": 3},
  "monitor_name": "shop-prices", "checked_at": "2026-11-05T06:00:12+00:00", "schema_version": "1"},
 {"change_type": "removed", "key": {"url": "https://shop.example/p/4"}, "changed_fields": [],
  "previous": null, "item": {"url": "https://shop.example/p/4"},
  "monitor_name": "shop-prices", "checked_at": "2026-11-05T06:00:12+00:00", "schema_version": "1"}]

What you get

FieldTypeMeaning
change_typestringnew = key not seen in the previous run; changed = a compared field differs; removed = key missing from this run.
keyobject | nullThe item's key field values, e.g. {"url": "https://..."}. null with whole-item comparison.
changed_fieldsarraychanged rows: the fields that differ (top-level field names, or your watchFields paths). Empty for new and removed.
previousobject | nullchanged rows: the old values of the changed fields that were kept: all watchFields, else small values (text up to 200 characters, numbers, booleans; up to 20 fields and 400 characters per item). Long text, lists and objects are left out; a field that was missing before is null. null when no old value is known, and for new and removed rows.
itemobject | nullThe current item, unchanged. For removed rows: the key values (the last known identity; with whole-item comparison, the removed item minus ignored fields).
monitor_namestringThe monitorName of this run.
checked_atstringWhen this run started the comparison (UTC, ISO 8601).
schema_versionstringOutput schema version, "1".

Use with AI agents (MCP)

Add this URL to your MCP client (Claude, Cursor, VS Code, ...) to load the Actor as a tool through the Apify hosted MCP server. It signs you in with OAuth or accepts Authorization: Bearer <APIFY_TOKEN>. It works once the Actor is live on the Apify Store.

https://mcp.apify.com?tools=bosco_labs/dataset-change-monitor

For “Compare today's run of my shop scraper with the previous one by url and tell me which prices changed.” the agent passes:

{"datasetId": "<the run's dataset ID>", "monitorName": "shop", "keyFields": ["url"], "watchFields": ["price"]}

Or call the Apify API: POST https://api.apify.com/v2/acts/bosco_labs~dataset-change-monitor/run-sync-get-dataset-items with the input as JSON body. Cost: $0.001 per change reported.

Input fields

FieldTypeMeaning
datasetIdstringApify dataset to compare with the previous run, e.g. "aBcD1234efGH5678i": usually your scraper run's default dataset. As an integration on your scraper, set it to "{{resource.defaultDatasetId}}" (see the README). Optional if you paste items.
itemsarrayItems to check directly, as a JSON array of objects, e.g. [{"url": "https://jobs.example.com/1042", "salary_eur": 65000}]. Checked before the dataset. Pasted items are all reported as new on a monitor's first run (no silent baseline). Without a monitorName nothing is saved, so every run is a fresh try-out; give a monitorName and paste again with an edit to see a change.
monitorNamestringName of the saved state: one name = one watched list, e.g. "shop-prices". Runs with the same name are compared with each other; use a different name per scraper or task. Letters, digits, - and _. Empty = "default" with a datasetId; with only pasted items, empty = a try-out that saves nothing.
keyFieldsarrayFields that identify the same item in every run, e.g. ["url"] or ["placeId"]. Dot paths work: ["product.sku"]. Required unless you use whole-item comparison. Items without any key field are skipped.
compareWholeItembooleanFor datasets without an ID field: an item is identified by all its fields (minus ignored ones), so items are only reported as new or removed, never as changed. Default false.
ignoreFieldsarrayFields that change on every run and are never compared, as dot paths. Default: scrapedAt, scraped_at, crawledAt, retrieved_at, timestamp and #debug. Set [] to compare everything.
watchFieldsarrayOnly changes in these fields count, e.g. ["price", "stock", "offer.status"]. The output then also shows the old values (previous). Empty = every field except the ignored ones.
reportarrayWhich changes become output rows (and are charged): new, changed and/or removed items. Removed is off by default, because a scraper run that stopped early would make many items look removed.
reportOnFirstRunbooleanThe first run of a monitor only saves the baseline and reports nothing (free). Turn on to report every item as new on that first run (charged per item).
slackWebhookUrlstringSlack incoming webhook URL (https://hooks.slack.com/services/...). Secret input, stored encrypted.
discordWebhookUrlstringDiscord channel webhook URL (https://discord.com/api/webhooks/...). Secret input, stored encrypted.
webhookUrlstringAny URL that receives a JSON POST with the counts, example rows and the link (Zapier, Make, n8n, your own API). Secret input, stored encrypted.
telegramBotTokenstringToken of your Telegram bot from @BotFather. Needs the chat ID too. Secret input, stored encrypted.
telegramChatIdstringChat, group or channel ID the bot posts to, e.g. "123456789" or "@mychannel".
emailstringAddress(es) for an email alert, comma-separated. Sent by the public Actor apify/send-mail, which runs in your account (a few seconds of compute).
alertMaxItemsintegerHow many changes to list in the alert as examples (key values and what changed). Default 10.
alertWhenNoChangesbooleanSend the alert even when a run found no changes (a daily "all quiet"). Default false.

Sources, licence and limits

Why does the first run report nothing? There is nothing to compare with yet. It saves the baseline for free, so you don't pay for every item on day one. Turn on reportOnFirstRun if you do want them all as new. Pasted items are the exception: they are always reported as new on a monitor's first run.

Where is the saved state? In a named key-value store dataset-change-monitor-state in your own Apify account, under the records <monitorName>-meta and <monitorName>-000 to -127. It holds fingerprints (hashes), the key values and the old values of small fields (with watchFields, the watched values; with compareWholeItem, the items themselves, so removals can be shown). Delete those records to start over.

I updated from an older version: why is previous null? Older versions kept fingerprints only. The first run after the update can't know the old values, so previous is null for that run's changes; it saves them, and from the next run on previous is filled. Nothing else changes: no new baseline, no extra charges.

What if I change keyFields, ignoreFields or watchFields? The monitor starts a new baseline on the next run (with a warning in the log), because old and new fingerprints can't be compared.

Why is "removed" off by default? A scraper run that stopped early, or was blocked, still "succeeds" with fewer items, and every missing item would be reported as removed. Turn it on when your scraper reliably returns the full list.

How many items can one monitor hold? Millions. Measured with 1 million shop items of 10 small fields: the state took 117 MB (the largest of the 128 records 0.95 MB, far under Apify's 9 MB record limit) and about 600 MB of memory, under the 1 GB default; comparing them took about a minute. Items with 20+ text fields need up to about 1.3 GB per million: give such a monitor 2 GB of memory. Up to 8 GB of memory covers roughly 8 million small items. Past that, a state record would pass 9 MB: the run then saves fingerprints only (previous stays null, with a warning in the log; use watchFields for the fields whose old values you need). If even that passes 9 MB, the run fails before saving and the old state stays intact: split the list over several monitors.

What if the run hits my max charge limit? It stops pushing rows. Changes that weren't reported are not marked as seen, so the next run reports them.

What if the run crashes or is restarted? The state is saved only at the end, after the changes are in the dataset, so a crash loses nothing: the next run finds the same changes. After a platform restart, rows already written are not charged again.

Two items with the same key in one dataset? The first one counts; the rest are skipped and counted as duplicate_keys in the log. Items without any key field are skipped too.

Which datasets can it read? Only the one you pick or pass in datasetId. The Actor runs with limited permissions.

Coming soon to the Apify StoreAsk for early access