# Lead List Cleaner - Dedupe, DNS & Email Hygiene (`zinin/lead-list-cleaner`) Actor

Normalize mixed lead rows, deduplicate domains and emails, check DNS/MX evidence, score retained leads, and return every rejection, confidence gap, safe action, and run summary for auditable CRM staging.

- **URL**: https://apify.com/zinin/lead-list-cleaner.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.50 / 1,000 cleaned lead rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

## Lead List Cleaner

Turn any scraper's raw lead dump — a Google Maps export, a directory scrape, a CSV
turned into JSON — into a clean, deduped, scored list ready to work. Every email gets a
real syntax + MX check, every domain gets a real DNS-alive check, duplicates collapse
into one row, and what's left is ranked by hotness so you know who to call first.

![Lead List Cleaner: buyer input to evidence-backed action](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/649545a2ef828a9f773d77b8b60ebcbf98710984/commercial115/lead-list-cleaner/readme-hero.webp)

### What you get

- Works on **any row shape** — no fixed schema. Point it at a Google Maps scraper
  export, a CSV turned into JSON, or a pasted list — it finds the email, domain, name
  and phone wherever the source row put them.
- Duplicate leads (same domain, or same email when there's no domain) collapse into one
  row, so you never pay to see the same company twice.
- Every email gets a real syntax + MX check, not a regex guess — dead addresses, role
  accounts (`info@`, `support@`...) and known disposable domains are all flagged.
- Every domain gets a real DNS alive/dead check — a company that folded two years ago
  doesn't make your call list.
- A 0-100 hotness score for what's left, so the leads worth calling first float to the
  top.
- Runs standalone, **or** as an Integration bolted straight onto the scraper you already
  use — see "How to run it" below.
- Runs on Apify: schedule it, monitor it, call it from the API, export to JSON/CSV/Excel
  or push straight into your own pipeline.

![Lead List Cleaner: evidence-to-action workflow](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/649545a2ef828a9f773d77b8b60ebcbf98710984/commercial115/lead-list-cleaner/readme-workflow.webp)

### How to run it

Three ways in — pick whichever fits your workflow.

1. **Paste rows directly.** Click **Try for free**, paste your lead rows into **Items**
   — any shape works — and hit **Start**.
2. **Point it at a dataset.** Leave **Items** empty, use the **Dataset ID** picker to
   select another Actor's run (or a public JSON URL into **Dataset URL**), then hit
   **Start**. Picking the dataset through that field — not just having its ID — is what
   grants this run read access to it; a dataset you didn't pick there is only reachable
   if it's public.
3. **Add it as an Integration** — how most buyers will actually use this. Open the
   scraper Actor you already run (a Google Maps scraper, a directory scraper, anything
   that produces a dataset of leads), go to its **Integrations** tab, click **Add
   integration**, and pick **Lead List Cleaner**. Leave **Dataset ID** empty in the
   integration's prefilled input — this Actor reads it automatically from the
   triggering run. From then on, every successful run of that scraper feeds straight
   into this one, no manual step required. One honest caveat: what actually grants
   access to another Actor's dataset is picking it through the **Dataset ID** field —
   that's a real permission grant, not just an ID. The raw
   `payload.resource.defaultDatasetId` this Actor also reads as a fallback carries no
   such grant, so it only succeeds when the triggering dataset happens to be public; on
   a private dataset it fails, and you'll see that failure spelled out in the run's free
   output row rather than a silent empty result.

### Pricing

Pay-per-event: **$0.005 per run start + $0.01 per cleaned lead delivered**. No monthly
seat, no minimum. 100 leads cost about **$1.01**; 1,000 leads about **$10.01**.

Both prices step down automatically with Apify's own account tier — from
$0.005/$0.01 on the FREE plan to $0.004/$0.008 on DIAMOND (up to 20% off, the standard
discount ladder every Actor in this fleet runs on, not a volume discount tied to how
many leads you clean) — this Actor's own Pricing tab shows what applies to your account.

The first **25 clean leads of every run are free** — no card, no commitment, see it
work on your own data before you pay for a single row. Rejected rows (duplicates, dead
emails, dead domains, below your score cutoff) and the final summary row are **always
free, on every run** — you only ever pay for a lead that actually passed.

### Input

| Field | Required | What it does |
|---|---|---|
| `items` | no | Raw lead rows to clean directly — any shape (a Google Maps scraper export, a CSV turned into JSON, anything with an email/website/phone somewhere in it). Takes priority over `datasetId` and `datasetUrl`. |
| `datasetId` | no | Another Actor's dataset to clean, chosen through the dataset picker (not a plain text ID) — picking it there is what grants this run read access to it. Filled in automatically when this Actor runs as an Integration — leave it empty in that setup. |
| `datasetUrl` | no | Public URL returning a JSON array of lead rows. Used only when both `items` and `datasetId` are empty. |
| `checks` | no | Pipeline stages to run: `dedupe`, `email`, `domain`, `score`. Default: all four. |
| `minScore` | no | Drop leads scoring below this (0-100) from the clean, billed output. They still come back as free rejected rows with a reason. Default `0`. |
| `maxRows` | no | Safety cap on how many input rows this run will process, regardless of source. Default `1000`. |

```json
{
    "items": [
        { "title": "Shopify", "categoryName": "Software company", "website": "https://www.shopify.com/", "phone": "+1 888-746-7439", "url": "https://www.google.com/maps/place/Shopify" },
        { "name": "HubSpot", "email": "press@hubspot.com", "phoneNumber": "+1 888-482-7768", "link": "https://www.hubspot.com/" },
        { "companyName": "Zoom Video Communications", "description": "Video conferencing platform. Contact us at info@zoom.us for enterprise sales.", "domain": "zoom.us" },
        { "title": "Shopify Inc", "website": "https://shopify.com/", "phone": "+1 888-746-7439" },
        { "title": "Stripe", "categoryName": "Payment processor", "website": "https://stripe.com/", "phone": "+1 888-926-2289", "emails": ["support@stripe.com"] }
    ],
    "checks": ["dedupe", "email", "domain", "score"],
    "minScore": 0,
    "maxRows": 1000
}
```

### Output

One dataset row per clean lead — this is a real row from a real run of the exact input
above:

```json
{
    "name": "Stripe",
    "email": "support@stripe.com",
    "domain": "stripe.com",
    "phone": "+1 888-926-2289",
    "sourceUrl": null,
    "raw": { "title": "Stripe", "categoryName": "Payment processor", "website": "https://stripe.com/", "phone": "+1 888-926-2289", "emails": ["support@stripe.com"] },
    "emailStatus": "valid",
    "emailFlags": ["role:support"],
    "domainStatus": "alive",
    "techStack": [],
    "hotnessScore": 70,
    "scoreReasons": ["valid email +40", "alive domain +30"],
    "found": true,
    "checkedAt": "2026-08-03T15:11:21.886Z"
}
```

The fourth row of that same run — "Shopify Inc" — shares its domain with the very first
row ("Shopify"), so it comes back as a free, rejected duplicate instead:

```json
{
    "name": "Shopify Inc",
    "email": null,
    "domain": "shopify.com",
    "phone": "+1 888-746-7439",
    "sourceUrl": null,
    "raw": { "title": "Shopify Inc", "website": "https://shopify.com/", "phone": "+1 888-746-7439" },
    "emailStatus": null,
    "emailFlags": [],
    "domainStatus": null,
    "techStack": [],
    "hotnessScore": 0,
    "scoreReasons": [],
    "rejected": true,
    "rejectReasons": ["duplicate"],
    "found": false,
    "checkedAt": "2026-08-03T15:11:21.885Z"
}
```

The final row of every run is a free summary, never billed — for this same 5-row input:

```json
{
    "rowType": "summary",
    "recordType": "run_advisory",
    "entityId": "lead-list-cleaner-run:ACTOR_RUN_ID",
    "totalIn": 5,
    "clean": 4,
    "rejected": 1,
    "billed": 0,
    "freeDemo": 4,
    "pushFailed": 0,
    "unbilledDelivered": 0,
    "rejectReasons": { "duplicate": 1 },
    "confidenceScore": 95,
    "confidenceBand": "high",
    "sourceEvidence": [{ "evidenceType": "RUN_COUNTERS", "totalIn": 5, "clean": 4, "rejected": 1 }],
    "dataGaps": [],
    "recommendedAction": "USE_CLEAN_ROWS",
    "actionPriority": "low",
    "safeToAutomate": true,
    "failureType": null,
    "retryable": false
}
```

The summary is a first-class commercial contract row rather than an empty separator: it carries a stable run-scoped `entityId`, observation timestamps, freshness, counter evidence, confidence, data gaps, an action, and failure/retry semantics. If a source read, delivery, or billing reconciliation is incomplete, its action changes to `REVIEW_RUN_RECONCILIATION`, the relevant gap is named, and `safeToAutomate` becomes `false`.

| Field | Meaning |
|---|---|
| `name` / `email` / `domain` / `phone` / `sourceUrl` | Normalized from whatever fields the source row actually had. `null` when that piece wasn't found anywhere in the row. |
| `emailStatus` | `valid` (syntax + MX both check out), `dead` (confirmed: bad syntax, the domain doesn't exist, or it can't receive mail at all), `risky` (a DNS resolver hiccup — not a confirmed answer either way), or `null` when there was nothing to check (no email found in the row, or `email` wasn't in `checks`). |
| `emailFlags` | `role:info` / `role:support` / etc. for shared-inbox addresses, `disposable` for a known temp-mail domain. Both are free bonus signals from a static list, not exhaustive. |
| `domainStatus` | `alive` (resolves, or has working mail servers), `dead` (confirmed: no DNS records at all), `unknown` (a resolver hiccup, never treated as dead), or `null` when there was nothing to check. |
| `hotnessScore` | 0-100, additive: `+40` valid email, `+30` alive domain, `+15` a non-role email address. `0` (with `scoreReasons: []`) if the `score` stage wasn't included in `checks`. |
| `scoreReasons` | Plain-text breakdown of exactly which bonuses added up to `hotnessScore`. |
| `techStack` | Reserved for a future check — currently always `[]`. The `+15` scoring bonus for tech signals is wired up and ready for when it ships. |
| `rejected` / `rejectReasons` | Present only on filtered-out rows. Reasons: `duplicate`, `dead-email`, `dead-domain`, `below-min-score`, `no-contact-signals` (no email AND no domain were found anywhere in the row — nothing here to ever contact). |
| `found` | `true` for a clean, billed lead; `false` for a rejected row or a processing error. |
| `raw` | The original row, completely untouched, for anything not extracted above. |
| `billed` + `freeDemo` (summary row) | How many of `clean` were actually charged vs. delivered free under the 25-lead demo cap. |
| `pushFailed` (summary row) | Always `0` on a normal run — rises only if a row genuinely couldn't be delivered to your dataset. |

#### Related tools

Related tools for adjacent workflows in B2B lead generation and data enrichment.

| Actor | What it does |
|---|---|
| [B2B Lead Enricher](https://apify.com/zinin/b2b-lead-enricher) | Pair it in the B2B lead generation and data enrichment workflow: Turn a list of company websites into sales-qualified lead cards: detected tech stack, a rough revenue... |
| [Company Profile Lookup](https://apify.com/zinin/company-lookup) | Pair it in the B2B lead generation and data enrichment workflow: Turn a domain or company name into one unified company card: website tech stack (CMS, ecommerce, key tech)... |
| [Company Registry Enricher](https://apify.com/zinin/company-registry-enricher) | Pair it in the B2B lead generation and data enrichment workflow: Turn a company name, LEI or UK company number into an official registry card: legal name, status,... |
| [Structured Data Extractor](https://apify.com/zinin/structured-extract) | Pair it in the B2B lead generation and data enrichment workflow: Turn any URL into clean structured JSON — title, description, image, JSON-LD, headings, links, emails and... |
| [Company Hiring Radar](https://apify.com/zinin/company-hiring-radar) | Pair it in the B2B lead generation and data enrichment workflow: Pull every open role a company is hiring for from its public job board (Greenhouse, Lever, Ashby) and turn... |

### FAQ / Limitations

**Does it need an API key, a login, or a browser?** No — just DNS lookups, and
optionally reading a dataset or URL you point it at. No proxies, no browser, no
scraping of its own.

**Why did a real company's email or domain come back `risky` / `unknown`?** That means
the DNS resolver didn't return a confirmed answer within the 12-second timeout — not
that the address or domain is actually bad. A confirmed `dead` verdict (no such domain,
or no mail servers at all) is a different, stronger signal, and the two are never
collapsed into each other.

**Why doesn't `domainStatus` flag a for-sale or placeholder page as dead?** It's a
live/dead call from DNS records alone — `alive`, `dead`, or `unknown`. This Actor
doesn't fetch the homepage or judge what's actually being served; a domain that
resolves comes back `alive` even if nobody's home yet.

**My lead has a non-Latin or accented email address and it came back `null`.** Email
extraction currently matches ASCII addresses only — an internationalized (Unicode)
local part or domain in the source text won't be picked up. Put it in a dedicated
`email` field and it will still be tried, but a genuinely non-ASCII address itself
won't match.

**Does a personal Gmail / Yahoo / Outlook / mail.ru address count as the lead's own
domain?** No. Free-mail providers are excluded from domain extraction on purpose — two
different companies both using `@gmail.com` must never collapse into one deduped
"lead," and a free-mail provider's own domain is always alive regardless of who's
behind the address.

**What happens if my source is empty?** A soft success, not a failure — you get one
free explanatory row telling you which input source was empty or missing, and why. No
charge beyond the automatic run-start fee.

**Who pays when this runs as an Integration, triggered by another Actor's run?**
Apify's own documentation doesn't spell this out explicitly for third-party
Integrations, and we haven't had this confirmed by Apify yet. Our working assumption,
until it is: the account that configured the Integration is billed. We'll update this
line the moment we have a confirmed answer.

**Can I call it from an AI agent?** Yes — standard Apify Actor, callable from the Apify
API, the SDK, or the Apify MCP server.

**What this is NOT.** Not a live mailbox check — no SMTP conversation, ever;
`emailStatus: valid` means the domain can receive mail, not that one specific inbox
exists. Not a company-enrichment tool — there's no tech-stack, firmographic or social
lookup here, just contact-signal validation and scoring. Not a web scraper — it never
fetches a lead's own website.

Found a wrong result, or need a check we don't run? Open an issue on this Actor's page.

***

Built by [zinin](https://apify.com/zinin). Questions? Telegram [@timzinin](https://t.me/timzinin).

## Commercial guide: Lead List Cleaner — Deduplication, DNS Hygiene, and Review Routing

> Normalize mixed lead rows, collapse deterministic duplicates, evaluate email-domain and website DNS evidence, score retained rows, and explain every rejection.

This guide is written for buyers, operators, analysts, and automation builders. It explains what the Actor observes, how to turn the Dataset into a controlled workflow, and where human verification remains mandatory.

### The decision this product supports

**Which source rows can enter the next manual lead-review stage, which should be deterministically removed, and which remain uncertain?**

The Actor reduces collection and first-pass triage work. It does not remove responsibility for source verification or authorize an external business action. The commercial value comes from a structured, repeatable evidence layer: stable identity, observation time, source evidence, confidence, gaps, recommended action, and failure semantics travel with the raw facts.

### Who uses it

| User | Value |
| --- | --- |
| Small-business marketers | Clean a spreadsheet or scraper export before paying for campaign execution. |
| Lead-generation agencies | Standardize mixed client datasets and deliver accepted, rejected, and quality-exception views. |
| Sales operations | Deduplicate by normalized domain or email and preserve rejection reasons for audit. |
| Apify Integration users | Attach a cleaning stage to another Actor Dataset while keeping source permissions explicit. |
| CRM administrators | Stage normalized leads before promoting fields into canonical company and contact records. |
| Automation builders | Use exact rejection, confidence, action, failure, and retry fields instead of a silent filter. |

### Input contract

| Input field | How to use it |
| --- | --- |
| items | Direct array of raw lead objects. It has highest priority and accepts heterogeneous source shapes. |
| datasetId | Dataset selected through the Apify resource picker. The picker grants this run READ access; a pasted identifier alone is not equivalent. |
| datasetUrl | Public JSON-array URL used only when direct items and datasetId are absent. |
| checks | Ordered subset of dedupe, email, domain, and score stages. Disabled checks remain explicit gaps. |
| minScore | Buyer-controlled 0–100 threshold. Falling below it is a policy rejection, not proof that the company is poor quality. |
| maxRows | Hard 1–100,000 processing cap used to bound run size and cost. |

#### Recommended first Input

```json
{
  "items": [
    {
      "name": "Example Co",
      "email": "sales@example.com",
      "website": "https://example.com/"
    },
    {
      "name": "Example duplicate",
      "website": "https://www.example.com/"
    }
  ],
  "checks": [
    "dedupe",
    "email",
    "domain",
    "score"
  ],
  "minScore": 0,
  "maxRows": 100
}
```

Start with this bounded example, inspect every Dataset field, and only then expand the scope. Input limits are product controls, not inconveniences: they make cost, completeness, and error handling visible.

### Field dictionary

| Field or group | Meaning |
| --- | --- |
| entityId, inputRef, observedAt | Normalized lead identity, source reference, and check time. |
| name, email, domain, phone, sourceUrl, raw | Normalized fields plus the original source row for traceability. |
| emailStatus, mxRecords, provider, emailFlags | Email-domain evidence and static risk flags; mailbox existence remains unchecked. |
| domainStatus, verificationLevel, mailboxChecked | Website/mail-domain status and exact verification level. |
| hotnessScore, scoreReasons, leadQualityBand | Transparent lead-data score and routing band, separate from evidence confidence. |
| rejected, rejectReasons, negativeSignals | Deterministic exclusion reasons and other risks that must survive export. |
| confidenceScore, confidenceBand, confidenceRisks | Support for the classification after disabled, unknown, or failed checks. |
| sourceEvidence | Email-domain, domain-DNS, and source-row evidence available for the normalized lead. |
| recommendedAction, actionPriority, safeToAutomate | Narrow next step such as drop duplicate, suppress confirmed dead contact, or review. |
| failureType, retryable, recommendation | Run or row failure semantics, including budget exhaustion. |

#### Common decision fields

| Field | Operational meaning |
| --- | --- |
| recordType | The semantic row family. Use it to distinguish a business result from an advisory or terminal record. |
| schemaVersion | Version of the additive decision-intelligence contract. Pin or validate it in strict consumers. |
| entityId | Stable entity identity for deduplication and joins. It is not necessarily a legal identifier. |
| inputRef | The relevant submitted input reference after normalization. |
| observedAt | When the Actor observed or finalized the evidence. It is not necessarily the source publication time. |
| firstSeenAt and lastSeenAt | Always-emitted observation boundaries. Stateful monitors use the compatible baseline/current boundary. Stateless rows set both equal to observedAt for the current run; that equality does not establish historical tenure. |
| freshness | A structured statement about evidence age or availability, not a prediction. Its basis and age unit follow the source-specific field definition. |
| eventId | For monitors, the stable identity of one observed transition or monitor outcome. It is distinct from entityId. |
| before and after | For monitors, the bounded comparable snapshots used for the decision. Null means that side of a comparison was not honestly available. |
| changedFields and changeFlags | Machine-readable monitor deltas and normalized change labels. Empty arrays mean no supported changed field was established, not that every possible real-world fact stayed constant. |
| materialityScore and materialityBand | Magnitude of an observed monitor change when the Actor can calculate it. Materiality is separate from evidence confidence and may be unknown when the source lacks the required facts. |
| confidenceScore | Evidence support on a 0–100 scale. It is separate from materiality, lead score, or business value. |
| confidenceBand | Readable high/medium/low/unknown grouping of evidence support. |
| confidenceReasons | Observed facts that raise confidence. |
| confidenceRisks | Missing, partial, ambiguous, inferred, or conflicting aspects that reduce confidence. |
| confidenceConflict | Explicit consistency warning when structured evidence does not reconcile. |
| sourceEvidence | Source-linked observations supporting the row. Preserve this during export. |
| dataGaps | Important evidence the Actor did not observe or cannot establish. Keep these gaps visible in CRM, spreadsheet, and automation exports. |
| negativeSignals | Machine-readable risks or gaps. A negative signal is not automatically a negative business outcome. |
| recommendedAction | Bounded review label produced from the available evidence. |
| actionPriority | Suggested queue priority, not urgency guaranteed by the source. |
| actionReason | Plain-language explanation for the recommended action. |
| safeToAutomate | Whether the narrow recommended action is deterministic enough for automation. Organizational policy still applies. |
| failureType | Normalized terminal or partial failure classification. Null means no classified failure. |
| retryable | Whether a later retry may legitimately change an operationally incomplete result. |
| recommendation | Human-readable handling guidance, especially for terminal rows. |

### Evidence, confidence, and honest boundaries

#### What the evidence supports

- Duplicate classification follows a normalized key within the current run.
- Email valid means syntax and email-domain MX evidence, not a live mailbox check.
- Dead email or domain requires a conclusive rule or DNS outcome; resolver uncertainty stays unknown.
- The score is a deterministic sum of visible reasons and is not evidence confidence.
- Rejected and summary rows remain visible so cleaning does not hide what happened to the source batch.

#### What this Actor never claims

- The Actor does not prove mailbox existence, consent, identity, seniority, buying intent, or reply likelihood.
- It does not guarantee arbitrary source rows contain the correct company or contact.
- It does not interpret a buyer-configured score threshold as an objective quality standard.
- It does not make a private Dataset public or bypass missing Dataset permissions.
- It does not perform technology detection even though the reserved techStack field exists for compatibility.

#### Reading data gaps correctly

A data gap is part of the result. Nulls, partial flags, confidence risks, source failures, and unavailable fields must survive export. Removing these fields makes the remaining facts look more complete than they are. When two sources conflict or a required identity cannot be proven, lower confidence and keep `safeToAutomate=false`.

#### Source evidence is not permission

A public source proves only that a value or statement was observable at the recorded time and URL. It does not establish consent, contractual rights, legal status, accuracy after observation, or authorization for a downstream action. Your organization remains responsible for source terms, privacy rules, outreach policy, retention, and human review.

### Decision policy and action routing

| Action | How to use it |
| --- | --- |
| DROP\_DUPLICATE | Remove the later duplicate from this run while retaining its rejection row and original source evidence. |
| SUPPRESS\_DEAD\_CONTACT | Exclude a conclusively dead email-domain or website domain from the active export. |
| DROP\_NO\_CONTACT\_SIGNAL | Remove a row that contains neither an email nor a domain under the normalization policy. |
| HOLD\_BELOW\_THRESHOLD | Keep the row outside the accepted export because it missed the configured score, without treating that as a universal negative. |
| VERIFY\_MAILBOX\_THEN\_EXPORT | The domain evidence is viable; verify the named mailbox and policy before activation. |
| REVIEW\_BEFORE\_EXPORT | Inspect uncertain or disabled checks before promoting the row. |
| RAISE\_BUDGET\_AND\_RESUME | Resume only the unfinished source scope after an authorized budget change. |

#### Confidence is not attractiveness

`confidenceScore` answers “how strongly does the available evidence support this factual classification?” It does not answer “how valuable is this lead, property, account, or address?” A high-confidence negative fact may be commercially uninteresting; a low-confidence positive signal may deserve research but not action. Keep the concepts separate in dashboards, exports, and CRM fields.

#### Why safeToAutomate is conservative

`safeToAutomate` is intentionally false whenever the next step could amplify an uncertain inference. It may be true only for narrow deterministic actions explicitly supported by the row, such as suppressing an email with invalid syntax. A true value does not waive legal, privacy, consent, contractual, or organizational rules.

#### Retry policy

- Retry when `retryable=true` and the failure is operational, such as a temporary source or DNS problem.
- Do not endlessly retry deterministic invalid input, policy refusal, or confirmed absence.
- A retry must preserve the original input reference and must not create duplicate downstream actions.
- Budget exhaustion is not negative evidence about the entity. Resume only the unprocessed scope with an authorized budget.
- A failed Actor run is an operational event. Never transform it into “no listing,” “no contact,” “bad lead,” or “invalid email.”

### Commercial use-case playbooks

#### 1. Google Maps lead cleanup

**Goal.** Point the Actor at an approved business Dataset, normalize inconsistent fields, deduplicate domains, and keep rejected rows in a separate audit view.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

#### 2. CSV-to-CRM staging

**Goal.** Convert the CSV to JSON, submit direct items, map entityId to a staging key, and require mailbox verification before activation.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

#### 3. Apify Integration pipeline

**Goal.** Select the triggering Dataset through the resource picker, test permissions with a small run, and route source-access failures as operational exceptions.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

#### 4. Agency client delivery

**Goal.** Deliver accepted, rejected, and summary views with exact checks and threshold used. Do not promise that accepted addresses are deliverable mailboxes.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

#### 5. Duplicate-cost control

**Goal.** Run dedupe first so later repeated company rows do not create redundant paid work or duplicate CRM actions.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

#### 6. Suppression hygiene

**Goal.** Use safe deterministic negative classifications while preserving the observation time and reason for reversible review.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

#### 7. Threshold experiment

**Goal.** Compare accepted outcomes in the CRM and adjust minScore as a downstream business policy, not as a truth label.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

#### 8. Resolver retry queue

**Goal.** Retry unknown DNS outcomes without relabeling them dead and without rerunning deterministic rejected rows.

**Recommended runbook.**

1. Define the submitted cohort and write down why it is in scope.
2. Start with the smallest useful Input and preserve the exact run ID.
3. Inspect the Dataset overview before exporting anything.
4. Check `failureType`, `retryable`, completeness indicators, and `confidenceBand`.
5. Open the relevant `sourceEvidence` or source URL for material rows.
6. Apply the recommended action as a review label, not as an instruction to contact, buy, delete, accuse, or publish.
7. Record the analyst's final disposition in the destination system.

**Do not skip.** An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

### Integration recipes

All examples use placeholders. Keep the Apify token in a secret manager and never write it into a Dataset, README, screenshot, or client-side application.

#### cURL: start a run and wait briefly

```bash
curl -sS -X POST 'https://api.apify.com/v2/acts/zinin~lead-list-cleaner/runs?waitForFinish=60' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"items":[{"name":"Example Co","email":"sales@example.com","website":"https://example.com/"},{"name":"Example duplicate","website":"https://www.example.com/"}],"checks":["dedupe","email","domain","score"],"minScore":0,"maxRows":100}'
```

The run response includes `defaultDatasetId`. Read clean JSON rows with:

```bash
curl -sS "https://api.apify.com/v2/datasets/$DEFAULT_DATASET_ID/items?clean=true&format=json" \
  -H "Authorization: Bearer $APIFY_TOKEN"
```

#### JavaScript with apify-client

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const input = {
  "items": [
    {
      "name": "Example Co",
      "email": "sales@example.com",
      "website": "https://example.com/"
    },
    {
      "name": "Example duplicate",
      "website": "https://www.example.com/"
    }
  ],
  "checks": [
    "dedupe",
    "email",
    "domain",
    "score"
  ],
  "minScore": 0,
  "maxRows": 100
};
const run = await client.actor('zinin/lead-list-cleaner').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems({ clean: true });

for (const row of items) {
    console.log({
        entityId: row.entityId,
        confidenceBand: row.confidenceBand,
        recommendedAction: row.recommendedAction,
        safeToAutomate: row.safeToAutomate,
        failureType: row.failureType,
    });
}
```

#### Python with apify-client

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("zinin/lead-list-cleaner").call(run_input={
    "items": [
        {
            "name": "Example Co",
            "email": "sales@example.com",
            "website": "https://example.com/"
        },
        {
            "name": "Example duplicate",
            "website": "https://www.example.com/"
        }
    ],
    "checks": [
        "dedupe",
        "email",
        "domain",
        "score"
    ],
    "minScore": 0,
    "maxRows": 100
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items(clean=True):
    print({
        "entityId": row.get("entityId"),
        "confidenceBand": row.get("confidenceBand"),
        "recommendedAction": row.get("recommendedAction"),
        "safeToAutomate": row.get("safeToAutomate"),
        "failureType": row.get("failureType"),
    })
```

#### Apify MCP call

```json
{
  "name": "call-actor",
  "arguments": {
    "actor": "zinin/lead-list-cleaner",
    "input": {
      "items": [
        {
          "name": "Example Co",
          "email": "sales@example.com",
          "website": "https://example.com/"
        },
        {
          "name": "Example duplicate",
          "website": "https://www.example.com/"
        }
      ],
      "checks": [
        "dedupe",
        "email",
        "domain",
        "score"
      ],
      "minScore": 0,
      "maxRows": 100
    }
  }
}
```

#### Generic webhook consumer policy

1. Trigger on a terminal Actor run event.
2. Confirm the run status is `SUCCEEDED` before reading business rows.
3. Retrieve rows from `defaultDatasetId`.
4. Reject or quarantine rows whose `failureType` is non-null unless your policy explicitly handles that failure.
5. Send `safeToAutomate=false` rows to a human-review queue.
6. Store `entityId`, `observedAt`, `sourceEvidence`, confidence, action, and the Apify run ID together.
7. Make retries idempotent by keying the destination on the stable entity ID plus the intended observation or event identity.

### Where this fits in a practical stack

| Destination | Recommended pattern |
| --- | --- |
| Apify Console | Use the visual Input form, start the run, then open the default Dataset overview. This is the fastest path for a one-off review and the best place to inspect evidence before automating anything. |
| Apify API | POST JSON input to the Actor run endpoint, wait or poll for completion, then read the default Dataset through the URL returned by the run object. |
| JavaScript client | Use apify-client from a Node.js service, pass the same JSON object as the Console Input, and preserve the returned run and Dataset IDs in your own audit log. |
| Python client | Use apify-client in a Python enrichment job, iterate Dataset items, and route rows by recommendedAction, confidenceBand, failureType, and retryable. |
| Make | Start the Actor from a scenario, wait for the run, retrieve Dataset items, filter unsafe or low-confidence rows, then insert review-ready rows into the destination application. |
| Zapier | Use an Apify run action or webhook trigger, fetch Dataset items, apply a Filter step, and send only review-approved fields into the next sales or operations step. |
| n8n | Use HTTP Request or Apify nodes, branch on failureType and retryable, keep a manual-review lane for safeToAutomate=false, and write sourceEvidence together with the business fields. |
| Google Sheets | Export the Dataset directly or append rows from an automation. Keep stable entityId as a hidden key so reruns update the correct record instead of creating ambiguous duplicates. |
| Airtable | Map entityId to a primary or deduplication field, store confidence and evidence in separate columns, and expose recommendedAction as the triage view. |
| Webhook | Configure an Apify webhook for terminal run states, retrieve the Dataset after SUCCEEDED, and treat FAILED or TIMED-OUT runs as operational events rather than negative business evidence. |

#### A safe automation shape

The Actor is a collection and decision-support component. A production workflow should keep raw evidence, decision metadata, and business action in distinct layers:

1. **Collect:** run the Actor with explicit bounded input.
2. **Validate:** require a successful run and schema-valid Dataset rows.
3. **Triage:** branch on `failureType`, `retryable`, `confidenceBand`, and `safeToAutomate`.
4. **Review:** open source evidence for rows that may affect a person, campaign, investment, compliance decision, or customer record.
5. **Act:** execute only the action approved by your own policy and authorized operator.
6. **Audit:** retain run ID, Dataset ID, observation time, input reference, source evidence, and the final human decision.

This separation prevents a common automation error: turning “data was observed” into “a business action is justified.”

### Operating guide

#### Before the first production run

1. Write the business question in one sentence: **Which source rows can enter the next manual lead-review stage, which should be deterministically removed, and which remain uncertain?**
2. Confirm every submitted input is within your authorized scope.
3. Use the prefilled small example and review all returned row types.
4. Map stable identifiers, confidence, evidence, actions, gaps, failure, and retry fields into the destination.
5. Establish a human owner for review exceptions.
6. Set a run budget and output bound appropriate to the test.
7. Verify that secrets are stored only in the platform or workflow secret manager.

#### After every scheduled run

1. Check terminal run status and logs.
2. Compare the number of submitted entities, produced business rows, and advisory rows.
3. Review partial, unknown, conflict, and low-confidence buckets.
4. Inspect a sample of source evidence, including at least one positive and one negative result.
5. Confirm the destination deduplicated on the intended stable key.
6. Verify that no downstream action was triggered from an error row.
7. Track cost per useful reviewed row rather than cost per raw request alone.

#### Production monitoring signals

Monitor source-unavailable rate, partial-row rate, low-confidence share, missing evidence, retry volume, run duration, Dataset row count, and spend. A sudden shift may indicate source drift, input drift, or an upstream outage. Stop automation and investigate before accepting a new pattern as business truth.

#### Cost control

Use direct items with five to twenty representative rows first, including a duplicate, missing-contact row, malformed email, and known domain. Confirm accepted/rejected partitions before selecting a large Dataset. Live Apify event pricing is authoritative.

Use `maxTotalChargeUsd` when calling a monetized Actor if your workflow supports it. Treat a buyer-set cap as a hard safety boundary. If the cap stops work, the unfinished items remain unprocessed; they do not become negative results.

### Review templates and quality reporting

#### Row-review worksheet

For every material row, an analyst should be able to answer the following without relying on memory or an unstated assumption:

1. What submitted entity or query does this row refer to?
2. Is it a business result, a baseline/advisory row, a partial observation, or a failure?
3. Which exact source evidence supports the headline fact?
4. When was the evidence observed, and is there a different source publication time?
5. Which fields are direct observations, which are normalized, and which are deterministic derivations?
6. What important evidence is null, missing, partial, ambiguous, or conflicting?
7. Does confidence describe evidence support only, or has someone incorrectly treated it as business value?
8. What recommended action is present, and what additional verification does its reason require?
9. Is the narrow action marked safe to automate? If yes, does organizational policy also permit it?
10. What final human disposition was made, by whom, and from which run and Dataset item?

#### Field-group review prompts

##### 1. `entityId, inputRef, observedAt`

**Contract meaning:** Normalized lead identity, source reference, and check time.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 2. `name, email, domain, phone, sourceUrl, raw`

**Contract meaning:** Normalized fields plus the original source row for traceability.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 3. `emailStatus, mxRecords, provider, emailFlags`

**Contract meaning:** Email-domain evidence and static risk flags; mailbox existence remains unchecked.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 4. `domainStatus, verificationLevel, mailboxChecked`

**Contract meaning:** Website/mail-domain status and exact verification level.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 5. `hotnessScore, scoreReasons, leadQualityBand`

**Contract meaning:** Transparent lead-data score and routing band, separate from evidence confidence.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 6. `rejected, rejectReasons, negativeSignals`

**Contract meaning:** Deterministic exclusion reasons and other risks that must survive export.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 7. `confidenceScore, confidenceBand, confidenceRisks`

**Contract meaning:** Support for the classification after disabled, unknown, or failed checks.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 8. `sourceEvidence`

**Contract meaning:** Email-domain, domain-DNS, and source-row evidence available for the normalized lead.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 9. `recommendedAction, actionPriority, safeToAutomate`

**Contract meaning:** Narrow next step such as drop duplicate, suppress confirmed dead contact, or review.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

##### 10. `failureType, retryable, recommendation`

**Contract meaning:** Run or row failure semantics, including budget exhaustion.

**Reviewer prompts:** Is the value present? Does its type match the schema? Is it supported by sourceEvidence or a documented deterministic transformation? Is any null being silently converted into a default? Would the value still mean the same thing after CSV export? Does the destination preserve the related confidence and gap fields?

#### Weekly quality report

Create a recurring internal report with these measures. The report is about pipeline health, not market demand unless the source contract explicitly measures demand.

| Metric | Why it matters | Investigate when |
| --- | --- | --- |
| Submitted inputs | Defines the actual denominator and scope of the run. | The count differs from the approved batch or schedule. |
| Business result rows | Shows how many usable observations were produced. | The rate changes sharply without an input explanation. |
| Advisory/failure rows | Prevents operational failures from disappearing in a results-only dashboard. | Any terminal class grows or is unmapped. |
| Partial-result rate | Measures incomplete source coverage or configured truncation. | It rises, or analysts stop seeing the partial warning. |
| Low-confidence rate | Shows the share of rows requiring more evidence. | It rises by source, cohort, or input pattern. |
| Retryable failure rate | Distinguishes temporary operational issues from deterministic outcomes. | Retries repeat without improving evidence. |
| Evidence-link coverage | Confirms material facts remain traceable after export. | Links or evidence objects are missing from delivered records. |
| Safe-automation share | Shows how little or much of the workflow can be deterministic. | A mapping change makes unsafe actions appear safe. |
| Manual-review backlog | Measures whether human verification capacity matches collection volume. | Rows age beyond the campaign or decision window. |
| Duplicate destination writes | Tests idempotency and stable identity mapping. | The same entity/run creates multiple external actions. |
| Cost per reviewed useful row | Relates platform spend to approved, decision-useful output. | Raw volume rises but reviewed utility falls. |
| Source-drift exceptions | Detects changed markup, response shape, policy, or source availability. | A new unknown pattern survives more than one bounded check. |

#### Client-facing delivery note template

Use a note like this when delivering exports to a client or another team:

> This Dataset contains bounded public-source observations produced by the Apify Actor for the submitted Input. Each row includes observation time, evidence confidence, recommended review action, and explicit gaps where available. A positive row is not proof of buyer intent, permission, legal status, future outcome, or any fact listed in the Actor's “never claims” section. Partial and failure rows are included so coverage is not overstated. Validate material rows at their source before acting.

Add the Actor URL, run URL, Dataset URL, build/version, exact Input scope, observation window, pricing model observed for the run, reviewer name, and date of approval.

#### CRM disposition vocabulary

Keep collection results and sales dispositions separate. A practical downstream vocabulary is:

- `needs_evidence_review`: useful signal exists but a reviewer has not approved it.
- `needs_identity_review`: entity or ownership association is not sufficiently proven.
- `needs_policy_review`: contact, privacy, suppression, legal, or contractual policy must be checked.
- `approved_for_research`: an analyst may perform more research; this is not approval for outreach.
- `approved_for_authorized_action`: a named operator approved one specific action under the organization's policy.
- `retry_operational_failure`: the source or infrastructure failed and a bounded retry is appropriate.
- `closed_no_supported_signal`: the completed bounded check found no supported signal; this is not a universal negative fact.
- `closed_out_of_scope`: the input should not have entered this workflow.

Never overwrite `recommendedAction` with the CRM disposition. The first is Actor-produced decision support; the second is your organization's accountable decision.

#### Sampling plan

For a new workflow, review every row in the first small run. When the contract is understood, sample all failure and partial rows plus a representative set of high-, medium-, and low-confidence results. Re-expand to full review whenever the source changes, the schema version changes, a new input cohort is introduced, the error distribution shifts, or a downstream user reports an unexplained result.

#### Change-management record

When you change field mappings or automation policy, record:

1. Previous mapping or rule.
2. New mapping or rule.
3. Actor build/version and schemaVersion used for validation.
4. Test run and Dataset URLs.
5. Positive, negative, partial, retry, and budget fixtures inspected.
6. Security and privacy review outcome.
7. Approver and activation time.
8. Rollback condition and responsible operator.

This makes a commercial data workflow supportable. Without the record, a later operator cannot distinguish a real source change from an undocumented mapping change.

### Delivery patterns for marketing and small-business teams

#### One-off research

Run the Actor in Console, inspect the overview table, open evidence for each material row, and export only the approved subset. Record the run URL in the client or campaign notes.

#### Recurring watch or hygiene job

Use an Apify schedule. Write rows into a staging table keyed by `entityId`. Compare current and previous observations only when the Actor supplies valid state or your own pipeline implements an explicit comparable baseline. Never infer a change from a failed run.

#### Agency client delivery

Deliver three views: business results, evidence/quality exceptions, and operational failures. Include the run URL, observation time, configured scope, and a plain-language statement of what the Actor does not prove. This makes the deliverable auditable and reduces disputes caused by overclaiming.

#### CRM enrichment

Write into staging fields first. A human or approved policy promotes values into canonical CRM fields. Keep raw source values separate from normalized and decision fields, and do not replace a verified value with a lower-confidence observation.

#### AI-assisted review

An LLM can summarize rows, but it must receive the evidence, confidence risks, negative signals, and limitations. Require citations to sourceEvidence and prohibit invented identity, intent, legal, funding, mailbox, valuation, or availability facts.

### Buyer and operator acceptance checklist

Use this checklist before calling the workflow production-ready.

#### Product fit

- \[ ] The business question matches: **Which source rows can enter the next manual lead-review stage, which should be deterministically removed, and which remain uncertain?**
- \[ ] The submitted entities were selected through an authorized process.
- \[ ] A human owner understands the positive, negative, partial, and failure row types.
- \[ ] The team accepts the boundaries listed in “What this Actor never claims.”
- \[ ] The destination keeps evidence confidence separate from business scoring.

#### Input and run controls

- \[ ] `items` is explicitly reviewed and bounded.
- \[ ] `datasetId` is explicitly reviewed and bounded.
- \[ ] `datasetUrl` is explicitly reviewed and bounded.
- \[ ] `checks` is explicitly reviewed and bounded.
- \[ ] `minScore` is explicitly reviewed and bounded.
- \[ ] `maxRows` is explicitly reviewed and bounded.
- \[ ] The first production-like run uses a small representative sample.
- \[ ] A maximum charge or internal spend alert is configured where appropriate.
- \[ ] The workflow records Actor ID, build/version, run ID, Dataset ID, and input hash.

#### Data handling

- \[ ] `entityId` is mapped to an idempotent destination key.
- \[ ] `observedAt` and source-specific time fields remain distinct.
- \[ ] `sourceEvidence`, gaps, and nulls are preserved.
- \[ ] Advisory and failure rows cannot enter the positive-results lane.
- \[ ] Low-confidence and partial rows have a visible manual-review view.
- \[ ] Retention and deletion rules match the type of data collected.

#### Action safety

- \[ ] `recommendedAction` is treated as a review label.
- \[ ] `safeToAutomate=false` blocks automatic external action.
- \[ ] Consent, suppression, legal, contractual, and platform rules are evaluated downstream.
- \[ ] A reviewer can trace a material action back to source evidence and run metadata.
- \[ ] Retry logic cannot duplicate a downstream action.

#### Ongoing quality

- \[ ] The team monitors failure, retry, partial, low-confidence, and empty-result rates.
- \[ ] A source-drift threshold pauses the workflow for inspection.
- \[ ] Sample evidence is manually reviewed on a recurring basis.
- \[ ] Cost per useful reviewed row is measured.
- \[ ] Documentation and field mappings are updated when schemaVersion changes.

### Frequently asked questions

#### Is this a database?

No. It is an on-demand observation tool. Each run collects or evaluates the submitted scope and records evidence at that time.

#### Does a found row prove commercial interest?

No. A found row proves only the factual observation described by its fields. Buyer intent is never inferred.

#### Can I automatically contact every result?

No. Use recommendedAction as triage, verify the evidence and identity, and apply your own consent, privacy, suppression, and outreach rules.

#### Why is safeToAutomate often false?

Because a useful observation can still require identity, context, legal, or source verification before action. Conservative routing prevents false certainty from scaling.

#### What should I do with low confidence?

Open confidenceRisks and sourceEvidence, close the important gap, or keep the row in a manual queue. Do not hide the confidence field.

#### What does partial mean?

The Actor obtained some usable evidence but could not support a complete observation of the configured scope. Partial is not the same as empty.

#### What is a confirmed zero?

Only an explicit source or deterministic rule can support a confirmed absence. An outage, truncation, or unreadable response is not a zero.

#### Should I retry every failure?

No. Retry only when retryable is true. Invalid input, policy refusal, or deterministic classification should be corrected or handled, not looped.

#### Can I delete failure rows?

You can exclude them from a business-results view, but retain them in operational logs so Dataset completeness and retry decisions stay explainable.

#### How should I deduplicate?

Use entityId for the entity and, for stateful monitors, eventId for the observed transition. Also retain the Apify run ID.

#### Can I treat confidence as conversion probability?

No. Confidence measures evidence support, not purchase probability, revenue, suitability, or expected return.

#### Can I change the recommended action?

Yes. It is an explainable default. Your downstream policy can be stricter, and should encode organization-specific authorization and risk tolerance.

#### How do I estimate cost?

Run the smallest representative input, inspect live event prices and run usage in Apify, then model the number of billable result events. The live pricing panel is authoritative.

#### Why use a small prefill?

It produces a cheap, fast, inspectable first run and reduces the chance of scaling a wrong input or workflow assumption.

#### Can I schedule it?

Yes. Use an Apify schedule, but make the destination idempotent and review changes in failure, partial, and confidence rates.

#### Can I export CSV or Excel?

Yes. Apify Datasets support common export formats. JSON is recommended when you need nested evidence and decision fields.

#### Can I send results to Sheets or Airtable?

Yes. Preserve entityId, confidence, evidence, gaps, actions, and failure fields instead of mapping only the headline value.

#### Can I use it from Make, Zapier, or n8n?

Yes. Start the Actor, wait for a successful terminal state, read Dataset items, then branch on decision and failure fields.

#### Can an LLM consume the output?

Yes, but pass the structured evidence and limitations together. Instruct the model not to invent missing facts and to cite sourceEvidence.

#### What happens when a source changes?

The run may become partial, unavailable, or fail validation. Monitor these rates and inspect logs before treating changed output as a real-world shift.

#### Does public mean unrestricted?

No. Public visibility does not remove source terms, privacy obligations, retention rules, or the need for a legitimate downstream purpose.

#### Is a source URL permanent?

Not necessarily. Store observation time and material facts because web content can change or disappear.

#### Can I rely on one row for a high-stakes decision?

No. High-stakes legal, financial, employment, compliance, safety, or personal decisions require appropriate primary evidence and qualified review.

#### How do I report a suspected parsing issue?

Provide the Actor run ID, a redacted input, affected field, expected source evidence, and whether the issue reproduces. Never include tokens or private data.

#### What does success mean?

An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

### Support information to include with an issue

Provide the public Actor name, Apify run ID, Dataset item index or stable entity ID, a redacted Input, the relevant source URL, expected behavior, observed behavior, and whether retrying produced the same result. Do not include an Apify token, API key, private customer record, or unnecessary personal data.

### Final interpretation rule

An accepted row passed the configured normalization, checks, and threshold. It remains a lead-data candidate, not a verified person, consent record, mailbox, or buyer.

# Actor input Schema

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

Pick another Actor's dataset to clean (e.g. a Google Maps or lead-scraper run). Ignored when 'items' is set. Picking it here — not just pasting the ID — is what grants this run's token READ access to that dataset. When this Actor is added as an Integration on another Actor, this field is filled in automatically from the triggering run.

## `datasetUrl` (type: `string`):

Public URL returning a JSON array of lead rows. Used only when both 'items' and 'datasetId' are empty.

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

Raw lead rows to clean directly, one object per lead — any shape works (Google Maps scraper export, CSV turned into JSON, anything with an email/website/phone somewhere in it). Takes priority over 'datasetId' and 'datasetUrl'.

## `checks` (type: `array`):

Pipeline stages to run, in order. 'dedupe' collapses rows sharing the same domain/email. 'email' verifies syntax + MX. 'domain' verifies the domain is alive. 'score' computes the hotness score used for the 'minScore' cutoff.

## `minScore` (type: `integer`):

Drop leads scoring below this threshold (0-100) from the clean, billed output. They still show up as free rejected rows with a reason.

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

Safety cap on how many input rows this run will process, regardless of source.

## Actor input object example

```json
{
  "datasetId": "your_dataset_id",
  "datasetUrl": "https://api.apify.com/v2/datasets/YOUR_PUBLIC_DATASET_ID/items?clean=true",
  "items": [
    {
      "name": "Example Co",
      "email": "sales@example.com",
      "website": "https://example.com/"
    }
  ],
  "checks": [
    "dedupe",
    "email",
    "domain",
    "score"
  ],
  "minScore": 40,
  "maxRows": 100
}
```

# Actor output Schema

## `results` (type: `string`):

API URL for the default dataset items produced by this run.

# 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": [
        {
            "title": "Shopify",
            "categoryName": "Software company",
            "website": "https://www.shopify.com/",
            "phone": "+1 888-746-7439",
            "url": "https://www.google.com/maps/place/Shopify"
        },
        {
            "name": "HubSpot",
            "email": "press@hubspot.com",
            "phoneNumber": "+1 888-482-7768",
            "link": "https://www.hubspot.com/"
        },
        {
            "companyName": "Zoom Video Communications",
            "description": "Video conferencing platform. Contact us at info@zoom.us for enterprise sales.",
            "domain": "zoom.us"
        },
        {
            "title": "Shopify Inc",
            "website": "https://shopify.com/",
            "phone": "+1 888-746-7439"
        },
        {
            "title": "Stripe",
            "categoryName": "Payment processor",
            "website": "https://stripe.com/",
            "phone": "+1 888-926-2289",
            "emails": [
                "support@stripe.com"
            ]
        }
    ],
    "checks": [
        "dedupe",
        "email",
        "domain",
        "score"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/lead-list-cleaner").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": [
        {
            "title": "Shopify",
            "categoryName": "Software company",
            "website": "https://www.shopify.com/",
            "phone": "+1 888-746-7439",
            "url": "https://www.google.com/maps/place/Shopify",
        },
        {
            "name": "HubSpot",
            "email": "press@hubspot.com",
            "phoneNumber": "+1 888-482-7768",
            "link": "https://www.hubspot.com/",
        },
        {
            "companyName": "Zoom Video Communications",
            "description": "Video conferencing platform. Contact us at info@zoom.us for enterprise sales.",
            "domain": "zoom.us",
        },
        {
            "title": "Shopify Inc",
            "website": "https://shopify.com/",
            "phone": "+1 888-746-7439",
        },
        {
            "title": "Stripe",
            "categoryName": "Payment processor",
            "website": "https://stripe.com/",
            "phone": "+1 888-926-2289",
            "emails": ["support@stripe.com"],
        },
    ],
    "checks": [
        "dedupe",
        "email",
        "domain",
        "score",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/lead-list-cleaner").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": [
    {
      "title": "Shopify",
      "categoryName": "Software company",
      "website": "https://www.shopify.com/",
      "phone": "+1 888-746-7439",
      "url": "https://www.google.com/maps/place/Shopify"
    },
    {
      "name": "HubSpot",
      "email": "press@hubspot.com",
      "phoneNumber": "+1 888-482-7768",
      "link": "https://www.hubspot.com/"
    },
    {
      "companyName": "Zoom Video Communications",
      "description": "Video conferencing platform. Contact us at info@zoom.us for enterprise sales.",
      "domain": "zoom.us"
    },
    {
      "title": "Shopify Inc",
      "website": "https://shopify.com/",
      "phone": "+1 888-746-7439"
    },
    {
      "title": "Stripe",
      "categoryName": "Payment processor",
      "website": "https://stripe.com/",
      "phone": "+1 888-926-2289",
      "emails": [
        "support@stripe.com"
      ]
    }
  ],
  "checks": [
    "dedupe",
    "email",
    "domain",
    "score"
  ]
}' |
apify call zinin/lead-list-cleaner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/lead-list-cleaner"
        }
    }
}

```

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/rxPSjoRfq3pODDMe6/builds/PHslDioacSvY6LxhN/openapi.json
