Install
openclaw skills install @cargo-ai/crm-deduplicationKeep 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. Triggers: "deduplicate our CRM accounts", "deduplicate CRM contacts", "our CRM has duplicate people", "merge duplicate contacts in HubSpot", "we keep creating duplicate account records", "merge duplicate companies in HubSpot", "set up recurring CRM deduplication", "review ambiguous duplicates". HubSpot, Salesforce, Attio, Slack, LinkedIn profiles, phone numbers, Cargo CDK, findRecords, Scoring, Human Review, mergeRecords. Skip when: the request is to add or refresh CRM data rather than merge duplicate records; use crm-enrichment.
openclaw skills install @cargo-ai/crm-deduplicationState: to-be-approved. Deploy-verified against a live workspace: not yet. Treat Done when
below as the acceptance test and review cargo-ai cdk plan before deploying. Make no outcome claim
for this skill until it is approved.
Choose the account path, contact path, or both. Each selected path has its own CRM model, identity audit, matching policy, survivor policy, disabled play, cost preview, pilot approval, and acceptance criteria. Approval for one path does not authorize the other.
deduplicate_accounts searches live CRM companies by LinkedIn company ID, LinkedIn company page,
and non-generic domain. It scores the evidence, selects a deterministic survivor, and merges only
an exact shared LinkedIn company ID when there is no identity, protected-ID, or parent-subsidiary
conflict. Every other candidate reaches Cargo's native Human Review node. Company name never
creates or scores a candidate.
The default survivor policy prefers protected business IDs, customers, commercial activity, populated records, recent activity, older creation time, then the smallest CRM record ID.
deduplicate_contacts searches live CRM contacts by LinkedIn person ID, normalized LinkedIn URL,
exact email, and exact stored phone. A second search expands the direct results into transitive
clusters using normalized identity values from retrieved records. Phone normalization also compares
records inside the cluster. The play merges automatically only on an exact LinkedIn person ID,
exact LinkedIn URL without a person-ID conflict, exact non-generic email without a LinkedIn conflict,
or a conflict-free transitive chain of those classes.
LinkedIn identity conflicts, generic or shared email, and phone-only matches never merge automatically. Low-confidence clusters reach Human Review when that path is enabled and otherwise remain untouched. The native merge is the final CRM write. Normalization, enrichment, and association changes belong in a separately approved workflow.
Both plays run directly on authoritative CRM-backed models, refresh candidates from the CRM before scoring, and stop when the queued source record is missing or no duplicate remains. Automatic and approved Human Review paths are the only merge paths. Decline or timeout leaves records separate. No candidate or staging model is created.
The checked example in infra/ is HubSpot. Salesforce and Attio adapt the connector, extractor,
record-ID field, search action, merge action, and property slugs together. Keep one CRM shape across
the selected paths.
When matching-key coverage is weak, recommend crm-enrichment before building the affected path.
That is a recommendation, not a dependency: crm-deduplication installs and operates independently.
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:
#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.
This folder is a worked example: real CDK resources written for another company. The job is to end with the code this company would have written in its project.
Install the required authoring skill first. If cargo-cdk is absent, run:
npx skills add getcargohq/cargo-skills --skill cargo-cdk
Read .agents/skills/cargo-cdk/SKILL.md directly after installation. Complete its bootstrap and use
its authoring, state, plan, and deployment rules throughout this pipeline.
cargo-ai cdk add cookbook/crm-deduplication writes this example to
infra/crm-deduplication/ and this procedure to .claude/skills/crm-deduplication/. No project
yet? cargo-ai cdk init <dir> --cookbook crm-deduplication && cd <dir> && npm install does both;
this folder never ships a shell. If you are reading this from the project's .claude/skills/,
the install already happened. Start at step 2. On a CLI too old to have add, copy this folder
in as a sibling of what is there by hand; everything below is unchanged..env.example; preserve existing
content.references/audit.md. Present its identity coverage, candidate classes,
conflicts, survivor policy, automatic class, and review destination. Stop for approval of that
path's policy and disabled deployment. Approval does not carry across paths.references/configure.md. Record adaptations under ## Decisions in
the copied skill. Run the executable contract, generated types, check, and plan. Inspect the
selected graph and deploy only its approved resources with isEnabled: false. Never run
cargo-ai cdk init --force in a non-empty directory.references/run.md, then walk its section in
evals/acceptance.md line by line.Verify the live company schema, record ID, LinkedIn company properties, domain property, protected
business identifiers, parent-company property, and survivor inputs. The account audit and policy
must be approved before adapting crm_accounts or deduplicate_accounts.
Verify the live contact schema, record ID, LinkedIn person properties, email, phone, association
evidence, and survivor inputs. Decide whether low-confidence clusters enter Human Review or remain
untouched. The contact audit and policy must be approved before adapting crm_contacts or
deduplicate_contacts.
Resolve the default CRM connector for every selected path. Account deduplication requires Slack
Human Review. Contact deduplication requires Slack only when low-confidence review is enabled.
Replace each active PLACEHOLDER_REVIEW_CHANNEL_ID with its approved destination. If both paths use
one channel, each still needs its own approved review policy and pilot. No paid call, review request,
or CRM write occurs during either audit.
Derive what can be discovered before asking the operator. Ask only for decisions that change the selected path's candidate population, merge policy, review ownership, or spend.
| Input | Kind | How it is answered | Why it matters |
|---|---|---|---|
crm_shape | derived | Inspect authenticated connectors, selected extracts, and generated action types | Sets objects, IDs, search, merge, and property behavior |
selected_paths | derived | Read whether the request names account deduplication, contact deduplication, or both | Sets which audits, resources, approvals, and pilots are in scope |
action_costs | derived | Read current CRM metadata plus Slack metadata for paths with active review | Each path's handoff must disclose its current maximum cost |
| Input | Kind | How it is answered | Why it matters |
|---|---|---|---|
account_evidence | derived | Audit company identifiers, conflicts, candidate classes, and survivor inputs | Grounds the account policy in current CRM records |
account_policy | asked | Review keys, protected fields, survivor precedence, score, and automatic class | Controls every company candidate and unattended merge |
account_review | asked | Select Slack channel, owner, and timeout for uncertain company clusters | Gives manual company decisions an accountable owner |
account_authorizations | asked | Approve the disabled build, then the exact merge-capable pilot as separate decisions | Separates deployment from company-record mutation |
| Input | Kind | How it is answered | Why it matters |
|---|---|---|---|
contact_evidence | derived | Audit person identifiers, conflicts, candidate classes, and survivor inputs | Grounds the contact policy in current CRM records |
contact_policy | asked | Review keys, global guards, survivor precedence, and automatic classes | Controls every person candidate and unattended merge |
contact_review | asked | Decide whether low-confidence groups enter review; if yes, approve channel and timeout | Defines the non-automatic contact path |
contact_authorizations | asked | Approve the disabled build, then the exact merge-capable pilot as separate decisions | Separates deployment from contact-record mutation |
Offer only variations relevant to the selected path. Record each approved variation with its effect on candidates, safeguards, and validation.
| Variation | When it is right | How | What it costs |
|---|---|---|---|
crm | The consumer uses Salesforce or Attio | Replace connector, extract, record ID, search, merge, and properties across selected paths | Generated types and native merge semantics must be revalidated |
structured_ai_review | Ambiguous evidence needs a concise review aid | Add priced structured evidence before Human Review only; never use it to bypass deterministic guards | Adds model cost and a non-deterministic review surface |
| Variation | When it is right | How | What it costs |
|---|---|---|---|
account_matching_keys | The CRM has another approved durable company identity | Add it to search, normalization, evidence, conflict gates, review, and contract tests | Wider matching can create new false-positive company classes |
account_survivor_precedence | Protected lifecycle, billing, tier, or customer policy must win | Change the account ranker and its contract fixtures together | A policy change can select a different survivor for every cluster |
| Variation | When it is right | How | What it costs |
|---|---|---|---|
contact_matching_keys | The CRM has another approved durable person identity | Add it to both searches, normalization, evidence, conflict gates, review, and tests | Wider matching can create new false-positive person classes |
contact_survivor_precedence | Commercial history or activity policy differs | Change the contact ranker and its contract fixtures together | A policy change can select a different survivor for every group |
low_confidence_review | The operator wants phone-only or ambiguous groups surfaced | Keep Human Review enabled, or remove that branch and update the contact contract so those groups end untouched | Review volume changes; disabled review leaves more duplicates |
infra/plays/) Each selected play uses its object-specific
CRM extract and record ID. A candidate model introduces another identity system and can target
the wrong record.infra/plays/) The CRM search refreshes candidate
membership for every run. Audit snapshots can become stale before a merge.infra/scripts/) Deterministic preparation retains the
fresh source once, normalizes evidence, and selects one survivor before the automatic gate.infra/plays/) Human approval reaches the
reviewed merge. Decline and timeout end without a CRM write.infra/plays/) A source missing from the fresh search ends before
scoring or emitting merge IDs.infra/plays/) Each selected play remains
disabled, noConcurrency, and limited to 15 rows until its verified pilot is approved for
expansion.infra/plays/deduplicate-accounts.ts) Exact shared
LinkedIn company ID, score at least 60, and no identity, protected-ID, or parent-subsidiary
conflict are all required. Every other candidate reaches Human Review.infra/scripts/accounts/) Company name is excluded from candidate
generation and scoring. The checked review payload does not rely on it.infra/scripts/contacts/evidence.ts) Every automatic contact
class is disqualified by a LinkedIn identity conflict or generic or shared email.infra/plays/deduplicate-contacts.ts) The second
live search gathers every high-confidence identity key discovered by the direct search.infra/plays/deduplicate-contacts.ts) Deduplication creates no
post-merge update node. A separate approved workflow owns normalization, enrichment, and
association changes.node --import tsx evals/contract.mjs, cargo-ai cdk types, cargo-ai cdk check, and
cargo-ai cdk plan pass after adaptationnoConcurrency, and limited to 15 CRM rows before its pilotdeduplicate_accounts runs directly on crm_accounts and searches companies livededuplicate_contacts runs directly on crm_contacts and performs both direct and transitive live
searchesImmediately 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.
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.
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.
crm-enrichment when either object path lacks reliable matching-key coverageaccount-scoring after duplicate account records have been consolidated into authoritative survivors