# Scraper Output Monitor | Silent Failure & Drift Alerts (`broomwagon/scraper-output-monitor`) Actor

Catch the day your scrape silently breaks. Point it at any scraper's dataset and get one verdict per run: fields that emptied out, fields that vanished, types that changed, volumes that collapsed, all judged against your own rolling baseline. Deterministic, stateless, priced per run not per row.

- **URL**: https://apify.com/broomwagon/scraper-output-monitor.md
- **Developed by:** [Brandon Mensing](https://apify.com/broomwagon) (community)
- **Categories:** Developer tools, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.02 / health verdict

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Scraper Output Monitor

<img align="right" src="https://broomwagon.vercel.app/icons/brand-mark.png" width="64" alt="broomwagon.">

> Catch the day your scrape silently breaks: field drift, completeness decay, and volume anomalies, judged against your own rolling baseline.

Scrapers rarely fail loudly. The run goes green, the dataset has rows, and only weeks
later does someone notice that `phone` has been empty since the site redesign, or that
`price` started arriving as a string, or that 4,000 listings quietly became 400. This
actor is the check that runs right after your scrape and answers one question every
time: **is this output normal for this scrape?**

Part of the **Broomwagon** family: deterministic tools that watch scraped and AI data
change. No LLM guesses at your data quality. Same records in, same verdicts out.

### The pipeline: scrape → check → alert

1. **Scrape (any actor).** Your scheduled scraper runs and produces a dataset. Nothing
   about it changes.
2. **Check (this actor).** Point `datasetId` at that run's dataset. Every field is
   profiled: how often it carries a value, what types those values are, whether it is
   there at all, and how many records came back.
3. **Alert.** Each run emits one `run-health` verdict plus one `field-health` row per
   field, with the delta and z-score against your baseline and `anomaly: true` when the
   change is statistically real rather than Tuesday noise. Filter on `verdict: "alert"`
   in n8n, Make, or a webhook and route it to Slack. That is the whole loop.

Chaining is zero-glue: create one key-value store in your account, put its ID in
`memoryStoreId`, and schedule this actor right after your scraper. Baselines accumulate
themselves, run after run.

### What it watches

| Signal | What breaking looks like | Verdict |
| --- | --- | --- |
| Record volume | 4,000 listings become 400, or zero | `volume-drop` / `volume-spike` |
| Field completeness | `phone` was 94% filled, now 3% | `fill-drop` / `fill-spike` |
| Schema drift | `phone` stops appearing in records at all | `missing-field` |
| Schema drift | a field you have never seen shows up | `new-field` |
| Type drift | `price` arrives as `"19.99"` instead of `19.99` | `type-shift` |

A field counts as filled only when it carries something usable. `null`, `""`, `[]`, and
`{}` are all gaps, because a `"reviews": []` from a broken selector is not a value.

### Stateless, with memory

Every run returns a compact aggregate (the `MEMORY` key-value record) holding your
rolling per-field baselines: sufficient statistics, never your data. Pass it back next
run (`memory`), or point the actor at a key-value store in **your** account
(`memoryStoreId`) and scheduled runs chain automatically. Broomwagon stores nothing;
your history is yours, and the memory stays a few kilobytes no matter how big the scrape.

First run establishes the baseline. Deltas begin on run two. Alerts engage after five
runs of baseline, so a pipeline that is still settling stays quiet. A field that
vanishes for good stops alerting after three runs and drops out of the baseline: the
monitor accepts the new normal rather than nagging forever.

### Try it in two minutes (no scraper needed)

**Run 1.** Leave the sample records in **Inline records** and start the run. You get a
`run-health` verdict, one `field-health` row per field, and a `MEMORY` record in the
run's key-value store: your baselines, held by you. Everything reads `baseline`, which
is correct, there is nothing to compare to yet.

**Run 2.** Copy the `MEMORY` object into **Previous memory**, delete a field from one of
the sample records, and run again. That field's row now carries `previousFillRate`, a
negative `delta`, and after five runs of baseline, `anomaly: true`. In real use, replace
the pasted records with your scraper run's dataset as described above; a picked dataset
always wins over the samples in the form.

### Input

- `datasetId` | `items` | `fileUrl`: the scrape output to check. A picked dataset wins.
- `fields`: limit the watch list, e.g. `["name", "price", "phone"]`. Default: every
  field, which is also how new fields get noticed.
- `memory` / `memoryStoreId`: the memory, as above.

### Output

- Dataset row 1, `kind: "run-health"`: `itemCount`, `previousItemCount`, `delta`,
  `zScore`, `status`, `fieldsTracked`, `fieldsNew`, `fieldsMissing`, `anomalies`, and
  `verdict` (`ok`, `baseline`, or `alert`).
- Then one `kind: "field-health"` row per field: `fillRate`, `previousFillRate`,
  `delta`, `zScore`, `dominantType`, `previousDominantType`, `status`, `anomaly`.
- `OUTPUT`: run report. `MEMORY`: your rolling baselines, to send back next run.

### Pricing (pay-per-event)

| Event | What you pay for | Price |
| --- | --- | --- |
| `run-report` | Per scrape checked | $0.02 |
| `anomaly-alert` | Per real problem detected | $0.05 |
| `apify-actor-start` | Run start, per GB of run memory | $0.005 |

**Worked example:** one scraper, checked daily: 30 × $0.025 = **$0.75/month**, plus a
nickel per genuine alert (a healthy month has none). Ten scrapers checked daily:
**$7.50/month** for continuous coverage of the whole pipeline. Note that the price does
not move with dataset size, a 500K-row scrape costs the same to check as a 50-row one.

Set a max charge on any run (`Maximum cost per run` in Console, or
`ACTOR_MAX_TOTAL_CHARGE_USD` via API) and the actor stops cleanly at your budget.

### Integrations

- **Apify Schedule (zero glue):** create a key-value store once, put its ID in
  `memoryStoreId`, and schedule this actor right after your scrape. Baselines, deltas,
  and alerts accumulate automatically.
- **API:** `POST https://api.apify.com/v2/acts/broomwagon~scraper-output-monitor/runs`
  with `{"datasetId": "<scrape run's dataset>", "memory": <last MEMORY record>}`. Read
  `MEMORY` back from the run's key-value store for the next call.
- **n8n / Make:** scraper node → this actor → filter on `verdict: "alert"` → Slack or
  PagerDuty. Four nodes and your scrapes stop failing silently.
- **MCP / AI agents:** callable as a tool via the Apify MCP server; agents round-trip
  the `MEMORY` object as tool context between calls, so an agent that scrapes on a
  schedule can tell when its own source went bad.

### Roadmap

Per-field value distributions (catching "the price column is now all zeros" when the
fill rate never moved), cross-field consistency rules, and drift attribution against a
named previous run. Built by an ex-Elastic engineer who spent years on log analytics:
this is time-series observability pointed at your scrape output.

### The Broomwagon family

This actor is one of nine deterministic post-processing tools from [Broomwagon](https://apify.com/broomwagon): the layer that follows your scrapers and agents, cleaning and watching what they produce. Same input, same output, every time.

- [Only New Items](https://apify.com/broomwagon/only-new-items) — deliver only records you have never delivered before.
- [Dataset Deduper](https://apify.com/broomwagon/dataset-deduper) — exact and fuzzy dedupe for any dataset, with an audit trail.
- [CSV Doctor](https://apify.com/broomwagon/csv-doctor) — repair broken CSV files, with a report of every fix.
- [LLM Output Guard](https://apify.com/broomwagon/llm-output-guard) — validate LLM and agent JSON against your schema.
- [PII Redactor](https://apify.com/broomwagon/pii-redactor) — strip emails, phones, SSNs, cards, and addresses, deterministically.
- [Record Linker](https://apify.com/broomwagon/record-linker) — fuzzy join two datasets that share no key.
- [Google Maps Scraper Deduper](https://apify.com/broomwagon/google-maps-deduper) — merge duplicate places across Google Maps scrapes.
- [AI Search Visibility Monitor](https://apify.com/broomwagon/ai-search-visibility-monitor) — track your brand's share of voice in AI answers.

# Actor input Schema

## `datasetId` (type: `string`):

The dataset from the scrape you want checked, usually the run right before this one. Pipe it straight in; a picked dataset always wins over the sample records below.

## `items` (type: `array`):

Records provided inline as a JSON array, for trying the actor out without wiring a scraper up first. Ignored when a dataset is picked above.

## `fileUrl` (type: `string`):

URL of a JSON or NDJSON file of records to check.

## `fields` (type: `array`):

Limit monitoring to these fields, e.g. \["name", "price", "phone"]. Default: every field the scrape returns, which is also how new fields get noticed.

## `memory` (type: `object`):

An object beginning with "memoryVersion", copied from the MEMORY record in your last run's key-value store (not your scraped records, those go in the dataset or inline fields above). This is what makes drift detection possible: it holds the baselines the current run is judged against.

## `memoryStoreId` (type: `string`):

Optional: a key-value store in YOUR account. The actor reads the baselines from it before running and writes the update back after, so scheduled runs chain automatically with no workflow tool and nothing to copy by hand.

## Actor input object example

```json
{
  "items": [
    {
      "name": "Hopleaf Bar",
      "address": "5148 N Clark St, Chicago, IL",
      "phone": "+1 773-334-9851",
      "website": "https://hopleaf.com",
      "rating": 4.5,
      "reviewsCount": 2841
    },
    {
      "name": "Big Jones",
      "address": "5347 N Clark St, Chicago, IL",
      "phone": "+1 773-275-5725",
      "website": "https://bigjoneschicago.com",
      "rating": 4.5,
      "reviewsCount": 1673
    },
    {
      "name": "Kopi Cafe",
      "address": "5317 N Clark St, Chicago, IL",
      "phone": "+1 773-989-5674",
      "website": null,
      "rating": 4.3,
      "reviewsCount": 786
    }
  ]
}
```

# Actor output Schema

## `verdicts` (type: `string`):

The run verdict plus one row per field: fill rates, deltas, z-scores, and what broke.

## `memory` (type: `string`):

Your rolling baselines for this scrape. Send it back as `memory` on the next run, or skip this by using `memoryStoreId`.

## `report` (type: `string`):

Records scanned, fields tracked, alerts raised, and baseline status.

# 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 = {
    "items": [
        {
            "name": "Hopleaf Bar",
            "address": "5148 N Clark St, Chicago, IL",
            "phone": "+1 773-334-9851",
            "website": "https://hopleaf.com",
            "rating": 4.5,
            "reviewsCount": 2841
        },
        {
            "name": "Big Jones",
            "address": "5347 N Clark St, Chicago, IL",
            "phone": "+1 773-275-5725",
            "website": "https://bigjoneschicago.com",
            "rating": 4.5,
            "reviewsCount": 1673
        },
        {
            "name": "Kopi Cafe",
            "address": "5317 N Clark St, Chicago, IL",
            "phone": "+1 773-989-5674",
            "website": null,
            "rating": 4.3,
            "reviewsCount": 786
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("broomwagon/scraper-output-monitor").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 = { "items": [
        {
            "name": "Hopleaf Bar",
            "address": "5148 N Clark St, Chicago, IL",
            "phone": "+1 773-334-9851",
            "website": "https://hopleaf.com",
            "rating": 4.5,
            "reviewsCount": 2841,
        },
        {
            "name": "Big Jones",
            "address": "5347 N Clark St, Chicago, IL",
            "phone": "+1 773-275-5725",
            "website": "https://bigjoneschicago.com",
            "rating": 4.5,
            "reviewsCount": 1673,
        },
        {
            "name": "Kopi Cafe",
            "address": "5317 N Clark St, Chicago, IL",
            "phone": "+1 773-989-5674",
            "website": None,
            "rating": 4.3,
            "reviewsCount": 786,
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("broomwagon/scraper-output-monitor").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 '{
  "items": [
    {
      "name": "Hopleaf Bar",
      "address": "5148 N Clark St, Chicago, IL",
      "phone": "+1 773-334-9851",
      "website": "https://hopleaf.com",
      "rating": 4.5,
      "reviewsCount": 2841
    },
    {
      "name": "Big Jones",
      "address": "5347 N Clark St, Chicago, IL",
      "phone": "+1 773-275-5725",
      "website": "https://bigjoneschicago.com",
      "rating": 4.5,
      "reviewsCount": 1673
    },
    {
      "name": "Kopi Cafe",
      "address": "5317 N Clark St, Chicago, IL",
      "phone": "+1 773-989-5674",
      "website": null,
      "rating": 4.3,
      "reviewsCount": 786
    }
  ]
}' |
apify call broomwagon/scraper-output-monitor --silent --output-dataset

```

## MCP server setup

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

```

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/2XmmGnD6lswljJtTL/builds/XsbhLuOvPjHzoP3Ql/openapi.json
