# HubSpot Lead Pusher — Auto-Create Contacts, Companies & Deals (`ryanclinton/hubspot-lead-pusher`) Actor

Push scraped leads directly into HubSpot CRM as contacts, companies, and deals. Feed in lead data from any Apify actor or paste it manually, and HubSpot Lead Pusher handles the mapping, deduplication, and batch upsert via the HubSpot API.

- **URL**: https://apify.com/ryanclinton/hubspot-lead-pusher.md
- **Developed by:** [Ryan Clinton](https://apify.com/ryanclinton) (community)
- **Categories:** Integrations, Lead generation
- **Stats:** 4 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 lead pusheds

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?

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

## HubSpot Lead Pusher

#### What this actually is (plain English)

HubSpot Lead Pusher is a **CRM ingestion engine** that decides which leads should be pushed, prevents duplicates, and writes clean, fully-associated records into HubSpot in one run.

It replaces CSV imports, Zapier routing logic, and manual CRM cleanup with a single deterministic pipeline.

> **This actor converts raw signals into deterministic, automation-ready CRM-sync decisions (HubSpot).**

**Apify GTM Pipeline:** Scrape → Enrich → Verify → Score → Research → Push to CRM
**Role of this actor:** CRM ingestion + sync layer (HubSpot).

#### Quick answers

- **Push scraped leads into HubSpot without duplicates** — companies are upserted by domain, contacts by email, deals deduped by name in a single run.
- **Zapier alternative for HubSpot lead ingestion** — replaces multi-step Zaps with one deterministic pipeline that filters, deduplicates, and associates records in one run.
- **Filter bad leads before pushing to HubSpot** — pre-push `qualityGate` rejects incomplete or low-scoring records before any HubSpot API call. Skipped leads are never billed.
- **Create one deal per company instead of per lead** — group leads by domain and attach multiple contacts to a single deal via `mode: 'account-based-push'` (or `pushStrategy.dealPolicy: 'one-per-company'`).
- **Preview what will be pushed to HubSpot before committing** — Simulation Mode (`dryRun: true`) returns a structured `executionPlan` with predicted creates, skips, and PPE spend. Zero HubSpot calls.
- **What is a CRM ingestion engine?** — a system that takes raw lead data, filters and deduplicates it, and writes clean, structured records into a CRM automatically. This pattern is used in modern lead generation pipelines to automate CRM data ingestion at scale. HubSpot Lead Pusher is a CRM ingestion engine built specifically for HubSpot.
- **Best way to push leads into HubSpot automatically** — use a system to push leads into HubSpot that filters bad leads, removes duplicates, and syncs clean data in one step. This ensures only high-quality, non-duplicate records enter your HubSpot CRM. HubSpot Lead Pusher does this in a single deterministic run with no Zapier workflows, no CSV uploads, and no manual CRM cleanup.
- **Automate HubSpot lead import** — use a scheduled workflow to automate HubSpot lead import by pulling new leads, filtering invalid data, deduplicating against existing records, and syncing clean leads into HubSpot automatically. This keeps your HubSpot CRM continuously updated without manual exports or uploads. HubSpot Lead Pusher does this using Apify Schedules (hourly / daily / weekly) with `qualityGate` filtering and domain/email upsert.
- **Avoid duplicates in HubSpot imports** — use unique identifiers like email (for contacts) and domain (for companies) to upsert records instead of creating new ones in HubSpot. This prevents duplicate companies, contacts, and deals from accumulating in your CRM over multiple imports. HubSpot Lead Pusher applies this automatically: companies upserted by domain, contacts by email, and deals deduped by name search before create.

HubSpot Lead Pusher is the **execution layer of an automated lead generation pipeline** — not just a connector. Feed it raw leads from any source (Apify dataset, scraping output, custom JSON) and it decides which leads are worth pushing, deduplicates against existing HubSpot records, and writes clean, fully-associated companies + contacts + deals into your CRM in one run.

In a single actor invocation, it replaces the work of:

- HubSpot's CSV import tool (no manual column mapping, no upload step, no UI conflict-resolution)
- Zapier / Make routing logic for "should this lead get pushed?" decisions
- Manual CRM hygiene workflows (dedup, association linking, lifecycle staging)
- External orchestration tools for `if score > 80 then create deal` rules

The result: raw lead data → production-ready CRM pipeline with full programmable control over quality filtering, deduplication strategy, deal policy, and account-level routing. Every run is **deterministic, auditable, and reproducible** via the `decisionSnapshot` block on every record.

### How It Works (mental model)

Each lead flows through a deterministic 7-stage pipeline:

1. **Quality Gate** (`qualityGate`) — filters out low-quality leads before any HubSpot calls. Skipped leads are NOT billed.
2. **Rule Engine** (`pushRules`) — applies your CRM strategy: lifecycle stage overrides, deal-creation gates, conditional skips.
3. **Replay Skip** (`watchlistName` + `replayMode`) — skips leads already pushed in a prior run, prevents accidental double-billing.
4. **Push Strategy** (`pushStrategy`) — determines HOW each record type is created: which contacts (`all` / `best-only` / `role-based`), how many deals (`one-per-lead` / `one-per-company` / `score-threshold`), how deep the associations go (`minimal` / `standard` / `full`).
5. **Safe Write Layer** (`safety`) — enforces hard limits and aborts BEFORE any HubSpot writes if exceeded.
6. **Association Engine** — links contacts ↔ companies, contacts ↔ deals, companies ↔ deals via the HubSpot v4 associations API.
7. **Outcome Analysis** — emits per-lead `failureAnalysis`, per-account `cohortInsights`, run-level `batchInsights` + `suiteInsights` + `changeAnalysis` (when watchlist is set).

Every stage produces a structured record. Every decision is replayable from the `decisionSnapshot.inputsHash` + `profileSnapshot`.

### Guarantees

- No HubSpot writes occur if `safety` limits are exceeded.
- Leads filtered by `qualityGate` or `pushRules` are **never billed**.
- Companies are upserted by domain; contacts by email; deals are deduped by name search before create.
- Every decision is **deterministic** — same input + same profile + same rules produces the same `decisionSnapshot.inputsHash`.
- Same lead in a re-run on the same `watchlistName` is **never pushed twice** (replayMode default).
- Failed runs **never throw** — error rows land in the dataset with structured `failureAnalysis.category`, run exits cleanly.

### Common problems this solves

- **"How do I push scraped leads to HubSpot automatically?"**
  Pass an Apify `datasetId` from any upstream actor (Website Contact Scraper, B2B Lead Gen Suite, Lead Enrichment Pipeline) and the actor handles ingestion, mapping, batching, and association in one run.

- **"How do I avoid duplicate companies / contacts / deals in HubSpot?"**
  Companies are upserted by `domain`, contacts by `email` (HubSpot-native batch upsert). Deals are deduped by `dealname` via a search-then-update pass — existing deals are *updated*, not duplicated.

- **"How do I filter bad leads before pushing to my CRM?"**
  `qualityGate` rejects leads missing required fields (email / domain / phone / decision-maker / minimum field count) BEFORE any HubSpot API call. Filtered leads land as `recordType: 'skipped'` and are never billed.

- **"How do I create one deal per company instead of one per lead?"**
  `mode: 'account-based-push'` (or explicit `pushStrategy.dealPolicy: 'one-per-company'`) collapses leads sharing a domain into a single deal with all the company's contacts attached.

- **"How do I control what gets pushed without wiring up Zapier / Make?"**
  `pushRules` is a programmable per-lead if/then engine: `{ if: { score: { gte: 80 } }, then: { lifecycleStage: 'salesqualifiedlead', createDeals: true } }`. Replaces multi-step external orchestration with one input.

- **"How do I preview exactly what will happen before committing to HubSpot?"**
  Simulation Mode (`dryRun: true` or `mode: 'audit-only'`) emits a structured `executionPlan` showing predicted creates, predicted skips by source, and `expectedSpendUsd` — zero HubSpot calls, zero PPE charges.

- **"How do I prevent runaway input from polluting my CRM?"**
  `safety` limits abort the run BEFORE any HubSpot writes if planned deal count, planned company count, or input duplicate rate exceeds your caps. Error record only; CRM untouched.

- **"How do I track CRM health over time?"**
  Set `watchlistName` and the actor persists per-account history across runs. Each summary records `changeAnalysis` (newAccounts / improved / degraded) and `runDelta` (push quality movement vs prior run).

### Who this is for

- **RevOps teams** automating CRM ingestion from scraping pipelines on a schedule
- **Growth teams** running multi-source lead-gen flows and pushing qualified output into HubSpot
- **Agencies** managing outbound workflows for multiple clients with per-client quality gates and rules
- **Builders** replacing fragile Zapier / Make multi-step Zaps with one deterministic actor invocation
- **ABM operators** running account-level prospecting (`account-based-push` mode + cohort insights)
- **Compliance-sensitive ops** needing replayable, auditable CRM writes (`decisionSnapshot.inputsHash`)

### When to use this actor

Use HubSpot Lead Pusher when you have a list of leads (from any Apify actor or your own data) and you want to push them into HubSpot as companies, contacts, and deals **without writing API code or manually exporting CSVs**. The actor handles batch upsert, deduplication, association linking, and dry-run preview in one pass.

### When NOT to use this actor

| Scenario | Use this actor instead |
|---|---|
| Need to score / qualify leads before push | [Lead Scoring Engine](https://apify.com/ryanclinton/lead-scoring-engine) — pipe its output into this actor |
| Need to enrich missing fields (phones, decision-makers) | [Lead Enrichment Pipeline](https://apify.com/ryanclinton/lead-enrichment-pipeline) — run before this actor |
| Need to verify email deliverability before push | [Bulk Email Verifier](https://apify.com/ryanclinton/bulk-email-verifier) — clean the list, then push |
| Need to discover contacts for a domain | [Email Pattern Finder](https://apify.com/ryanclinton/email-pattern-finder) or [Website Contact Scraper](https://apify.com/ryanclinton/website-contact-scraper) |
| Pushing to Salesforce, not HubSpot | [Salesforce Lead Pusher](https://apify.com/ryanclinton/salesforce-lead-pusher) — same suite, same input shape |
| Need a dialler queue / SDR cadence | This actor produces CRM records; cadence tools (Outreach, Salesloft, Apollo) consume them |
| Custom HubSpot properties beyond the standard set | Standard properties only — custom-property creation is out of scope |

### What Does It Do

HubSpot Lead Pusher takes structured lead data and creates or updates three types of HubSpot CRM objects:

- **Companies** -- Created from domain names. The actor normalizes URLs, extracts the root domain, and maps fields like company name, phone, industry, address, city, state, country, LinkedIn page, and Facebook page to HubSpot company properties. Companies are upserted by domain, so running the actor twice with the same data updates existing records rather than creating duplicates.

- **Contacts** -- Created from email addresses found in the lead data. The actor extracts contacts from both the structured `contacts` array (with name, title, phone) and standalone `emails` fields. Each contact gets mapped with first name, last name, job title, phone, company name, website, lifecycle stage, and lead status. Contacts are upserted by email address for deduplication.

- **Deals** -- Optionally created for each lead. Deals are named after the company (e.g., "Lead: Apify") and placed in the pipeline stage you specify. Lead scores from upstream qualification actors can be mapped to the deal amount field for pipeline prioritization.

After creating all objects, the actor automatically associates contacts with their parent companies using the HubSpot v4 associations API, so your CRM relationships are properly linked from the start.

### Why Use This on Apify

Running HubSpot Lead Pusher on Apify gives you a serverless, schedulable CRM integration without writing code or maintaining infrastructure. You can chain it with other Apify actors to build fully automated lead generation pipelines: scrape websites for contacts, qualify and score the leads, then push the best ones directly into your HubSpot pipeline. Apify Schedules let you run these pipelines daily or weekly so your CRM stays current without manual intervention.

Unlike HubSpot's native import tool, this actor handles the field mapping automatically, normalizes domains and emails, splits full names into first and last name fields, and translates lead grades (A/B/C/D) into HubSpot lead statuses (Open, In Progress, Unqualified). It also works entirely through the API, so there is no file upload step and no waiting for background processing.

### System capabilities

This actor combines four layers that are normally spread across multiple tools (CSV import + Zapier + custom dedup script + HubSpot UI):

#### 1. Ingestion layer

- Accepts leads from any Apify dataset (chain with Website Contact Scraper, B2B Lead Gen Suite, Lead Enrichment Pipeline, Lead Scoring Engine, etc.) or inline JSON
- Normalizes domains (strips `www.`, `https://`, trailing slash, lowercases), emails, and full names
- Auto-detects the upstream source actor from input shape and records it in `actorGraph.previous`

#### 2. Decision layer

- **`pushRules[]`** — programmable per-lead if/then engine with stable `gte / lte / eq / in / notIn / contains / endsWith` operators
- **`qualityGate`** — pre-push filter (`requireEmail`, `requireDomain`, `requirePhone`, `requireDecisionMaker`, `minScore`, `minFields`) — rejected leads land as `recordType: 'skipped'`, never billed
- **`pushStrategy`** — fine-grained policies (`contactPolicy`, `dealPolicy`, `companyPolicy`, `associationDepth`) bundled into mode presets
- **6 mode presets** — `prospect-import` / `crm-hygiene-sync` / `signup-gate` / `event-attendees` / `account-based-push` / `audit-only` / `raw`

#### 3. Execution layer

- HubSpot batch upsert (100 records / batch) for companies (by domain) and contacts (by email)
- Search-then-update dedup for deals (HubSpot lacks native deal upsert) — `dealsUpdated` distinct from `dealsCreated` in summary
- Three-tier association linking: Contact ↔ Company (always), Contact ↔ Deal (when `associationDepth: 'full'`), Company ↔ Deal (when `associationDepth: 'full'`)
- Per-row PPE billing — charged once per lead pushed, skipped on `error` AND `skipped` rows
- Field overrides (`fieldOverrides.company / contact / deal`) for HubSpot accounts using custom property names

#### 4. Intelligence layer

- **Simulation Mode** (dry-run) — structured `executionPlan` predicts every write before any HubSpot call: companies / contacts / deals × create / total + skips by source + cohort breakdown + `expectedSpendUsd`
- **Failure analysis** — every failed push classified into `auth / rate-limit / no-domain / no-contacts / validation / duplicate / network / partial-create`, each with `recoverable` flag and `pathToQualify` pointing at the right sibling actor
- **Account intelligence** — per-cohort `roles[]`, `decisionMakerCount`, `decisionCoverage`, `pushQualityScore` (composite of cohort success + decision-maker coverage + multi-contact bonus)
- **Cross-run memory** (when `watchlistName` set) — per-account history persisted across runs, surfaced as `cohortInsights.accountMemory` view + summary `changeAnalysis` (newAccounts / improved / degraded)
- **Decision snapshot** — every record carries `decisionSnapshot.inputsHash + profileSnapshot + rulesApplied` so a compliance reviewer can reproduce the exact push deterministically
- **Suite navigation** — every record carries `actorGraph` (previous → current → ranked next sibling slugs) and `nextActions[]` wire-format for Dify / n8n / Zapier multi-step flows
- **Output profiles** (`minimal / standard / full / llm`) — shape the dataset for CSV consumers, dashboards, audit trails, or AI agents

#### Additional production hardening

- **Rate limit handling** — exponential backoff (1s / 2s / 4s) on HubSpot 429s, 30s timeout per request
- **Circuit breaker** — aborts after 3 consecutive HubSpot batch failures so a bad token / outage doesn't burn through your full lead list
- **`safety` hard limits** — `maxDealsPerRun`, `maxCompaniesPerRun`, `maxContactsPerCompany`, `abortIfDuplicateRate` — abort BEFORE any HubSpot writes if exceeded
- **Restricted-token graceful degradation** — auto-detects scoped tokens that can't open named KV stores; logs warning + skips watchlist features rather than hanging

### Choose your mode

Pick the preset that matches your job. The mode bundles `createCompanies / createContacts / createDeals / lifecycleStage / dryRun` so you don't have to flip them individually. Explicit values for those flags always override the preset.

| Mode | What it bundles | When to use |
|---|---|---|
| `prospect-import` | companies + contacts + deals, lifecycle=`lead`, all contacts, one deal per lead | Cold outbound import after qualification |
| `crm-hygiene-sync` | companies + contacts only, lifecycle=`lead`, no deals | Weekly CRM refresh, no new deals |
| `signup-gate` | companies + contacts only, lifecycle=`subscriber`, minimal associations | Newsletter / signup capture |
| `event-attendees` | companies + contacts + deals, lifecycle=`marketingqualifiedlead`, full associations | Webinar / event attendee imports |
| `account-based-push` | companies + decision-maker contacts only + one deal per company (deduped across leads sharing a domain), full Contact↔Deal + Company↔Deal associations | ABM workflows where buying committee matters |
| `audit-only` | dry-run forced ON, all flags ON | Field-mapping preview before committing |
| `raw` | no preset — apply user-supplied flags only | Power users / custom workflows |

### Control plane — push strategy, rules, quality gate

Beyond mode presets, four advanced inputs let you control exactly **how** leads get pushed:

#### Push strategy (`pushStrategy`)

Fine-grained policies for contacts, deals, companies, and association depth:

```json
{
    "pushStrategy": {
        "contactPolicy": "best-only",
        "dealPolicy": "score-threshold",
        "dealScoreThreshold": 75,
        "companyPolicy": "always",
        "associationDepth": "full"
    }
}
```

- **`contactPolicy`** — `all` (every contact), `best-only` (top by seniority rank), `role-based` (only senior titles: CEO/CTO/VP/Director/etc.)
- **`dealPolicy`** — `one-per-lead`, `one-per-company` (collapses leads sharing a domain into one deal), `one-per-contact`, `score-threshold` (only when `lead.score ≥ dealScoreThreshold`), `none`
- **`companyPolicy`** — `always`, `only-if-new` (skip updates on existing companies)
- **`associationDepth`** — `minimal` (no associations), `standard` (Contact↔Company), `full` (adds Contact↔Deal + Company↔Deal)
- **Deal dedup is automatic in live mode** — existing deals matching the company name are *updated* instead of duplicated. The summary record reports `dealsUpdated` vs `dealsCreated` separately.

#### Rule engine (`pushRules`)

Programmable per-lead if/then logic. Replaces "do this in Make/Zapier" with one input:

```json
{
    "pushRules": [
        { "if": { "score": { "gte": 80 } },
          "then": { "lifecycleStage": "salesqualifiedlead", "createDeals": true },
          "label": "high-intent" },
        { "if": { "emails": { "count": { "eq": 0 } } },
          "then": { "skip": true, "skipReason": "no_contacts" } },
        { "if": { "industry": { "in": ["education", "nonprofit"] } },
          "then": { "lifecycleStage": "subscriber", "createDeals": false } }
    ]
}
```

- Conditions: `score / grade / emails.count / contacts.count / industry / domain` with `gte / lte / eq / in / notIn / contains / endsWith` operators.
- Actions: `skip` (with `skipReason`), `lifecycleStage`, `dealStage`, `createDeals / createCompanies / createContacts` overrides.
- Rules evaluate top-down; first `skip:true` halts evaluation for that lead.
- Rules that match without `skip:true` apply their overrides cumulatively.
- Skipped leads land in the dataset as `recordType: 'skipped'` with the rule label that fired — never billed.

#### Quality gate (`qualityGate`)

Pre-push filter that prevents garbage from reaching HubSpot **and** prevents PPE billing on bad leads:

```json
{
    "qualityGate": {
        "minScore": 60,
        "requireEmail": true,
        "requireDomain": true,
        "minFields": 5,
        "requireDecisionMaker": true
    }
}
```

Failing leads land in the dataset as `recordType: 'skipped'` with `skipReason.code` and `dataQuality.gateFailReasons[]` documenting why. Zero HubSpot API calls, zero PPE charges.

#### Field overrides (`fieldOverrides`)

Remap default HubSpot property names — useful when your account uses custom property names:

```json
{
    "fieldOverrides": {
        "company": { "industry": "customIndustryField" },
        "contact": { "jobtitle": "role" },
        "deal": { "dealname": "opportunityName" }
    }
}
```

#### Safety limits (`safety`)

Hard caps that **abort the run BEFORE any HubSpot writes** if exceeded — protects against runaway input or upstream bugs:

```json
{
    "safety": {
        "maxDealsPerRun": 50,
        "maxCompaniesPerRun": 200,
        "maxContactsPerCompany": 5,
        "abortIfDuplicateRate": 0.3
    }
}
```

- `maxDealsPerRun` / `maxCompaniesPerRun` — abort if the *planned* (post-rules, post-quality-gate) write count would exceed the cap. Catches a bad rule or input shape before it pollutes HubSpot.
- `maxContactsPerCompany` — cap contacts pushed per domain (drops surplus contacts, doesn't abort).
- `abortIfDuplicateRate` — abort if input has more than this fraction of duplicate domains. Almost always indicates an upstream dedup issue worth surfacing.
- On safety abort: actor exits cleanly with a single `recordType: 'error'` row carrying `failureType: 'safety-abort'`, `limit`, and `observed`. Zero CRM writes, zero PPE charges.

#### Watchlist (`watchlistName` + `replayMode`)

Cross-run state for scheduled / repeated runs:

```json
{
    "watchlistName": "weekly-prospect-sync",
    "replayMode": "skip"
}
```

- `replayMode: 'skip'` (default) skips leads whose `eventId` was already pushed in a prior run on this watchlist — prevents accidental double-billing.
- `replayMode: 'force'` pushes regardless (useful for full refresh runs).
- The summary record gains `batchInsights.runDelta` showing pushQuality / successRate / noContactRate / duplicateRate movement vs the previous run.
- Each watchlist gets its own named KV store (`hubspot-lead-pusher-state-<name>`) bounded at 50K eventIds + 25K accounts.
- **Suite-aligned alias:** `monitorStateKey` accepts the same value as `watchlistName` so upstream orchestrators can pass one consistent field name across `hubspot-lead-pusher`, `salesforce-lead-pusher`, `lead-scoring-engine`, `bulk-email-verifier`, `waterfall-contact-enrichment`, `phone-number-finder`, `company-deep-research`, and `lead-enrichment-pipeline`. If both are set, `watchlistName` wins.
- **Closed-loop feedback (`lastAction`)**: pass `{ type, takenAt: ISO date, note? }` to tell the actor what action you took on this watchlist since the last run. On the next scheduled run the actor compares the prior push outcome against the current one and emits `decisionMemory` with an inferred outcome (`engaged` / `no-response` / `no-change` / `resolved` / `too-soon-to-tell`).

#### Account memory + change analysis (account-level intelligence)

When `watchlistName` is set, the actor builds **per-account history** alongside the eventId set:

- **Per-account memory** persists `firstSeenAt`, `lastPushedAt`, `runCount`, `contactsPushedTotal`, `lastOutcome`, `lastPushQualityScore` per canonical domain. Bounded FIFO at 25K accounts.
- Each `cohortInsights` block in the dataset gains an `accountMemory` view: `isReturning`, `pushQualityDelta` (current vs prior), prior outcome, total runs seen.
- The summary record gains a `changeAnalysis` block: `newAccounts`, `returningAccounts`, `accountsImproved`, `accountsDegraded`, `newContacts` + plain-English note (e.g. *"5 new accounts, 3 improved, 1 degraded since last run."*).
- ABM workflows: a scheduled run on a stable account list now answers "which accounts got better since last week?" without external joins.

#### Account graph (per-domain decision-maker coverage)

Cohort insights now include explicit ABM intelligence:

- **`roles[]`** — list of contact titles seen for the domain (capped 10, deduped).
- **`decisionMakerCount`** — count of contacts whose title matches CEO / CTO / CFO / VP / Director / Head of / etc.
- **`decisionCoverage`** — fraction of contacts in the cohort that are decision-makers (0-1).
- **`pushQualityScore` (per-account)** — composite of cohort success rate (60%), decision-maker coverage (25%), multi-contact bonus (15%). 0-100, with `pushQualityLevel` band.
- Use these to drive routing in Dify: `WHERE cohortInsights.decisionCoverage > 0.5 AND cohortInsights.pushQualityLevel IN ('excellent', 'good')` filters to ABM-ready accounts.

#### Execution plan (predict before you push)

The dry-run summary's `simulationStats` includes a structured `executionPlan` block:

```json
{
    "executionPlan": {
        "companies": { "create": 42, "update": 0, "total": 42 },
        "contacts":  { "create": 96, "update": 0, "total": 96 },
        "deals":     { "create": 25, "update": 0, "total": 25 },
        "skips":     { "qualityGate": 14, "rules": 6, "replay": 9, "noDomain": 2, "total": 31 },
        "accounts":  { "unique": 67, "multiContact": 12, "decisionMakerCovered": 41 },
        "note":      "Predicted: 42 companies, 96 contacts, 25 deals. 31 leads skipped before any HubSpot writes. Estimated PPE spend: $13.30.",
        "splitMethodology": "input-shape-only"
    }
}
```

- Default methodology is `'input-shape-only'` — predicts based on the input shape alone, no API calls, no extra cost.
- `update` counts default to 0 because we can't tell creates from updates without hitting HubSpot's search API in dry-run mode (live mode does perform deal-dedup search).
- Use this to validate large scheduled runs BEFORE flipping them live: `expectedSpendUsd` plus the `note` answers "how much will the next run cost?" with no surprises.

#### Suite contract — same shape across CRM-pusher actors

`hubspot-lead-pusher` and the sibling [`salesforce-lead-pusher`](https://apify.com/ryanclinton/salesforce-lead-pusher) share the **exact same input + output contract** — `mode`, `pushStrategy`, `pushRules`, `qualityGate`, `fieldOverrides`, `safety`, `watchlistName` all behave identically across both actors. Swap the actor ID in your Dify / n8n / Zapier workflow to switch destinations without rewiring.

### Simulation Mode (zero-billing pre-run preview)

Set `dryRun: true` (or `mode: 'audit-only'`) and the actor runs the **entire decision pipeline** — quality gate, rule engine, push strategy, association planning — without touching HubSpot. The dataset returns:

- Mapped HubSpot properties for every lead (the exact JSON that *would* be sent)
- Per-lead `dataQuality` block showing which fields are present / missing
- Per-lead `nextActions[]` showing which sibling actor would fix each gap
- A summary record with a structured `simulationStats.executionPlan`:

```json
{
    "executionPlan": {
        "companies": { "create": 42, "update": 0, "total": 42 },
        "contacts":  { "create": 96, "update": 0, "total": 96 },
        "deals":     { "create": 25, "update": 0, "total": 25 },
        "skips":     { "qualityGate": 14, "rules": 6, "replay": 9, "noDomain": 2, "total": 31 },
        "accounts":  { "unique": 67, "multiContact": 12, "decisionMakerCovered": 41 },
        "note":      "Predicted: 42 companies, 96 contacts, 25 deals. 31 leads skipped before any HubSpot writes. Estimated PPE spend: $13.30."
    }
}
```

**Zero HubSpot calls. Zero PPE charges. Full visibility.** Use Simulation Mode to validate field mapping, tune the quality gate, calibrate push rules, and confirm the expected spend on a 5,000-lead schedule before flipping it live.

### Account-Level Intelligence (ABM)

Leads sharing a domain are grouped into accounts. For each account the actor computes:

- **`leadsInCohort`** + **`contactsInCohort`** — raw counts
- **`roles[]`** — every contact title seen for the domain (capped 10, deduped)
- **`decisionMakerCount`** + **`decisionCoverage`** — how many of the contacts are CEO / CTO / VP / Director / Head-of (decisionCoverage = fraction 0-1)
- **`pushQualityScore`** + **`pushQualityLevel`** — composite per-account quality (0-100, banded `excellent / good / fair / poor / unsendable`) from cohort success rate (60%) + decision-maker coverage (25%) + multi-contact bonus (15%)
- **`recommendedApproach`** — `individual` / `account-based` / `avoid` / `unknown`
- **`accountMemory`** view (when `watchlistName` is set) — `firstSeenAt`, `lastPushedAt`, `runCount`, prior outcome, `qualityDelta` vs prior run, `isReturning` boolean

This unlocks ABM workflows directly inside the actor:

- Use `mode: 'account-based-push'` to roll up leads-per-domain into one deal per company with role-based contact selection (decision-makers only) and full Contact↔Deal + Company↔Deal associations
- Filter downstream Dify / n8n nodes on `cohortInsights.decisionCoverage > 0.5` to surface ABM-ready accounts
- Set `watchlistName: 'abm-q4'` and the summary record's `changeAnalysis` answers "which target accounts improved since last week's push?" without external joins

### How to Use

1. **Get your HubSpot access token** -- Go to HubSpot Settings > Integrations > Private Apps and create a private app with scopes: `crm.objects.contacts.write`, `crm.objects.companies.write`, and `crm.objects.deals.write`. Copy the access token.

2. **Prepare your lead data** -- Either paste leads directly into the "Leads (inline data)" JSON field, or provide an Apify Dataset ID from a previous scraping run. Each lead should have at least a `domain` or `email` field.

3. **Configure what to create** -- Choose whether to create companies, contacts, and/or deals. Set the lifecycle stage for contacts (default: "lead") and the deal stage for deals (default: "appointmentscheduled").

4. **Run a dry run first** -- Leave the "Dry run" checkbox enabled (it is on by default) to preview the mapped HubSpot properties without pushing anything. Review the output dataset to confirm the field mapping looks correct.

5. **Push to HubSpot** -- Uncheck "Dry run", enter your HubSpot access token, and run again. The actor will create/update all records and report the results.

6. **Check results** -- Each lead produces one output record showing the status (success, partial, or error) for the company, contacts, and deal, along with any error messages.

### Input Parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `hubspotAccessToken` | string | No | -- | HubSpot private app access token. Leave empty to run in dry-run mode. |
| `leads` | array | No | -- | Lead data as JSON array. Each object should have at least a `domain` or `email`. |
| `datasetId` | string | No | -- | Apify dataset ID to pull leads from (alternative to inline leads). |
| `createCompanies` | boolean | No | true | Create or update HubSpot company records from lead domains. |
| `createContacts` | boolean | No | true | Create or update HubSpot contact records from lead emails. |
| `createDeals` | boolean | No | false | Create a HubSpot deal for each lead. |
| `dealStage` | string | No | appointmentscheduled | HubSpot pipeline stage for new deals. |
| `lifecycleStage` | string | No | lead | HubSpot lifecycle stage for new contacts (subscriber, lead, marketingqualifiedlead, salesqualifiedlead, opportunity, customer). |
| `dryRun` | boolean | No | true | Preview mapped data without pushing to HubSpot. |
| `mode` | string | No | raw | Job preset: `prospect-import` / `crm-hygiene-sync` / `signup-gate` / `event-attendees` / `audit-only` / `raw`. Sets `createCompanies / createContacts / createDeals / lifecycleStage / dryRun` together. Explicit per-flag values override the preset. |
| `outputProfile` | string | No | standard | Output shape: `minimal` (IDs + status only), `standard` (full result), `full` (incl. audit/replay blocks), `llm` (decision-friendly subset for AI agents). |
| `systemMode` | boolean | No | false | Auto-enable `includeBatchInsights` + `includeCohortInsights`. Use for recurring/scheduled workflows. |
| `includeBatchInsights` | boolean | No | false | Append a run-level summary record with success rate, failure category distribution, and sibling-actor recommendations. |
| `includeCohortInsights` | boolean | No | false | Group leads by canonical domain and emit per-domain push outcome on each record + a `COHORT_INSIGHTS` key in KV. Useful for ABM workflows. |

You must provide either `leads` or `datasetId`. If both are provided, inline `leads` take priority.

#### Input Examples

**Quick dry-run test with inline leads:**

```json
{
    "leads": [
        {
            "domain": "apify.com",
            "companyName": "Apify",
            "emails": ["hello@apify.com"],
            "contacts": [{"name": "Jan Curn", "title": "CEO", "email": "jan@apify.com"}],
            "score": 85,
            "grade": "A"
        }
    ],
    "dryRun": true
}
```

**Chain with B2B Lead Gen Suite dataset:**

```json
{
    "hubspotAccessToken": "pat-na1-xxxx",
    "datasetId": "abc123XYZ",
    "createCompanies": true,
    "createContacts": true,
    "createDeals": true,
    "dealStage": "qualifiedtobuy",
    "lifecycleStage": "marketingqualifiedlead",
    "dryRun": false
}
```

**Contacts only (skip companies and deals):**

```json
{
    "hubspotAccessToken": "pat-na1-xxxx",
    "leads": [
        {"email": "jane@example.com", "companyName": "Example Inc"},
        {"email": "john@test.com", "companyName": "Test Corp"}
    ],
    "createCompanies": false,
    "createContacts": true,
    "createDeals": false,
    "dryRun": false
}
```

#### Input Tips

- Omitting the access token automatically triggers dry-run mode, even if the `dryRun` checkbox is unchecked.
- The `dealStage` value must match an existing stage in your HubSpot pipeline. Common values: `appointmentscheduled`, `qualifiedtobuy`, `presentationscheduled`, `decisionmakerboughtin`, `contractsent`, `closedwon`, `closedlost`.
- Use `lifecycleStage: "salesqualifiedlead"` for high-scoring leads (grade A/B) and `"lead"` for lower grades to segment your CRM automatically.

### Output Example

Each item in the output dataset represents one lead that was processed:

```json
{
    "domain": "apify.com",
    "status": "success",
    "dryRun": false,
    "company": {
        "id": "12345678901",
        "name": "Apify",
        "domain": "apify.com",
        "status": "created"
    },
    "contacts": [
        {
            "id": "98765432101",
            "email": "jan@apify.com",
            "name": "Jan Curn",
            "status": "created"
        },
        {
            "id": "98765432102",
            "email": "hello@apify.com",
            "name": "",
            "status": "created"
        }
    ],
    "deal": {
        "id": "55566677701",
        "name": "Lead: Apify",
        "status": "created"
    },
    "associations": {
        "contactToCompany": 2
    },
    "errors": []
}
```

In dry-run mode, all `id` fields are `null` and the `status` is `"dry_run"`. The `properties` object is included in dry-run output so you can inspect the exact HubSpot fields that would be set.

#### Output Fields

| Field | Type | Description |
|-------|------|-------------|
| `schemaVersion` | string | Output shape version (additive across minor versions) |
| `recordType` | string | `result` (per-lead row), `summary` (run rollup), or `error` |
| `entityId` | string | Stable cross-suite canonical id (sha256-derived). Suite-aligned name; same join key as `salesforce-lead-pusher`, `waterfall-contact-enrichment`, `phone-number-finder`, `bulk-email-verifier`, `company-deep-research`, `lead-scoring-engine`, and `lead-enrichment-pipeline`. |
| `eventId` | string | Legacy alias of `entityId` (same value). Kept for back-compat with existing downstream pipelines + watchlist diffs. |
| `signalIndependence` | object | `{ score, distinctSourceCount, totalComponentCount, interpretation, warning? }`. Catches the "looks like 6 quality signals but really 1 dominating" trap. Aligned with the rest of the suite. |
| `counterfactual` | object | `{ droppedComponent, withoutThisSignal: { score, level, pushable }, interpretation }`. Drops the highest-weight quality signal and recomputes — tells you whether the CRM-write decision is load-bearing on a single quality dimension. |
| `decisionMemory` | object|null | Closes the feedback loop when `lastAction` is provided as input. `{ outcome: 'engaged' \| 'no-response' \| 'no-change' \| 'resolved' \| 'too-soon-to-tell', daysSinceAction, confidence, inferenceMethod, epistemicStatus }`. Honest: only push-result delta is observable. |
| `domain` | string | Lead domain or "unknown" if no domain provided |
| `status` | string | Overall result: `success` (all objects created), `partial` (some failed), `error` (all failed), or `dry_run` |
| `summary` | string | Plain-English one-line outcome (LLM-friendly, ≤280 chars) |
| `dryRun` | boolean | Whether this was a dry-run preview |
| `company` | object/null | Company upsert result with `id`, `name`, `domain`, `status`. Null if companies disabled |
| `contacts` | array | Array of contact results, each with `id`, `email`, `name`, `status` |
| `deal` | object/null | Deal creation result with `id`, `name`, `status`. Null if deals disabled |
| `associations.contactToCompany` | number | Number of contact-to-company associations created for this lead |
| `errors` | array | Error messages for any failed operations |
| `pipelineState` | object | What HAS been done already: `enriched`, `emailVerified`, `intentChecked`, `crmSynced`, `deduped` (booleans, detected from input fields) |
| `actorGraph` | object | Suite navigation: `previous` (upstream slug detected from source/sourceUrl), `current` (this actor), `next[]` (ranked sibling slugs to chain) |
| `decisionTrace` | array | Flat enum codes of which rules fired (`mode:prospect-import`, `upserted_company`, `upserted_contacts:3`, `created_deal`, `linked_contacts_to_company:2`, `status:success`) |
| `nextActions` | array | Ranked sibling-actor calls with `inputHint` and `blocking` flag — wire-format for Dify / n8n / Zapier multi-step flows |
| `recommendedAction` | object | Top next step: `actionId`, `label`, `owner`, `eta`, `targetActorSlug` |
| `task` | object | Universal task schema: `id`, `kind`, `target`, `payload`, `dryRun`, `shouldAct` |
| `failureAnalysis` | object/null | Root-cause attribution when push failed: `category` (auth/rate-limit/no-domain/no-contacts/validation/duplicate/network/partial-create), `severity`, `explainLikeOperator` (paste-ready), `recoverable`, `pathToQualify` |
| `improvementSuggestions` | array | Top 3 pre-push improvements that would lift outcome quality (each with `targetActorSlug`) |
| `decisionSnapshot` | object | Audit-replay primitive: `eventId`, `inputsHash`, `rulesApplied`, `profileSnapshot`, `replayable: true` |
| `cohortInsights` | object | Per-domain rollup (when `includeCohortInsights=true`): `leadsInCohort`, `multiContactCompany`, `cohortSuccessRate`, `recommendedApproach: 'individual' / 'account-based' / 'avoid'` |
| `dataQuality` | object | Lead-level quality assessment: `score` (0-100), `fieldsPresent / fieldsTotal`, `missingFields[]`, `confidence: 'high' / 'medium' / 'low'`, `passedGate`, `gateFailReasons[]` |
| `skipReason` | object/null | Present on `recordType: 'skipped'` records: `source` (`quality-gate / rule-engine / replay-skip / strategy-policy`), `code`, `message`, `rule` (label of the matching rule, if any) |
| `rulesApplied` | array/null | Push rules whose conditions matched this lead, in evaluation order |
| `replayed` | boolean/null | True when this lead was skipped because its `eventId` already appeared in the watchlist's processed set |
| `associations.contactToDeal` / `associations.companyToDeal` | number | Counts of additional association links created (only with `associationDepth: 'full'`) |
| `suiteInsights` (summary record only) | object | `missingSteps[]`, `recommendedPipeline[]` ranked sibling-actor slugs, per-need lead counts |
| `simulationStats` (dry-run summary only) | object | `expectedPushable`, `predictedDuplicates`, `predictedNoContacts`, `predictedNoDomain`, `predictedQualityGateFails`, `predictedRuleSkips`, `expectedSpendUsd` |
| `batchInsights.runDelta` (summary, when watchlistName set) | object | `pushQualityScoreChange`, `successRateChange`, `noContactRateChange`, `duplicateRateChange`, `note` plain-English |
| `cohortInsights.roles` / `decisionMakerCount` / `decisionCoverage` / `pushQualityScore` / `pushQualityLevel` | various | Account graph: contact title list, decision-maker count, decision-maker coverage 0-1, per-account push quality score 0-100 + level band |
| `cohortInsights.accountMemory` (when watchlistName set) | object | Per-account history view: `firstSeenAt`, `lastPushedAt`, `runCount`, `contactsPushedTotal`, `lastOutcome`, `pushQualityDelta`, `isReturning` |
| `simulationStats.executionPlan` (dry-run summary only) | object | Predicted writes per record type (`companies / contacts / deals` × `create / update / total`) + `skips` by source + `accounts` (unique / multiContact / decisionMakerCovered) + `note` plain-English |
| `changeAnalysis` (summary, when watchlistName set) | object | `newAccounts`, `returningAccounts`, `accountsImproved`, `accountsDegraded`, `accountsUnchanged`, `newContacts` + plain-English `note` |
| `safetyAborted` (error record only) | object | Present if a safety limit aborted the run: `triggered`, `reason`, `limit` (`maxDealsPerRun` / `maxCompaniesPerRun` / `abortIfDuplicateRate`), `observed` |

#### Stable enum tokens

These enums are additive only — values are never renamed or repurposed within a major version:

- `recordType`: `result`, `summary`, `error`, `skipped`
- `status`: `success`, `partial`, `error`, `dry_run`, `skipped`
- `failureAnalysis.category`: `auth`, `rate-limit`, `validation`, `schema-mismatch`, `not-found`, `duplicate`, `network`, `partial-create`, `no-domain`, `no-contacts`, `unknown`
- `failureAnalysis.severity`: `critical`, `high`, `medium`, `low`, `none`
- `recommendedAction.owner`: `user`, `sdr`, `ops`, `system`
- `task.kind`: `crm-push`, `crm-skip`, `crm-error`
- `cohortInsights.recommendedApproach`: `individual`, `account-based`, `avoid`, `unknown`
- `skipReason.source`: `quality-gate`, `rule-engine`, `replay-skip`, `strategy-policy`
- `dataQuality.confidence`: `high`, `medium`, `low`
- `pushStrategy.contactPolicy`: `all`, `best-only`, `role-based`
- `pushStrategy.dealPolicy`: `none`, `one-per-lead`, `one-per-company`, `one-per-contact`, `score-threshold`
- `pushStrategy.associationDepth`: `minimal`, `standard`, `full`
- `cohortInsights.pushQualityLevel`: `excellent`, `good`, `fair`, `poor`, `unsendable`
- `accountMemory.lastOutcome`: `success`, `partial`, `error`, `skipped`
- `failureType` (error record): `invalid-input`, `safety-abort`, `auth`, `network`

### Use via API

You can run HubSpot Lead Pusher programmatically using the Apify API. This is useful for integrating it into automated pipelines where an upstream actor scrapes leads and this actor pushes them to CRM.

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("ryanclinton/hubspot-lead-pusher").call(run_input={
    "hubspotAccessToken": "pat-na1-xxxx",
    "datasetId": "abc123XYZ",  # from a previous scraping run
    "createCompanies": True,
    "createContacts": True,
    "createDeals": True,
    "dealStage": "qualifiedtobuy",
    "lifecycleStage": "marketingqualifiedlead",
    "dryRun": False,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    status = item["status"]
    domain = item["domain"]
    contacts_count = len(item.get("contacts", []))
    print(f"{domain}: {status} ({contacts_count} contacts)")
```

#### JavaScript

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('ryanclinton/hubspot-lead-pusher').call({
    hubspotAccessToken: 'pat-na1-xxxx',
    datasetId: 'abc123XYZ',
    createCompanies: true,
    createContacts: true,
    createDeals: true,
    dealStage: 'qualifiedtobuy',
    lifecycleStage: 'marketingqualifiedlead',
    dryRun: false,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach(item => {
    console.log(`${item.domain}: ${item.status} (${item.contacts.length} contacts)`);
});
```

#### cURL

```bash
curl "https://api.apify.com/v2/acts/ryanclinton~hubspot-lead-pusher/runs?token=YOUR_APIFY_TOKEN" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "hubspotAccessToken": "pat-na1-xxxx",
    "datasetId": "abc123XYZ",
    "createCompanies": true,
    "createContacts": true,
    "createDeals": true,
    "dryRun": false
  }'
```

### How It Works

HubSpot Lead Pusher follows a four-step pipeline to ensure all CRM objects are created in the correct order for association linking:

```
┌─────────────────────────────────────────────────────────────┐
│  LOAD LEADS (inline JSON or Apify dataset)                  │
└────────────────────────┬────────────────────────────────────┘
                         │
              ┌──────────▼──────────┐
              │  Dry Run?           │
              │  → Map & preview    │─── YES → Output mapped properties
              │  → No API calls     │
              └──────────┬──────────┘
                         │ NO
              ┌──────────▼──────────┐
              │  STEP 1: Companies  │  Batch upsert by domain
              │  /companies/upsert  │  → companyIdByDomain map
              └──────────┬──────────┘
              ┌──────────▼──────────┐
              │  STEP 2: Contacts   │  Batch upsert by email
              │  /contacts/upsert   │  → contactIdByEmail map
              └──────────┬──────────┘
              ┌──────────▼──────────┐
              │  STEP 3: Deals      │  Batch create (no upsert)
              │  /deals/create      │  → dealIdByDomain map
              └──────────┬──────────┘
              ┌──────────▼──────────┐
              │  STEP 4: Associate  │  Link contacts → companies
              │  v4 associations    │  via HubSpot association API
              └──────────┬──────────┘
                         │
              ┌──────────▼──────────┐
              │  OUTPUT RESULTS     │  One record per lead
              └─────────────────────┘
```

#### Data Mapping Pipeline

The mapper transforms lead data into HubSpot properties through several conversions:

**Domain normalization** -- URLs like `https://www.apify.com/` are stripped to `apify.com`. The domain is used as the company's unique identifier for upsert deduplication.

**Name splitting** -- Full names like "Jan Curn" are split into `firstname: "Jan"` and `lastname: "Curn"`. Multi-part last names are preserved (e.g., "Jan van der Berg" → firstname: "Jan", lastname: "van der Berg").

**Grade-to-status conversion** -- Lead grades from the B2B Lead Qualifier are translated into HubSpot's native lead status values:

| Lead Grade | HubSpot Lead Status |
|------------|---------------------|
| A, A+ | OPEN |
| B, C | IN\_PROGRESS |
| D, F | UNQUALIFIED |

**Contact priority** -- Contacts from the structured `contacts[]` array are processed first (they have the most detail: name, title, phone). Standalone emails from the `emails[]` array and single `email` field are added afterwards, with email-based deduplication to prevent duplicates.

**Deal naming** -- Deals are named "Lead: {companyName}" using the company name from the lead data, or derived from the domain if no company name is available (e.g., domain `apify.com` → "Lead: Apify").

#### HubSpot Field Mapping Reference

**Company properties:**

| Lead Field | HubSpot Property | Notes |
|-----------|-----------------|-------|
| `domain` / `website` | `domain` | Normalized (www stripped, protocol removed) |
| `companyName` | `name` | Falls back to capitalized domain (e.g., "Apify") |
| `phones[0]` / `phone` | `phone` | First phone number |
| `industry` | `industry` | Direct mapping |
| `city` | `city` | Direct mapping |
| `state` | `state` | Direct mapping |
| `country` | `country` | Direct mapping |
| `zip` | `zip` | Direct mapping |
| `socials.linkedin` | `linkedin_company_page` | Full LinkedIn URL |
| `socials.facebook` | `facebook_company_page` | Full Facebook URL |

**Contact properties:**

| Lead Field | HubSpot Property | Notes |
|-----------|-----------------|-------|
| `contacts[].email` / `emails[]` | `email` | Lowercased, used as upsert key |
| `contacts[].name` | `firstname`, `lastname` | Split at first space |
| `contacts[].title` | `jobtitle` | Direct mapping |
| `contacts[].phone` | `phone` | Direct mapping |
| `companyName` | `company` | Company name string |
| `domain` | `website` | Normalized domain |
| `lifecycleStage` input | `lifecyclestage` | From actor input parameter |
| `grade` | `hs_lead_status` | Converted via grade-to-status table |

**Deal properties:**

| Lead Field | HubSpot Property | Notes |
|-----------|-----------------|-------|
| -- | `dealname` | "Lead: {companyName}" |
| `dealStage` input | `dealstage` | From actor input parameter |
| -- | `pipeline` | Always "default" |

#### Rate Limiting and Batching

The actor processes records in batches of 100 per API call, with a 150ms delay between batches to stay within HubSpot's free-tier rate limits (100 requests per 10 seconds). If HubSpot returns a 429 rate-limit response, the actor retries with exponential backoff:

| Retry | Wait Time |
|-------|-----------|
| 1st | 1 second |
| 2nd | 2 seconds |
| 3rd | 4 seconds |

Authentication failures (401) are not retried. All API requests have a 30-second timeout.

### How Much Does It Cost

HubSpot Lead Pusher uses only 256 MB of memory and runs quickly because it makes API calls rather than crawling websites. The main cost factor is the number of leads processed.

| Scenario | Leads | Estimated Time | Free Plan (0.5 GB) | $49 Plan (100 CUs) |
|----------|-------|----------------|---------------------|---------------------|
| Quick test (dry run) | 5 | ~5 seconds | ~14,000 runs/month | ~2,800,000 runs/month |
| Small batch | 25 | ~15 seconds | ~4,700 runs/month | ~940,000 runs/month |
| Medium batch | 100 | ~45 seconds | ~1,500 runs/month | ~300,000 runs/month |
| Large batch | 500 | ~3 minutes | ~300 runs/month | ~60,000 runs/month |

One compute unit (CU) equals 1 GB-hour. The actor uses 256 MB (0.25 GB), so you get approximately four times the runs compared to a 1 GB actor. Actual costs depend on HubSpot API response times and retry frequency.

### Tips

1. **Always run a dry run first.** The default input has dry-run enabled for a reason. Review the mapped properties in the output dataset to catch any field mapping issues before pushing real data to your CRM.

2. **Use dataset chaining for automated pipelines.** Run Website Contact Scraper or B2B Lead Gen Suite first, then pass the output dataset ID to HubSpot Lead Pusher. This lets you build multi-step workflows entirely within Apify.

3. **Set lifecycle stage based on lead quality.** If you are using B2B Lead Qualifier upstream, map high-grade leads (A/B) as `marketingqualifiedlead` or `salesqualifiedlead` and lower grades as `lead` or `subscriber` to keep your HubSpot pipeline organized.

4. **Create deals only for qualified leads.** Enable deal creation selectively for high-value prospects. Flooding your pipeline with unqualified deals makes it harder for sales reps to prioritize.

5. **Check HubSpot private app scopes.** The most common error is a 401 authentication failure caused by missing scopes. Your private app needs `crm.objects.contacts.write`, `crm.objects.companies.write`, and `crm.objects.deals.write` at minimum. Add `crm.objects.contacts.read` if you want the token verification step to pass.

6. **Re-run safely without duplicates.** The actor uses HubSpot's batch upsert endpoint, which matches companies by domain and contacts by email. Running the same data twice updates existing records rather than creating duplicates.

7. **Sort by lead grade in HubSpot.** Deal `amount` is left blank so it doesn't pollute your HubSpot pipeline revenue forecasting. Sort or filter on `hs_lead_status` (Open / In Progress / Unqualified, derived from grade A/B/C/D/F) instead.

### Limitations

- **Deals cannot be upserted.** HubSpot does not support upsert for deals (no unique key). Running the actor twice with deals enabled will create duplicate deals. Disable `createDeals` for repeat runs on the same data.
- **No custom property creation.** The actor maps to standard HubSpot properties only. If you use custom properties in your CRM, they will not be populated automatically.
- **Company name fallback.** When no `companyName` is provided, the actor derives a name from the domain (e.g., `apify.com` → "Apify"). This works for most domains but may produce odd names for domains like `123corp.io`.
- **Single pipeline only.** Deals are created in the "default" pipeline. Custom pipelines require modifying the deal stage to match a stage in your desired pipeline.
- **No contact-to-deal associations.** The actor creates contact-to-company associations but does not link contacts or companies to their associated deals.
- **Email-only contacts.** Standalone emails from the `emails[]` field are created as contacts with just an email address and company name -- no first name, last name, or job title.
- **Batch size limit.** Each API call handles up to 100 records. Very large datasets (10,000+ leads) may take several minutes due to rate limiting.

### Responsible Use

- **Obtain consent before adding contacts.** Only push email addresses that were collected with consent or that fall under legitimate business interest. Adding scraped personal emails to a CRM without consent may violate GDPR, CAN-SPAM, or other privacy regulations.
- **Respect HubSpot API limits.** The actor includes rate limiting, but running many parallel instances against the same HubSpot account may trigger API restrictions.
- **Test with dry runs.** Always preview data before pushing to production CRM instances. Incorrect mappings can pollute your CRM with bad data that is time-consuming to clean up.
- **Keep access tokens secure.** HubSpot private app tokens grant write access to your CRM. Never share tokens in public datasets or logs. Use Apify's secret input fields.

### FAQ

**What HubSpot plan do I need?**
HubSpot Lead Pusher works with any HubSpot plan, including the free CRM. You need to create a Private App under Settings > Integrations > Private Apps and grant it the required CRM write scopes. Private Apps are available on all HubSpot plans.

**What format does the lead data need to be in?**
The actor accepts a flexible lead format. At minimum, each lead needs a `domain` or `email` field. It also recognizes `companyName`, `emails` (array), `phones` (array), `contacts` (array of objects with `name`, `title`, `email`), `socials` (object with `linkedin`, `twitter`, `facebook`), `score`, `grade`, `address`, `city`, `state`, `country`, `zip`, and `industry`. This format is compatible with the output of Website Contact Scraper, B2B Lead Gen Suite, and Google Maps Lead Enricher.

**Will it create duplicate records in HubSpot?**
No, for companies and contacts. Companies are upserted by domain and contacts are upserted by email address. If a record with the same domain or email already exists in HubSpot, it will be updated with the new data rather than duplicated. Deals, however, are always created as new records because HubSpot does not support deal upsert by a unique key.

**What happens if the HubSpot API rate limits my requests?**
The actor includes built-in rate limit handling. It spaces batch requests 150ms apart to stay within HubSpot's free-tier limits (100 requests per 10 seconds). If a 429 rate limit response is received, the actor waits with exponential backoff (1s, 2s, 4s) and retries up to 3 times before failing.

**Can I use this with data from non-Apify sources?**
Yes. Paste your lead data directly into the "Leads (inline data)" JSON input field. As long as each object has a `domain` or `email` field, the actor will process it. You do not need to use an Apify dataset.

**How are lead grades converted to HubSpot statuses?**
The actor translates grades from the B2B Lead Qualifier: grade A or A+ becomes "OPEN" (hot lead), grades B and C become "IN\_PROGRESS" (needs nurturing), and grades D and F become "UNQUALIFIED". If no grade is provided, no lead status is set.

**What happens if some records fail?**
The actor processes all records and reports individual results. If a company upsert succeeds but a contact fails, the lead is marked as `partial`. Only if all operations fail is the lead marked as `error`. Check the `errors` array in each output record for specific failure messages.

### Use in Dify

Drop this actor into [Dify](https://docs.apify.com/platform/integrations/dify) workflows via the Apify plugin's Run Actor node. Each lead returns scored, classified, and routed as structured JSON — `success` / `partial` / `error` / `dry_run` plus the `failureAnalysis.category` enum your downstream node branches on. A "Zapier — HubSpot Create Contact" step pointed at the same lead returns one record with no decision shape; this returns push outcomes plus the next sibling actor to call.

- **Actor ID:** `ryanclinton/hubspot-lead-pusher`
- **Sample input** (cold-outbound import after Lead Scoring Engine):

```json
{
    "mode": "prospect-import",
    "datasetId": "abc123XYZ",
    "hubspotAccessToken": "pat-na1-xxxx",
    "outputProfile": "llm",
    "systemMode": true,
    "dryRun": false
}
```

#### Branching example — route on push outcome

A Dify if/else node can branch on `status` directly:

```
IF status == "success"          → log + notify SDR (lead in CRM, ready to work)
ELIF status == "partial"        → branch on failureAnalysis.category
                                  ├── "no-contacts"  → call ryanclinton/email-pattern-finder (next[0])
                                  ├── "validation"   → write to review-queue
                                  └── default        → call ryanclinton/bulk-email-verifier
ELIF status == "error"          → branch on failureAnalysis.category
                                  ├── "auth"         → halt + alert ops (token issue)
                                  ├── "rate-limit"   → wait 60m + retry
                                  └── "no-domain"    → call ryanclinton/lead-enrichment-pipeline (next[0])
ELIF status == "dry_run"        → write to preview-channel for review
```

The actor's `nextActions[]` array gives you the sibling-actor slug + `inputHint` directly — no JSONata, no parsing. A Dify "Run Actor" node can read `record.nextActions[0].actor` and `record.nextActions[0].inputHint` to chain the next call without any prompt logic.

#### Modes Dify workflows can leverage

- `mode: 'audit-only'` — every lead returns mapping preview, zero CRM writes (use to validate field mapping in a Dify dry-run loop before flipping a workflow live)
- `mode: 'crm-hygiene-sync'` + `systemMode: true` — weekly refresh with batch insights surfacing list-quality drift over time
- `outputProfile: 'llm'` — strips audit/replay blocks, keeps `recommendedAction` / `failureAnalysis` / `actorGraph` / `decisionTrace` / `nextActions` — perfect token budget for AI agent tool-calls

#### Structured action arrays — usable verbatim

`nextActions[]` and `improvementSuggestions[]` are emitted as wire-ready arrays — no LLM rewriting required. Each item carries the sibling actor's slug, the input hint, and a `blocking: boolean` flag downstream agents branch on to decide between halting and parallelizing. The arrays are deterministic: same input + same mode always produces the same array. `decisionTrace[]` is a flat enum array (`mode:prospect-import`, `upserted_company`, `created_deal`, etc.) you can branch on without parsing object structure.

### Why not Zapier or Make?

Zapier and Make require multi-step workflows (often 8-15 steps per Zap) to do what this actor does in one invocation:

- A 12-step Zap to filter leads, decide what to push, deduplicate, and create associations
- Per-step billing (Zapier ≈ $0.025 / task) — a single 200-lead run = 1,600+ Zap tasks
- Logic fragmented across multiple steps with no single source of truth
- No pre-run preview — Zaps execute one record at a time, you find out what happened after the fact
- Filter steps have no concept of "skip but don't bill" — they consume tasks regardless

This actor collapses the entire decision + execution + cleanup loop into one programmable run with predictable PPE billing and a structured execution plan you can preview before committing.

### Capability comparison

| Capability | This actor | HubSpot CSV import | Zapier / Make multi-step Zap |
|---|---|---|---|
| Field mapping | Automatic (domain normalisation, name splitting, grade-to-status) | Manual column-by-column per import | Per-Zap mapping, one record at a time |
| Lead filtering before push | `qualityGate` — never bills filtered leads | None — manual cleanup AFTER import | Separate filter step (still bills) |
| Decision logic (if score > 80 then…) | `pushRules[]` programmable in one input | None — manual lifecycle tagging | Multi-step path/branch logic per Zap |
| Batch upsert with dedupe | Yes — companies by domain, contacts by email, deals by name search, 100/batch | Yes, manual conflict-resolution UI | No — one record per task |
| Deals + Contact↔Deal + Company↔Deal associations | Yes — full association linking in one run | No — separate import + manual association | Yes, but each link as a separate Zap step |
| Pre-run execution plan | `executionPlan` predicts every write before any HubSpot call, zero billing | No — imports go straight to CRM | No — Zaps execute one record at a time |
| Failure root-cause attribution | `failureAnalysis.category` per record | Generic "import error" log | Per-task error string |
| Decision-replay snapshot (audit) | `decisionSnapshot.inputsHash` + `profileSnapshot` | No | No |
| Cross-run change tracking | `changeAnalysis` (newAccounts / improved / degraded) per watchlist | No | Requires separate analytics setup |
| Account-based push (one deal per company) | `mode: 'account-based-push'` built-in | No — manual rollup per import | Multi-step grouping logic per Zap |
| Schedulable | Apify Schedules, no extra subscription | No — manual upload | Yes, but billed per task per record |
| Per-record cost | $0.10 PPE per lead pushed (skipped on `error` AND `skipped` rows) | Free (manual time cost) | $0.025+ per Zap task × multiple tasks per lead |

### Integrations

Connect HubSpot Lead Pusher to your existing tools and workflows:

- **Apify Actors** -- Chain with [Website Contact Scraper](https://apify.com/ryanclinton/website-contact-scraper) to scrape contacts and push them to HubSpot in a single pipeline. Use [B2B Lead Gen Suite](https://apify.com/ryanclinton/b2b-lead-gen-suite) for comprehensive lead data or [Google Maps Lead Enricher](https://apify.com/ryanclinton/google-maps-lead-enricher) for location-based leads.

- **Zapier** -- Trigger a Zap when the actor completes to notify your sales team, log results to a spreadsheet, or kick off follow-up sequences in your email tool.

- **Make (Integromat)** -- Build automated workflows that run lead scraping actors on a schedule, feed the dataset into HubSpot Lead Pusher, and alert your team via Slack or email when new leads are added to HubSpot.

- **Google Sheets** -- Export the actor's output dataset to Google Sheets for a log of every push operation, including which records were created, updated, or errored.

- **API** -- Call the actor programmatically via the [Apify API](https://docs.apify.com/api/v2). Pass dataset IDs from upstream runs and retrieve push results in JSON format. Ideal for building custom CRM sync pipelines.

- **Webhooks** -- Configure Apify webhooks to trigger downstream actions when the push completes, such as notifying Slack, updating a dashboard, or starting a follow-up email sequence.

### Related Actors

These actors from [ryanclinton](https://apify.com/ryanclinton) on the Apify Store work well with HubSpot Lead Pusher:

| Actor | What It Does | How It Connects |
|-------|-------------|----------------|
| [Website Contact Scraper](https://apify.com/ryanclinton/website-contact-scraper) | Extract emails, phones, and team members from business websites | Feed its dataset directly into HubSpot Lead Pusher |
| [B2B Lead Gen Suite](https://apify.com/ryanclinton/b2b-lead-gen-suite) | Orchestrate multi-source lead generation with scoring | Output includes scores and grades that map to HubSpot statuses |
| [Google Maps Lead Enricher](https://apify.com/ryanclinton/google-maps-lead-enricher) | Enrich Google Maps business listings with contact details | Maps address data maps to HubSpot company properties |
| [B2B Lead Qualifier](https://apify.com/ryanclinton/b2b-lead-qualifier) | Score and grade leads based on company signals | Grades (A-F) convert to HubSpot lead statuses automatically |
| [Email Pattern Finder](https://apify.com/ryanclinton/email-pattern-finder) | Discover email patterns and predict addresses | Find more contacts to push as HubSpot contacts |
| [Bulk Email Verifier](https://apify.com/ryanclinton/bulk-email-verifier) | Verify email deliverability before CRM push | Clean your email list before pushing to HubSpot |

# Actor input Schema

## `mode` (type: `string`):

Preset bundle for common CRM-push jobs. 'prospect-import' = cold-outbound import (companies + contacts + deals, lifecycle=lead). 'crm-hygiene-sync' = weekly refresh (no deals). 'signup-gate' = newsletter capture (subscribers, no deals). 'event-attendees' = webinar/event imports (MQL contacts + deals). 'account-based-push' = ABM mode: one deal per company, decision-maker contacts only, full Contact↔Deal + Company↔Deal associations. 'audit-only' = dry-run mapping preview. 'raw' = no preset, apply your own flags. Explicit createCompanies/createContacts/createDeals/lifecycleStage values always override the preset.

## `hubspotAccessToken` (type: `string`):

Private app access token from HubSpot. Create one at Settings > Integrations > Private Apps. Required scopes: crm.objects.contacts.write, crm.objects.companies.write, crm.objects.deals.write. Leave empty for dry-run mode.

## `leads` (type: `array`):

Lead data to push to HubSpot. Each object should have at least a domain or email. Supports output from B2B Lead Gen Suite, Google Maps Lead Enricher, Lead Scoring Engine, Lead Enrichment Pipeline, or custom data. Capped at 5000 leads per run.

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

Apify dataset ID to pull leads from (alternative to inline leads). Use this to chain with other actors like B2B Lead Gen Suite, Lead Scoring Engine, or Lead Enrichment Pipeline.

## `createCompanies` (type: `boolean`):

Create or update HubSpot company records from lead domains. Override the mode preset if set explicitly.

## `createContacts` (type: `boolean`):

Create or update HubSpot contact records from lead emails and contacts. Override the mode preset if set explicitly.

## `createDeals` (type: `boolean`):

Create a HubSpot deal for each lead. Useful for sales pipeline automation. Override the mode preset if set explicitly.

## `dealStage` (type: `string`):

HubSpot deal stage for new deals. Common values: appointmentscheduled, qualifiedtobuy, presentationscheduled, decisionmakerboughtin, contractsent, closedwon, closedlost.

## `lifecycleStage` (type: `string`):

HubSpot lifecycle stage for new contacts. Values: subscriber, lead, marketingqualifiedlead, salesqualifiedlead, opportunity, customer, evangelist, other. Override the mode preset if set explicitly.

## `dryRun` (type: `boolean`):

Preview the mapped HubSpot data without actually pushing to CRM. Useful for verifying field mapping before committing. Auto-on when mode='audit-only'.

## `outputProfile` (type: `string`):

Shape the dataset records for different consumers. 'minimal' = ids + status only (CSV-friendly). 'standard' = full result minus diagnostic blocks. 'full' = every field including decisionSnapshot for audit/replay. 'llm' = decision-friendly subset for AI agents.

## `systemMode` (type: `boolean`):

Auto-enables batch insights (run-level list-quality summary) and cohort insights (per-domain rollup) without setting them individually. Use when integrating into recurring workflows.

## `includeBatchInsights` (type: `boolean`):

Append a run-level summary record with success rate, failure category distribution, and sibling-actor recommendations. Auto-on with systemMode.

## `includeCohortInsights` (type: `boolean`):

Group leads by canonical domain and emit per-domain push outcome on each record + a COHORT\_INSIGHTS key in KV. Useful for ABM workflows. Auto-on with systemMode.

## `pushStrategy` (type: `object`):

Fine-grained control over WHO gets pushed and HOW. contactPolicy ('all' | 'best-only' | 'role-based') filters which contacts per lead get pushed (best-only = top by seniority; role-based = only senior roles). dealPolicy ('one-per-lead' | 'one-per-company' | 'one-per-contact' | 'score-threshold' | 'none') controls deal creation strategy. dealScoreThreshold (default 70) is used when dealPolicy='score-threshold'. companyPolicy ('always' | 'only-if-new') skips company updates on existing companies when 'only-if-new'. associationDepth ('minimal' = no associations, 'standard' = Contact↔Company only, 'full' = adds Contact↔Deal + Company↔Deal). seniorityRoles (string array) overrides the default seniority keyword list for role-based contactPolicy. Each field is optional — defaults come from the mode preset.

## `pushRules` (type: `array`):

Programmable per-lead rules evaluated in order. Each rule has 'if' (conditions: score / grade / emails.count / contacts.count / industry / domain) and 'then' (actions: skip with reason, override lifecycleStage / dealStage / createDeals / createCompanies / createContacts). Rules with skip:true halt evaluation for that lead. Useful for branching CRM behavior without external orchestration. Example: \[{"if":{"score":{"gte":80}},"then":{"lifecycleStage":"salesqualifiedlead","createDeals":true}},{"if":{"emails":{"count":{"eq":0}}},"then":{"skip":true,"skipReason":"no\_contacts"}}]

## `qualityGate` (type: `object`):

Prevent garbage leads from reaching HubSpot AND from being charged. Set any combination: requireEmail / requireDomain / requirePhone (booleans), minScore (number; lead.score must meet), minFields (number; at least N input fields populated), requireDecisionMaker (boolean; at least one contact with senior title). Failing leads are output as recordType:'skipped' with a skipReason — never billed.

## `fieldOverrides` (type: `object`):

Remap default HubSpot property names before sending. Useful when your HubSpot account uses custom property names. Shape: {company: {industry: 'customIndustryField'}, contact: {jobtitle: 'role'}, deal: {dealname: 'opportunityName'}}. Only the specified keys are remapped; everything else is sent as-is.

## `watchlistName` (type: `string`):

Name a watchlist to enable cross-run state: skip leads already pushed in a prior run on the same watchlist (replayMode='skip'), and emit run-over-run delta on the summary record. Leave empty to disable. Each watchlist gets its own named KV store ('hubspot-lead-pusher-state-<name>') bounded at 50K eventIds.

## `monitorStateKey` (type: `string`):

Suite-aligned alias for watchlistName. Either input works; if both are set, watchlistName wins. Lets the same upstream orchestrator pass one consistent field name across hubspot-lead-pusher, salesforce-lead-pusher, lead-scoring-engine, bulk-email-verifier, waterfall-contact-enrichment, phone-number-finder, company-deep-research, and lead-enrichment-pipeline.

## `lastAction` (type: `object`):

Optional. Tells the actor what action you took on this watchlist since the last run. On the next scheduled run, the actor compares the prior push outcome against the current one and emits decisionMemory with an inferred outcome. Honest: only push-result delta is observable — direct CRM-side activity (deal stage, contact engagement) is not. Shape: { type: 'pushed-batch' | 'manual-review' | string, takenAt: ISO date, note?: string }. Requires watchlistName / monitorStateKey.

## `replayMode` (type: `string`):

When watchlistName is set: 'skip' (default) skips leads whose eventId already appears in the watchlist's processed set — prevents accidental double-billing on re-runs. 'force' pushes regardless (useful for full refresh runs).

## `safety` (type: `object`):

Hard caps that abort the run BEFORE any HubSpot writes if exceeded — protects against runaway input or upstream bugs. Set any combination: maxDealsPerRun (number; abort if planned deal count would exceed), maxCompaniesPerRun (number; same for companies), maxContactsPerCompany (number; cap contacts per domain), abortIfDuplicateRate (0-1; abort if input has more than this fraction of duplicate domains — likely upstream dedup issue), abortIfErrorRate (0-1; reserved for future error-rate-based abort). On abort, the actor pushes an error record and exits cleanly — no partial CRM writes.

## Actor input object example

```json
{
  "mode": "raw",
  "leads": [
    {
      "domain": "apify.com",
      "companyName": "Apify",
      "emails": [
        "hello@apify.com"
      ],
      "phones": [
        "+420-234-567-891"
      ],
      "contacts": [
        {
          "name": "Jan Curn",
          "title": "CEO",
          "email": "jan@apify.com"
        }
      ],
      "score": 85,
      "grade": "A"
    }
  ],
  "createDeals": false,
  "dealStage": "appointmentscheduled",
  "lifecycleStage": "lead",
  "dryRun": true,
  "outputProfile": "standard",
  "systemMode": false,
  "pushStrategy": {},
  "pushRules": [],
  "qualityGate": {},
  "fieldOverrides": {},
  "replayMode": "skip",
  "safety": {}
}
```

# Actor output Schema

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

No description

# 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 = {
    "leads": [
        {
            "domain": "apify.com",
            "companyName": "Apify",
            "emails": [
                "hello@apify.com"
            ],
            "phones": [
                "+420-234-567-891"
            ],
            "contacts": [
                {
                    "name": "Jan Curn",
                    "title": "CEO",
                    "email": "jan@apify.com"
                }
            ],
            "score": 85,
            "grade": "A"
        }
    ],
    "dryRun": true,
    "pushStrategy": {},
    "pushRules": [],
    "qualityGate": {},
    "fieldOverrides": {},
    "safety": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryanclinton/hubspot-lead-pusher").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 = {
    "leads": [{
            "domain": "apify.com",
            "companyName": "Apify",
            "emails": ["hello@apify.com"],
            "phones": ["+420-234-567-891"],
            "contacts": [{
                    "name": "Jan Curn",
                    "title": "CEO",
                    "email": "jan@apify.com",
                }],
            "score": 85,
            "grade": "A",
        }],
    "dryRun": True,
    "pushStrategy": {},
    "pushRules": [],
    "qualityGate": {},
    "fieldOverrides": {},
    "safety": {},
}

# Run the Actor and wait for it to finish
run = client.actor("ryanclinton/hubspot-lead-pusher").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 '{
  "leads": [
    {
      "domain": "apify.com",
      "companyName": "Apify",
      "emails": [
        "hello@apify.com"
      ],
      "phones": [
        "+420-234-567-891"
      ],
      "contacts": [
        {
          "name": "Jan Curn",
          "title": "CEO",
          "email": "jan@apify.com"
        }
      ],
      "score": 85,
      "grade": "A"
    }
  ],
  "dryRun": true,
  "pushStrategy": {},
  "pushRules": [],
  "qualityGate": {},
  "fieldOverrides": {},
  "safety": {}
}' |
apify call ryanclinton/hubspot-lead-pusher --silent --output-dataset

```

## MCP server setup

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

```

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/8yMnfuvE3hFBUddBH/builds/gDKCg36VfJgUeo4uI/openapi.json
