CRM deduplication
Keep CRM accounts and contacts duplicate-free: audit company and person identity, run recurring deduplication plays directly on CRM models, merge safe exact matches, and route uncertain clusters to manual review.
Set up the crm-deduplication cookbook in this project.
1. If the Cargo CLI is not installed yet, follow every step in https://api.getcargo.io/INSTALL.md
2. From the Cargo project, run: cargo-ai cdk add cookbook/crm-deduplication
3. Then follow .claude/skills/crm-deduplication/SKILL.md. Stop for my approval before any paid call, and stop at "cargo-ai project plan" before deploying anything.$cargo-ai cdk add cookbook/crm-deduplicationSay this to your agent
Merge duplicate HubSpot companies automatically only when the LinkedIn company ID matches, and send everything else to #crm-hygiene for a human to approve.
Illustrative output #
Fictional records, to show the shape of what comes back.
#crm-hygiene · Review CRM account merge into 18234
Duplicate score: 72/100
Survivor: 18234
Records to merge: 20911
Identity conflict: false
Protected ID conflict: false
Parent/subsidiary warning: false
Evidence:
18234, linkedin id none, linkedin northwind-traders, domain northwind.example, protected no, parent none
20911, linkedin id none, linkedin northwind-traders, domain northwind.example, protected no, parent none
[Approve] [Decline]
Two records sharing a LinkedIn page and a domain, but no LinkedIn company ID, wait for a reviewer; approve merges 20911 into 18234, while decline or 24 hours of silence leaves both untouched.
What you will be asked #
| Input | Why | |
|---|---|---|
| crm_shape | Looked up | Sets objects, IDs, search, merge, and property behavior |
| selected_paths | Looked up | Sets which audits, resources, approvals, and pilots are in scope |
| action_costs | Looked up | Each path’s handoff must disclose its current maximum cost |
| account_evidence | Looked up | Grounds the account policy in current CRM records |
| account_policy | Asked | Controls every company candidate and unattended merge |
| account_review | Asked | Gives manual company decisions an accountable owner |
| account_authorizations | Asked | Separates deployment from company-record mutation |
| contact_evidence | Looked up | Grounds the contact policy in current CRM records |
| contact_policy | Asked | Controls every person candidate and unattended merge |
| contact_review | Asked | Defines the non-automatic contact path |
| contact_authorizations | Asked | Separates deployment from contact-record mutation |
Done when #
- the operator selected account deduplication, contact deduplication, or both
- each selected path has its own reconciled audit, policy approval, disabled-build approval, priced pilot approval, and final report
- the selected resources use one CRM connector, a Slack connector for every path with active review, object-specific CRM models, and no candidate or staging model
- generated types confirm selected search and merge payloads plus Human Review for paths that use it
node --import tsx evals/contract.mjs,cargo-ai cdk types,cargo-ai cdk check, andcargo-ai cdk planpass after adaptation- every selected play is disabled,
noConcurrency, and limited to 15 CRM rows before its pilot - the account audit reconciles company identifier coverage, mutually exclusive classes, conflicts, survivor evidence, and proposed merge IDs to the source total
deduplicate_accountsruns directly oncrm_accountsand searches companies live- its graph contains preparation, native Scoring, the guarded Branch, native Human Review, and CRM merges only on automatic or approved paths
- automatic merge requires exact shared LinkedIn company ID and every account conflict guard
- every account survivor and absorbed company ID is verified in the CRM
- the contact audit reconciles source totals, person identifier coverage, unique candidate clusters, conflicts, survivor evidence, and proposed merge IDs, and reports overlapping matched classes separately
deduplicate_contactsruns directly oncrm_contactsand performs both direct and transitive live searches- its graph contains preparation, native Scoring, the guarded Branch, optional native Human Review, and CRM merges only on automatic or approved paths
- automatic merge requires one approved high-confidence class plus every global contact guard
- the merge is the final CRM write, and every contact survivor and absorbed ID is verified in the CRM
What it costs #
Immediately before each selected path’s preview, run
cargo-ai connection integration get <crm> and, when review is enabled,
cargo-ai connection integration get slack. Record the CLI version, lookup time, action slugs, and
current cost metadata. The repository does not hard-code pipeline action prices.
Path 1: Account cost
Price the account search, automatic or approved company merge, and Human Review actions against the exact account population. Approval authorizes only the company clusters shown for that pilot.
Path 2: Contact cost
Price both contact searches, automatic or approved contact merge, and enabled Human Review actions against the exact contact population. Approval authorizes only the person clusters shown for that pilot.
Enabling either recurring schedule is a separate final approval after that path’s pilot passes.
This pipeline has two independent paths. Deploy account deduplication, contact deduplication, or both. Each path has its own CRM model, identity audit, matching and survivor policies, disabled play, cost preview, pilot approval, and acceptance checklist. Both paths search the live CRM before each merge decision and avoid candidate or staging models.
Path 1: Account deduplication #
The account path searches companies by LinkedIn company ID, LinkedIn company page, and non-generic domain. Only an exact shared LinkedIn company ID without identity, protected-ID, or parent-subsidiary conflict can merge automatically. Every other candidate reaches Human Review.
flowchart LR
account["CRM account"] --> search["Search live companies"]
search --> evidence["Normalize, score, select survivor"]
evidence --> exact{"Exact LinkedIn ID<br/>and no conflict?"}
exact -->|yes| merge["Merge automatically"]
exact -->|no| review["Human Review"]
review -->|approved| merge
review -->|declined or timeout| stop["Keep separate"]
Account resources
| Resource | Kind | Purpose |
|---|---|---|
crm_accounts | Model | Direct CRM company extract |
deduplicate_accounts | Play | Search, score, review, and merge duplicate companies |
infra/scripts/accounts/ | Code | Normalize company identity, cluster, rank, and summarize |
Company name is excluded from candidate generation and scoring. The deterministic survivor policy protects business IDs and prefers customer and commercial history.
Path 2: Contact deduplication #
The contact path searches people by LinkedIn person ID, normalized LinkedIn URL, exact email, and exact stored phone. It expands direct matches with a second live search so high-confidence identity chains form one cluster. The second search and in-cluster comparison use normalized phone values derived from records already retrieved.
flowchart LR
contact["CRM contact"] --> direct["Search direct identity keys"]
direct --> transitive["Expand keys and search again"]
transitive --> evidence["Normalize, score, select survivor"]
evidence --> safe{"Approved identity class<br/>and no global conflict?"}
safe -->|yes| merge["Merge automatically"]
safe -->|no, review enabled| review["Human Review"]
safe -->|no, review disabled| stop["Keep separate"]
review -->|approved| merge
review -->|declined or timeout| stop
Contact resources
| Resource | Kind | Purpose |
|---|---|---|
crm_contacts | Model | Direct CRM contact extract |
deduplicate_contacts | Play | Search, score, review, and merge duplicate contacts |
infra/scripts/contacts/ | Code | Normalize person identity, expand clusters, rank, and review |
Exact LinkedIn person ID, conflict-free LinkedIn URL, conflict-free non-generic email, and conflict-free transitive chains are the automatic classes. A LinkedIn conflict, generic or shared email, or phone-only match always leaves the automatic path. The native merge is the final CRM write. Normalization and enrichment belong in a separate workflow.
Install and adapt #
This folder is a worked HubSpot example. Salesforce and Attio require adapting the connector, extracts, record IDs, search and merge actions, and property slugs together. Keep one CRM shape across selected paths.
This pipeline requires the cargo-cdk authoring skill:
npx skills add getcargohq/cargo-skills --skill cargo-cdk
From a Cargo CDK project:
cargo-ai cdk add cookbook/crm-deduplication
Before planning or deploying:
- Reconcile the default CRM connector and any active Slack review connector with compatible project resources.
- Choose the account path, contact path, or both. Remove unselected model and play files, then update the contract’s exact resource inventory, connector assertions, and object-specific assertions.
- Audit and approve each selected path independently.
- Resolve each selected path’s object-specific policy inputs, identity fields, survivor policy, and active review destination.
- Typecheck, inspect the compiled graph, and deploy only approved plays disabled.
- Request separate approval for each exact 15-row maximum merge-capable pilot.
Nothing in this folder deploys, runs, or touches customer data by itself.
Verify #
npm run typecheck
node scripts/check-pipelines.mjs
node --import tsx crm-deduplication/evals/contract.mjs
Use the selected sections in the audit contract, configuration guide, runbook, and two-path acceptance checklist.
File map #
| Path | Purpose |
|---|---|
SKILL.md | Route selection, approvals, invariants, and completion |
infra/connectors/ | Shared uncached CRM and Slack connectors |
infra/folders/ | Shared skill-owned model and play folders |
infra/scripts/common/ | Record reading and value helpers both paths share |
infra/models/crm-accounts.ts | Account path CRM extract |
infra/plays/deduplicate-accounts.ts | Account path workflow and trigger |
infra/scripts/accounts/ | Company identity, clustering, survivor policy, and evidence |
infra/models/crm-contacts.ts | Contact path CRM extract |
infra/plays/deduplicate-contacts.ts | Contact path workflow and trigger |
infra/scripts/contacts/ | Person identity, clustering, survivor policy, and evidence |
references/ | Object-specific audit, configuration, and run instructions |
evals/acceptance.md | Independent acceptance checklists for both paths |
evals/contract.mjs | Executable graph and safety contract |