# Dataset Output Acceptance Gate (`plusonets/dataset-output-acceptance-gate`) Actor

Decide whether a dataset is safe to send downstream. Check schema, coverage, duplicates, currency, geography, canaries, and a saved baseline, then return accepted, partial, or rejected.

- **URL**: https://apify.com/plusonets/dataset-output-acceptance-gate.md
- **Developed by:** [Olalekan Johnson](https://apify.com/plusonets) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 run assesseds

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 Output Acceptance Gate

Decide whether a dataset is safe to send into a CRM, warehouse, report, or customer workflow. You give it a dataset from your own Apify account, or a public JSON/CSV file, plus a contract. It returns **accepted**, **partial**, or **rejected**, with the reasons a webhook can branch on.

It is built for automation agencies and small SaaS teams that already collect data and need a gate in front of the next step. A run that merely finishes is not treated as a complete business result.

The default input needs no token and no dataset. It checks the bundled office-supply sample against the products contract and returns an accepted verdict.

### What you put in

| Input | Role |
| --- | --- |
| Source | Bundled sample, a dataset ID, a run ID, a public JSON/CSV URL, or pasted JSON/CSV |
| Preset | Products, jobs, places, reviews, or a custom contract |
| Contract | Identity keys, types, required-field coverage, allowed values such as currency and country, min/max business rows, canaries |
| Empty allowance | Accept zero business rows when the source is quiet, including a status row that says `no_results` |
| Baseline | A named key-value store, plus an optional previous dataset. Set a stable baseline key for scheduled runs |
| Fail on rejection | Mark this run failed after the report is saved, so an integration stops |

Dataset and baseline-dataset fields request **read** access only. The baseline store requests **read and write**. The Actor uses limited permissions. It does not ask for access to the rest of your account.

Run ID lookup uses the run's default dataset. If the limited-permission token cannot read that run, copy the run's default dataset ID into **Dataset ID** instead.

Public URLs must be `http` or `https`. Localhost, private IPs, link-local addresses, and embedded credentials are rejected.

### What you get back

The default dataset is flat, so a webhook, Make, n8n, or a spreadsheet can filter it without unpacking nested objects.

The first row is the verdict:

```json
{
  "recordType": "verdict",
  "verdict": "accepted",
  "coverageStatus": "complete",
  "confidence": "medium",
  "gateFailed": false,
  "materialChange": true,
  "businessRows": 16,
  "uniqueEntities": 16,
  "duplicateRatio": 0,
  "statusOnlyRows": 0,
  "dominantCurrency": "USD",
  "dominantCountry": "US",
  "currencyShift": false,
  "geographyShift": false,
  "coverageCollapse": false,
  "legitimatelyEmpty": false,
  "lastKnownGoodDatasetId": null,
  "chargedRunAssessed": 1,
  "chargedThousandRows": 0,
  "sourceRef": "bundled:samples/products.json",
  "preset": "products",
  "profileKey": "products:bundled-sample",
  "reasons": "",
  "observedAt": "2026-10-23T04:19:00Z"
}
```

Further rows are checks (`recordType: "finding"`) and per-field coverage (`recordType: "field"`). The same verdict is repeated on every row.

`coverageStatus` is separate from the verdict:

| Status | Meaning |
| --- | --- |
| complete | The whole source was read and it contains business rows |
| partial | The read stopped at the row cap, so the rest was not judged |
| empty | No business rows |
| blocked | The source could not be read (permissions, private URL, HTTP error) |
| unsupported | The body was not a JSON or CSV table |

The run key-value store holds:

- `OUTPUT` — counts, reasons, and charged units
- `ACCEPTANCE_REPORT` — findings, field coverage, and the contract that was used
- `ACCEPTANCE-REPORT.html` — the same profile as a page

An **accepted** run writes a profile to the named store `acceptance-gate-baselines` (or the store you name). The profile keeps coverage, currency and country mix, unique-entity count, and `lastKnownGoodDatasetId`. A partial verdict, a rejection, or a crash does not replace that profile. If this accepted run has no dataset ID, the previous last-known-good dataset ID stays in the profile.

Confidence is **medium** on the first accepted run because there is no baseline yet. Later accepted runs that match a baseline can be **high**.

### What the contract catches

- Present values with the wrong type, and review ratings outside 0–5
- Required fields below their coverage threshold
- A critical field whose coverage falls below 80% of the baseline (coverage that was already under 50% is ignored)
- Duplicate identities. Any duplicates make the verdict partial. Above the duplicate cap (default 2%) rejects
- Currency or country values outside an allow-list
- A new currency or country on more than 5% of rows, compared with the baseline
- A dominant currency or country change inside the values the baseline already saw (partial, not rejected)
- A dataset of status or error objects and no business rows
- A quiet source, when you turn on **Allow a legitimately empty result**
- Canary records that must appear, matched on the fields you list
- A large drop in unique entities, or a jump in upstream cost per unique entity, when the source run reports usage

Cost per unique entity is a warning. This Actor cannot refund the upstream run that already finished.

Min and max row counts apply to business rows, after status objects are removed. The check window defaults to 50,000 rows and cannot exceed 200,000. A window that stops early is partial, and it does not move the baseline, because the unseen rows were not judged.

### Presets

| Preset | Identity, in order | Required coverage |
| --- | --- | --- |
| Products | `sku`, then `productId`, then `id` | name 95%, price 90%, currency 90%, url 80%, sku 90% |
| Jobs | `jobId`, then `id`, then `url` | title 95%, company 90%, url 80% |
| Places | `placeId`, then `id`, then `url` | name 95%, address 80%, latitude and longitude 70% |
| Reviews | `reviewId`, then `id`, then `url` | rating 98% (and rating must be from 0 to 5), text 70% |
| Custom | Only what you type | Only what you type |

Alternate identity uses the first key that is filled on at least half of the business rows. Composite identity requires every key. Leave the override lists empty to keep the preset. Values you do set are merged on top.

The bundled sample is synthetic catalog data created for this Actor. It is not a scrape of a store. One product has no URL and one has no brand, which is still inside the products thresholds.

### Pricing

Pay per event, for a check that finished and was written. A source that cannot be read is not charged. There is no per-dataset-item event.

| Event | When it is charged | Price |
| --- | --- | --- |
| Run assessed | One completed verdict | $0.05 |
| 1,000 rows checked | One event per full 1,000 rows read. The remainder is included in the run assessment | $0.002 |
| Platform start | Once per run, per GB of memory. This Actor defaults to 512 MB | $0.005 per GB, about $0.0025 per run |

A baseline dataset is not billed as a second assessment. The platform also charges the start event in the table. Those are the rates this Actor is priced at.

**Trial.** 20 assessments of 400 rows. 20 × $0.05 = $1.00. No thousand-row events. Start charges about 20 × $0.0025 = $0.05. About **$1.05**.

**A production estate.** 300 assessments of 50,000 rows. 300 × $0.05 = $15. 300 × 50 × $0.002 = $30. Start charges about $0.75. About **$45.75**.

**A capped run.** One 50,000-row check is $0.05 + $0.10 + about $0.0025, roughly **$0.15**, if the row cap stays at 50,000. If the task's maximum charge is lower, charging stops when the platform budget is exhausted, `chargeLimitReached` is true, and unbilled thousand-row units are not charged. The data verdict is still returned. Raising the check window to the 200,000-row maximum is $0.05 + $0.40, plus the start charge, for a single assessment.

### Wire it into a schedule

1. Let the upstream Actor finish.
2. Call this Actor with that run's default dataset ID and a stable **Baseline key**, such as `acme-products-daily`. A new run ID on every schedule is fine. The key is what keeps the profile.
3. Turn on **Fail the run when rejected** if the next step should not run.
4. In the webhook, Make, or n8n branch, read the dataset and keep the row where `recordType` is `verdict`.
5. Stop the downstream write when `gateFailed` is true or `verdict` is `rejected`. Treat `partial` as needs a person unless you have decided partial data is still loadable.
6. For a quiet alert, turn on **Dataset rows only when the profile changes** and filter to `materialChange` true. The HTML and JSON reports are still saved on every run.

Sheets: download the dataset as CSV. The columns are already flat.

A rejected gate does not undo charges from the Actor that produced the dataset. Put this gate before the system that spends money on the rows, not after a customer has already been billed for them.

### Limits

- Types are checked as stored. A price that arrives as text fails a number field. CSV input does parse numeric cells.
- Country and currency comparison is literal after trimming and case-folding. `UK` and `GB` are different until you put both in the allow-list.
- The baseline stores coverage and the dataset ID, not a second copy of the rows. The report names a short identity when a type check fails. It does not copy review text.
- Two runs writing the same baseline key at the same time can race. Give each workflow its own key and one schedule.
- The first accepted run creates the baseline. It cannot yet detect a currency shift.
- Memory is 512 MB, with a 1,024 MB ceiling. Very wide rows near the 200,000 cap can be tight. Lower the check window if a run runs out of memory.
- This version does not start a fallback Actor.

### Run it locally

Python 3.12.

```bash
pip install -r requirements.txt
python -m src
```

With no `INPUT.json`, the Actor uses the bundled product sample. Output is under `storage/`: the dataset, `OUTPUT`, and `ACCEPTANCE-REPORT.html`.

```bash
pip install pytest
pytest
```

### FAQ

**Why was a successful upstream run rejected?**
The upstream run can succeed and still return status objects, the wrong currency, or a field that collapsed. `coverageStatus` says how much was read. `verdict` says whether it met the contract.

**A quiet day keeps failing.**
Turn on **Allow a legitimately empty result**, or set the minimum business rows to 0. An error status is still rejected. A row that only says the source was empty (`emptyReason` `no_results`, or status `empty`) is accepted when the allowance is on.

**Partial and rejected feel different.**
Rejected means a hard contract failure: do not load the data. Partial means the rows are usable with a warning, such as a few duplicates or a dominant currency that changed inside the known set. Only an accepted verdict replaces the baseline.

**Will a bad run erase yesterday's good dataset ID?**
No. The last accepted profile, including `lastKnownGoodDatasetId`, is left in place.

**Does the default run call the internet?**
No. It reads `samples/products.json` inside the Actor.

Questions: plusonecares@gmail.com.

# Actor input Schema

## `sourceType` (type: `string`):

Where to read rows. Dataset and run use this run's token. A URL must be public JSON or CSV. Inline accepts pasted JSON or CSV. The bundled sample needs no token.

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

Dataset in your Apify account. Limited permissions grant read access only to the dataset you enter here.

## `runId` (type: `string`):

Run in your account. The gate reads that run's default dataset and, when the token allows it, the run's usage. If run lookup is denied, paste the run's default dataset ID instead.

## `sourceUrl` (type: `string`):

http or https URL of a JSON array, JSON object with an items list, NDJSON, or CSV. Private networks, localhost, and link-local addresses are rejected.

## `inlineData` (type: `string`):

Raw JSON or CSV, up to 20 MB. Used when Source is Pasted JSON or CSV.

## `inlineFormat` (type: `string`):

Format of the pasted rows: JSON (an array of objects) or CSV with a header row.

## `preset` (type: `string`):

Starting contract. Jobs, products, places, and reviews fill identity keys, types, and coverage thresholds. Custom starts empty. Any field you set below overrides the preset.

## `identityMode` (type: `string`):

Alternate uses the first identity field that is filled on at least half the business rows. Composite requires every identity field on a row.

## `identityKeys` (type: `array`):

Fields that identify one business row, in priority order. Leave empty to keep the preset. Example: sku, productId, id.

## `fieldTypes` (type: `object`):

Expected type for each present value. Missing values are a coverage problem, not a type error. Types: string, number, integer, boolean, array, object, url, date, email.

## `requiredFields` (type: `array`):

Leave empty to keep the preset. A type error on a required field rejects the dataset.

## `coverageThresholds` (type: `object`):

Minimum share of business rows that must have each field, from 0 to 1. Example: {"price": 0.9, "currency": 0.99}.

## `allowedValues` (type: `object`):

Allow-list per field. Any other present value rejects the dataset. Example: {"currency": \["USD", "EUR"], "country": \["US", "CA"]}.

## `numericRanges` (type: `object`):

Inclusive min and max for numeric fields. The reviews preset already limits rating to 0-5. Example: {"rating": {"min": 1, "max": 5}}.

## `minRows` (type: `integer`):

Counted after status-only rows are removed. Use 0 when an empty table is acceptable on its own.

## `maxRows` (type: `integer`):

Reject when the business-row count is above this. Leave empty for no maximum.

## `allowLegitimatelyEmpty` (type: `boolean`):

Accept zero business rows when the dataset is empty, or when a status row says the source was empty (empty, no_results, legitimately_empty). Error statuses still reject.

## `canaryRecords` (type: `array`):

Objects that must match at least one business row. Example: \[{"sku": "OFC-1001"}].

## `coverageCollapseRatio` (type: `number`):

Reject when a critical field's coverage falls below this fraction of the baseline. Critical fields are identity, required fields, threshold fields, currency, and geography. Baseline coverage under 50% is ignored.

## `maxDuplicateRatio` (type: `number`):

Extra copies divided by business rows. Above this rejects. Any duplicates at or below this make the verdict partial.

## `shiftTolerance` (type: `number`):

Share of business rows that may carry a currency or country the baseline has never seen. Above this rejects. A dominant-value change inside the known set is partial.

## `maxRowsToCheck` (type: `integer`):

Stop after this many rows. The verdict is partial, and the baseline is not replaced, because the rest of the dataset was not seen.

## `baselineStoreName` (type: `string`):

Named key-value store for accepted profiles. A rejected, partial, or failed run does not overwrite the last accepted profile.

## `baselineKey` (type: `string`):

Stable name for this workflow, such as acme-products-daily. Empty uses the preset plus the dataset ID, upstream Actor ID, or URL.

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

Optional previous dataset to compare for this run. When set, it is the comparison target instead of the stored profile. It is not billed as a second assessment.

## `failOnRejection` (type: `boolean`):

Exit with a failed run status after the report is written. Use this so a webhook or integration stops on rejection. Partial and accepted runs still succeed.

## `alertOnlyOnChange` (type: `boolean`):

When the verdict is accepted and coverage, identity count, currency, and geography are close to the baseline, push only the verdict row. The HTML and JSON reports are still written. Rejected and partial runs always include the checks.

## Actor input object example

```json
{
  "sourceType": "bundledSample",
  "inlineFormat": "json",
  "preset": "products",
  "identityMode": "alternate",
  "fieldTypes": {},
  "coverageThresholds": {},
  "allowedValues": {},
  "numericRanges": {},
  "minRows": 1,
  "allowLegitimatelyEmpty": false,
  "canaryRecords": [],
  "coverageCollapseRatio": 0.8,
  "maxDuplicateRatio": 0.02,
  "shiftTolerance": 0.05,
  "maxRowsToCheck": 50000,
  "baselineStoreName": "acceptance-gate-baselines",
  "failOnRejection": false,
  "alertOnlyOnChange": false
}
```

# Actor output Schema

## `verdict` (type: `string`):

Verdict, finding, and field-coverage rows. Filter recordType equals verdict for the decision.

## `checks` (type: `string`):

One row per contract check.

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

Readable profile report stored on this run.

## `reportJson` (type: `string`):

Machine-readable profile, findings, and contract echo.

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

OUTPUT record with counts, reasons, and charged units.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("plusonets/dataset-output-acceptance-gate").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("plusonets/dataset-output-acceptance-gate").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 '{}' |
apify call plusonets/dataset-output-acceptance-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plusonets/dataset-output-acceptance-gate"
        }
    }
}
```

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/02gyZmhWvRN2w1AXU/builds/dYfriPhepAKCg7bEa/openapi.json
