Agent skill
crm-sync-expert
Designs a CRM synchronisation before anyone runs one — which object a row belongs to, which key deduplicates it, what wins on a collision, and what the CRM will silently truncate, coerce or drop on import.
Filed under CRM and RevOps.
From richapiai/gtm-skills · 34 skill entries · 0 · pushed 2026-09-18
What it does when it runs
Designs a CRM synchronisation before anyone runs one — which object a row belongs to, which key deduplicates it, what wins on a collision, and what the CRM will silently truncate, coerce or drop on import. Use when asked "sync this to HubSpot", "push to Salesforce", "map these fields", "which dedupe key", "two-way sync", "custom fields for enriched data", "lifecycle stage", or "why did my import create duplicates". It advises and prepares; it never writes to a CRM, because no endpoint in this pack can. (richapi-gtm)
Automated analysis of the skill and the 0 files bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.
- Keys and connectors you must supply
- None found.
- Hosts it reaches
- No third-party host appears in the skill or its bundled files.
- Tool permissions it declares
- Bash(richapi:*)
- Bash(richapi-skills-preflight:*)
- Bash(node:*)
- Bash(head:*)
- Bash(tr:*)
- Bash(cat:*)
- Read
- Write
- Actions present in the files
- shell
Install it
View source on GitHub ↗git clone --depth 1 --filter=blob:none --sparse https://github.com/richapiai/gtm-skills.git /tmp/gtm-skills git -C /tmp/gtm-skills sparse-checkout set "skills/crm-sync-expert" mkdir -p ~/.claude/skills/crm-sync-expert cp -R "/tmp/gtm-skills/skills/crm-sync-expert/." ~/.claude/skills/crm-sync-expert/
Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗
Or take the whole library
This repo ships a .claude-plugin manifest, so Claude Code can install all 34 skills at once. Plugin skills are invoked as /<plugin>:<skill>, so they never collide with your own.
/plugin marketplace add richapiai/gtm-skills /plugin
The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.
The skill
Source on GitHub ↗Reproduced in full from richapiai/gtm-skills/blob/c1a5d5881be89b65b155d772aef4b491aef3354e/skills/crm-sync-expert/SKILL.md, which is licensed MIT (repository). 3,016 words, 14 headings.
CRM sync — design the mapping, then hand the file over
A botched CRM mapping is not a bug you notice. It is a quarter of enrichment landing in the wrong column, a picklist that grew a one-off value per import, and a duplicate population nobody sees until someone runs a report. It costs a day of cleanup at best and a re-import at worst. This skill exists to make that day not happen.
It is worth being blunt about the shape of the help on offer, because the honest version is more useful than the flattering one.
What this skill does, and the one thing it cannot do
It does not execute a sync. There is no CRM write endpoint in this pack. Not one
of the endpoints in _lib/api-catalog.json writes to
HubSpot, Salesforce, Pipedrive, GoHighLevel or anything else; every one of them reads.
That is not an oversight this skill is working around.
ROADMAP.md lists true two-way
CRM sync execution under blocked on the API growing: the design exists, the endpoint
does not, and the honest word for that is "blocked", not "coming".
So the boundary is hard, and it runs right through the middle of the job:
| The pack does this | Someone else does this |
|---|---|
| Decides the object, the key, the conflict rule | Creates the custom fields in the CRM |
| Names what will truncate, coerce or be rejected | Runs the import, the API job or the iPaaS scenario |
| Prepares the file the importer reads | Confirms the records actually changed |
| Says what a re-run will do to existing records | Owns the undo |
Never say "synced". Not in a summary, not in a status line, not in a report. The words this skill is allowed to use are mapped, prepared, planned, and handed off. A user who reads "synced to HubSpot" and closes the terminal believes their CRM is current when nothing has been written to it, and every decision they make afterwards rests on that. It is the single worst outcome available here and it is available for free, in one careless sentence.
What it spends
Nothing. This skill owns no endpoints —
_lib/endpoint-owners.yaml assigns it none, which is
correct rather than an omission, because designing a mapping is reasoning over a file
that already exists. It makes zero paid calls, so there is no plan to approve and no
ledger line to read back. Law 3 asks that every paid call be named and costed; naming
none is how that law is satisfied here.
If the mapping turns out to need data the file does not carry, that is an enrichment
question and it belongs to /enrich-waterfall, which
will price it. Do not quietly grow this skill a budget.
Inference mode — local, always
Mode: local inference only. Zero LLM hops.
ai_enrich is not in this skill's endpoint set. Deciding that a column called
co_name is the company name, or that a CRM's Industry picklist has no value for
"Vertical SaaS", is reasoning over text already on disk — and this pack already runs
inside a model that does that at no charge. Paying an endpoint to re-read a CSV header
is exactly the waste local inference exists to stop.
The two reasons the pack permits an ai_enrich call cannot arise here. Perplexity web
grounding does not apply: nothing in a field mapping is a question about the public
web. Batch scale does not apply either: a mapping is decided once for a file, not
once per row — if you find yourself wanting a model call per row, the mapping is wrong,
not under-resourced.
Before anything else
richapi-skills-preflight
API_KEY_SET: no— not a blocker. This skill makes no paid call. Say so plainly instead of sending the user to find a key they do not need.CATALOG_OK: no— regenerate withrichapi catalog gen. You will be readingfield_map_statusout of the catalog in step 2, and a stale catalog is the wrong answer to a question about what the API actually returns.SUPPRESSION: STOP— this one is a blocker for the handoff, though not for the conversation. A CRM is a sending machine with a database attached: once a suppressed contact is inside it, the CRM's own workflows will re-sequence them, and the pack's suppression store never gets consulted again. Run./setupbefore any file leaves.
Step 1 — decide which question this actually is
Five different asks arrive wearing the word "sync". They have different right answers and only one of them is a mapping problem.
| The ask | The right answer |
|---|---|
| One-time load of a prospect list | A file, produced by the pack's export skill, imported by the CRM's own importer |
| Recurring refresh of records the CRM already holds | A scheduled job outside this pack, reading a file this skill helped shape |
| React the moment a record changes stage | A webhook from the CRM into whatever the user already runs. The pack is not in this loop |
| Genuine bidirectional sync | An iPaaS or bespoke integration. Not this pack, and not soon — ROADMAP.md |
| "We have not picked a CRM" | A selection conversation. Ask before you map |
If the user has not said which, ask. Mapping for a one-time import and mapping for a recurring upsert differ on the only question that matters — what happens the second time a row arrives.
Step 2 — derive the mapping from the file, never from a documented schema
This is the rule that most separates this skill from a plausible-sounding one, and it comes straight out of law 2: the spec is a cost-and-route source, not a schema source.
Check it yourself rather than taking it on trust:
richapi catalog gen
node -e "const c=require('./_lib/api-catalog.json');
const s=new Set(Object.values(c.endpoints).map(e=>e.field_map_status));
console.log([...s]);"
field_map is null for every endpoint in the generated catalog. Most endpoints now
report a field_map_status of keys_from_spec_example and publish a field_map_keys
array — the top-level key NAMES the spec's 200 example declares, nothing more; the rest
report TODO_no_usable_example, having no usable example at all. A key list is not a
field map: it says nothing about what a key means, its type, whether it is always
present, or what is nested under it, because the response examples in the spec are
mostly the literal string example. Live fixtures have not been captured yet.
The consequence is concrete: there is no authoritative list of the field names the API returns, so a mapping table written from a remembered response shape is fiction that will pass review and fail at import. Read the actual output file instead:
head -1 gtm/lists/q3-uk.csv | tr ',' '\n' | cat -n
Map from those column names, and record, for every target field, where the source column came from. A mapping whose left-hand side nobody verified is the same defect as a hand-typed credit number: it looks authoritative and it is stale on arrival.
The pack's freshness classes are the right anchor for an *_enriched_at column:
firmographic data is treated as good for gates.yaml:cache_ttl.classes.firmographics,
so a record older than that is a candidate for refresh rather than a fact.
Step 3 — pick the dedupe key, and write down what a collision does
Every CRM deduplicates, none of them the same way, and the failure is silent in all of them. Decide two things explicitly and put both in the mapping document.
The key. Email is the default in most CRMs and it is a poor stable identifier: it is the first thing that changes when someone moves job, which is precisely the population a GTM pack keeps finding. Prefer, in order:
- An identifier you assign and keep — a row id you control changes never.
- The LinkedIn URL — changes rarely, and this pack can usually supply one.
- Email — changes often, and must be lower-cased and trimmed before any comparison.
[email protected]and[email protected]are two contacts in every CRM that matters.
The collision rule. Name it before the import, not after:
| Rule | Use when |
|---|---|
create_only | First load into an empty object. Fails loudly on an existing key |
update_blank_only | The safe default for enrichment. Fills gaps, never overwrites a human |
overwrite | Only where the pack is the acknowledged system of record for that field |
append | Multi-value fields, and only where the CRM actually supports it |
update_blank_only is the default this skill recommends because the alternative failure
is unrecoverable: overwriting a rep's hand-typed job title with a stale enriched one
destroys the better value and leaves no trace that it happened.
A note on the direction nobody plans for: the same file imported twice with
create_only produces duplicates, and with overwrite produces a silent rollback of
every edit made since the first import. Ask what the second run is meant to do.
Step 4 — name what the CRM will silently damage
Rejections are the good case; the user sees them. These are the ones that succeed:
- Picklist values outside the defined set. Depending on the CRM, an unknown value
is dropped, coerced, or added — and "added" is how a picklist reaches four hundred
values. Constrain to a fixed set. Where the field is a search taxonomy the API also
uses,
_lib/filters-catalog.jsonholds the real label sets for seniority, function and company size; use those rather than inventing parallel ones. - Text length caps. Long fields (headline, summary, description) are truncated at the field's limit without a warning. Decide what gets truncated deliberately.
- The pack's explicit nulls. This pack has exactly one way to say "no value":
not_found,not_verifiable,not_applicable, defined in_lib/dual-contract.schema.json. A CRM has no such vocabulary. Never let those strings reach a CRM text field — that is hownot_foundbecomes four hundred people's job title and then shows up in a merge tag. Decide per field: either the cell is blank, or you create a real picklist value that means the same thing. The mapping document must say which, for every field. - Type coercion. A string
"false"is truthy on import in more places than it should be; a leading+on a phone number is eaten by a spreadsheet before the CRM ever sees it; leading zeros vanish from postcodes the same way. If a file passes through a spreadsheet between here and the CRM, assume all three happened. - Display name versus internal name. Most import failures are this. The CRM's import mapper wants the internal API name, and the column header the user is looking at is the label. Confirm which one the importer reads before blaming the data.
- Character encoding. A UTF-8 byte-order mark in the first header cell makes the first column unmappable in several importers, and the error message never mentions it.
Step 5 — the object model, and only the parts that do not rot
Structural facts about a CRM's object model are stable and worth stating. Rate limits, API versions, plan tiers and pricing are not, and this skill does not state them from memory — that is the same failure mode law 1 was written for, one abstraction up. Sixteen of fifty-three surviving endpoints in this pack's own API repriced in four months; a third party's rate limit is no more durable than that.
- HubSpot — Contacts (person), Companies (organisation), Deals, Tickets. Associations are first-class and labelled. Contact dedupe is native on email; a contact with no email will duplicate.
- Salesforce — Lead is pre-qualification and converts, irreversibly, into Contact plus Account plus Opportunity. A Lead and a Contact with the same email may coexist by design, which is where most "dedupe is broken" reports come from. Dedupe is rule-based, not field-based. Custom field type cannot be changed once records exist, so pick the end-state type on day one.
- Pipedrive — Person, Organization, Deal. Deliberately shallow, and that is a feature for a sales-only team.
- GoHighLevel — Contacts live inside a sub-account and there is no Company object. Company data is custom fields on the contact and tags are the primary segmentation. Do not model it like HubSpot; it will not hold.
For anything version-specific — limits, endpoint shapes, which auth flow is current — tell the user to read the vendor's current documentation. Saying "check the docs" is a worse answer than a confident number only if the confident number is right.
Step 6 — write the mapping document, then hand off
The artifact this skill produces is a mapping document, not a synchronised CRM. Write it where the user can commit it, and give every row four columns: target field, source column, transform, collision rule. Add a fifth for the explicit-null decision.
Then hand off honestly:
- The file itself is written by
/crm-export, which is the sole writer of that artifact. Hand it the mapping document — that skill reads it asMAP=and it overrides the built-in defaults, which are explicitly unverified against any particular CRM instance. This skill does not write the file and neither should you improvise one: a second writer is how two files with the same name disagree. - A contact list bound for a sender is a different artifact with a different gate.
Only
/launchwrites that one, and only against a PASS verdict from/campaign-review. - Before anything leaves,
/complydecides whether these contacts may be contacted at all, under the regimes ingates.yaml:skills.comply.jurisdictions. A CRM import that bypasses that gate re-arms every contact in it. - If coverage on the list is below
gates.yaml:quality_stops.coverage_min_pct, syncing it imports the gaps too. Fix the list first; a CRM makes bad data permanent and expensive.
Close by telling the user exactly what has and has not happened: the mapping is decided, the document is at this path, and their CRM is unchanged.
Step 7 — find the path that will actually run the sync
The mapping document is the deliverable and it is inert. Before closing, find out what
the user has to execute it with, using the ladder in
docs/destination-handoff.md:
- An MCP server for this CRM in this session. Name it, and check the mapping against what it can actually express — if the design needs a custom field the MCP cannot create, that is a finding, and it belongs in the document rather than in a message that scrolls away.
- The CRM's API docs or Postman collection. Confirm the object, the upsert semantics and the field types the design assumes. A mapping derived from the file and never checked against the destination's real schema is the defect this skill exists to prevent, one level out.
- Neither. Say what you looked for and design for the file-import path, which is the conservative assumption anyway.
Record the answer in the mapping document under the destination, because the next person to run this sync needs to know whether an integration exists before they plan around one.
This does not make the skill a sync runner. It designs the sync and names the path;
the user executes it under their own credentials. ROADMAP.md puts two-way sync
permanently out of scope and that is unchanged.
What this skill will not do
- It will not sync anything. There is no CRM write endpoint in this pack and there
is not going to be one soon.
ROADMAP.mdlists true two-way CRM sync execution as blocked on the API growing — a design with no endpoint under it. Every write is performed by the user, their CRM's importer, or an integration platform they own. A report from this skill claiming any record was created or changed would be a fabrication: it cannot observe the CRM at all, so it has nothing to report. - It will not write the import file. That artifact belongs to
/crm-export, which suppression-filters at write time and ships a manifest. One writer per artifact, or the reviews attached to it are advice. - It will not write a sender export. That is
/launch's alone, gated on a PASS verdict bound to the list's content hash. - It will not clear a contact for contact.
/complyis the gate; a mapping that routes a suppressed contact into a CRM has defeated it. - It will not spend credits. It owns no endpoints and makes no paid calls.
- It will not quote a rate limit, API version or price from memory. Those rot faster than this file is revised, and a confidently wrong limit is worse than no limit.
- It will not assert a response field name the pack cannot verify.
field_mapisnullfor every endpoint, and no endpoint has a live-captured fixture; the mapping comes from the file on disk, or it is guesswork with a table around it. - It will not send, dial, post to LinkedIn, or host an inbox. Those are outside this pack permanently, not pending.
Related
/enrich-waterfall— fills the columns a mapping turns out to need, with a dry-run plan and a per-hop cost/list-hygiene— normalises and de-duplicates before the import, which is the only cheap time to do it/comply— the gate that decides whether these contacts may be contacted; a CRM import does not inherit its verdict/campaign-review— the verdict a list needs before any outbound artifact is written/launch— the sole writer of the sender-format export, for when the destination is a sending tool rather than a CRM/outreach-expert— the sending-side counterpart: domain, deliverability and sequence hygiene, also advisory/richapi-gtm— the router and the session receipt/crm-export— writes the import file this mapping describes, and reads the mapping document asMAP=- What is built, what is not, and what is blocked:
../../ROADMAP.md
Other skills for the same job
Different authors, same problem. Matched on the words in the skill name, across every library in the catalogue except this one.
- cross-crm-opportunity-sync by zapier · 342
- sheets-crm-sync by composio-community · 2
- expert-panel by ericosiu · 3,608
- meta-ads-expert by Varnan-Tech · 672
- suede-sync-packaging by JasonColapietro · 127
- crm-integration by manojbajaj95 · 104
- expert-pov by matteotitta · 62
- crm-hygiene-scanner by Othmane-Khadri · 56
Need help setting it up?
This page tells you what crm-sync-expert does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.
Book a call →The directory stays free. There is nothing gated behind this.