agentleFS
getcargohqgetcargohq/gtm-skills/crm-enrichment/SKILL.md

crm-enrichment

Skill2 starsChanged 27 days agoMITView on GitHub

Clean up and enrich a messy CRM: fill the missing identifiers on accounts and contacts first (LinkedIn, domain, email), write them back into blank fields only, then deduplicate on those identifiers with crm-deduplication. Keeps records filled and refreshes them when they go stale. Triggers: "keep our CRM accounts filled", "keep our CRM contacts filled", "nobody refreshes the company records", "nobody refreshes the contact records", "enrich my CRM", "refresh stale firmographics", "every new CRM company", "every new CRM contact", "contacts are missing emails, LinkedIn URLs, or titles". HubSpot, Salesforce, Attio, Cargo project. Skip when: the records are not in a CRM. A supplied company list is enrich-company-data, a supplied LinkedIn URL list is enrich-linkedin-profile, and a one-time email lookup is find-work-email.

  • Reads credentials
  • Installs packages

The file

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 6 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Very specific:

    Every write fills blanks only, and cargo_last_enriched_at is stamped only after a successful write, so a contact where nothing resolved, …

    line 88
  2. Very specific:

    Verify the live CRM company schema and both LinkedIn company actions.

    line 125
  3. Distinctive:

    No paid call or CRM write occurs during either audit.

    line 139
  4. Distinctive:

    Keep every optional provider field as its own decision row.

    line 199
  5. Distinctive:

    Fill approved blanks only: Every business-field mapping uses a CRM-native blank-only flag or an equivalent fresh-read guard.

    line 211
  6. Very specific:

    One contact play and three tool resources: Keep contact orchestration in enrich_contacts and use only Find Email, Find LinkedIn Profile …

    line 230
  7. Distinctive:

    No unresolved write: When email cannot resolve to LinkedIn, stop without custom enrichment, CRM write, or successful freshness.

    line 234
  8. Distinctive:

    No contact creation or movement tracking: Update only the triggering contact.

    line 237
  9. Distinctive:

    Run the compiled graph contract from this skill's folder after every adaptation:

    line 240

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

CRM enrichment

State: to-be-approved. This pipeline has not been deploy-verified against a live workspace. Review cargo-ai project plan, the compiled graph, and the live connector schemas before deployment.

The outcome

Choose the account path, contact path, or both. Each selected path has its own model, tool contract, play, field approval, cost preview, pilot, and acceptance criteria. Approval for one path does not authorize the other.

Path 1: Account enrichment

enrich_accounts keeps approved company identity and firmographic fields filled. It calls account_enrichment, selects the LinkedIn company route or domain fallback, and writes approved blanks to the triggering CRM account. A row takes only one provider route.

The starting HubSpot destinations are linkedin_company_id, name, domain, website, linkedin_company_page, and numberofemployees.

Path 2: Contact enrichment

enrich_contacts is one play calling three gated tool resources:

  1. Cargo-native Find Email runs only when LinkedIn is present and email is blank.
  2. Cargo-native Find LinkedIn Profile from Email runs only when email is present and LinkedIn is blank.
  3. Custom Contact LinkedIn Enrichment runs only after a LinkedIn URL is available.
Starting identifiersCallsOutcome
Email and LinkedInContact LinkedIn EnrichmentFill approved LinkedIn identity and role blanks
LinkedIn onlyFind Email, then Contact LinkedIn EnrichmentFill email and approved LinkedIn blanks
Email onlyFind LinkedIn Profile from Email, then Contact LinkedIn EnrichmentStop without a write if no profile resolves
NeitherNoneStop without a write

The starting HubSpot destinations are email, linkedin_person_id, linkedin_profile_url, and jobtitle. This path contains no customer split, movement detection, relationship mutation, note creation, or alerting.

Shared write contract

Both plays run directly on their CRM extracts and match the CRM record ID. The custom tools own provider access and have no CRM access. The plays own every CRM write. Business fields fill blanks only. cargo_last_enriched_at and cargo_enrichment_status are stamped only after a successful write.

The checked example targets HubSpot companies and contacts by hs_object_id. Adapt the connector, extracts, record IDs, write action, and blank-only behavior together for Salesforce or Attio.

Example

Keep our HubSpot contacts filled: find the missing email or LinkedIn profile, then fill job title, but never overwrite a value a rep already entered.

Illustrative output, fictional records:

Contact (hs_object_id)Started withFields filledcargo_enrichment_status
Priya Raman (51007)LinkedIn onlyemail priya.raman@northwind.example, linkedin_person_id, jobtitle VP RevOpssucceeded
Tomás Ortega (51022)Email onlylinkedin_profile_url, linkedin_person_id, jobtitle Head of Sales Opssucceeded
Mei Lin (51040)Email and LinkedInjobtitle left as entered; linkedin_person_id filledsucceeded
Sam Hale (51055)Email onlynothing: no profile resolved(not stamped)

Every write fills blanks only, and cargo_last_enriched_at is stamped only after a successful write, so a contact where nothing resolved, like Sam Hale, is never marked enriched.

Put it in your project

Install the required authoring skill first. If cargo-project is absent, run:

npx skills add getcargohq/cargo-skills --skill cargo-project

Then read .agents/skills/cargo-project/SKILL.md and follow its state, plan, and deployment rules.

From the Cargo project, install this example with:

cargo-ai project add cookbook/crm-enrichment

Or start a project with:

cargo-ai project init <dir> --cookbook crm-enrichment
cd <dir>
npm install

Reconcile default connectors and models with existing project resources. Do not deploy duplicate slugs. Append environment requirements to the project .env.example; never overwrite it.

Choose the account path, contact path, or both before the audit. Follow the selected path sections in references/audit.md, references/configure.md, and references/run.md. Walk the matching path in evals/acceptance.md. If both paths are selected, both must pass separately.

Account path setup

Verify the live CRM company schema and both LinkedIn company actions. Approve the account field contract before adapting crm_accounts, account_enrichment, or enrich_accounts.

Contact path setup

Instantiate the two Cargo-native tools and replace:

  • REPLACE-WITH-FIND-EMAIL-TOOL-UUID
  • REPLACE-WITH-FIND-LINKEDIN-PROFILE-FROM-EMAIL-TOOL-UUID

Confirm their live contracts. This example expects Find Email to accept linkedin_url, first_name, and last_name and return email. It expects Find LinkedIn Profile from Email to accept email and return linkedin_url.

No paid call or CRM write occurs during either audit. Deploy selected resources disabled only after the matching field contract is approved. Run a paid pilot only after the operator approves that path's priced population.

What you will be asked

Derive what can be discovered before asking the operator. Ask only for decisions that change the selected path's field contract, target population, cadence, or spend.

Shared inputs

InputKindHow it is answeredWhy it matters
crm_shapederivedInspect authenticated connectors, selected extracts, and live CRM propertiesDetermines objects, record IDs, destinations, and write behavior
selected_pathsderivedRead whether the request names account enrichment, contact enrichment, or bothSets which resources, audits, approvals, and pilots are in scope

Path 1: Account inputs

InputKindHow it is answeredWhy it matters
account_provider_schemaderivedInspect both live LinkedIn company actionsPrevents guessed output paths and types
account_field_contractaskedReview one decision row per company provider propertyControls every company mutation
account_populationderivedCount mutually exclusive LinkedIn URL and domain fallback routesSets account scope and spend
account_runaskedReview disabled resource links, exact population, and maximum spendAuthorizes only the account pilot

Path 2: Contact inputs

InputKindHow it is answeredWhy it matters
contact_tool_contractsderivedInspect profile enrichment and both instantiated Cargo-native toolsPrevents guessed inputs, output paths, and prices
contact_field_contractaskedReview one decision row per contact provider propertyControls every contact mutation
contact_populationderivedCount both-identifiers, LinkedIn-only, email-only, and neither routesSets contact scope and spend
contact_runaskedReview disabled resource links, exact population, and maximum spendAuthorizes only the contact pilot

What you can change

Shared variations

VariationWhen it is rightHowWhat it costs
crm_shapeThe consumer uses Salesforce or AttioReplace the connector, selected extracts, object names, record IDs, write action, and blank-only guard togetherGenerated types and write semantics must be reverified
refresh_cadenceThe default window does not fit the data policyChange freshness independently for each selected path and report repeat-spend impactFaster cadence repeats provider charges more often

Path 1: Account variations

VariationWhen it is rightHowWhat it costs
account_fieldsThe approved company contract differs from the starting fieldsChange account tool outputs, CRM mappings, and transformations togetherEvery added field expands schema and write review
account_eligibilityOnly a governed company subset should be enrichedIntersect the account play filter with approved lifecycle, tier, or ownership conditionsNarrower scope reduces coverage and paid calls
account_refresh_policyApproved populated company fields must be refreshedRemove blank-only protection only for approved fields and compare against a fresh CRM readThe provider snapshot can replace CRM-authoritative values

Path 2: Contact variations

VariationWhen it is rightHowWhat it costs
contact_fieldsThe approved person contract differs from the starting fieldsChange custom tool outputs, contact mappings, blank filters, and transformations togetherEvery added field expands schema and write review
contact_eligibilityOnly a governed contact subset should be enrichedIntersect the contact play filter with approved lifecycle, tier, or ownership conditionsNarrower scope reduces coverage and paid calls
native_tool_contractsThe deployed native tools expose different schemasUpdate each toolRef, call payload, and result access from the verified live schemaA guessed path can create paid calls with no usable result
profile_providerThe workspace uses an equivalent approved profile actionReplace the provider call inside contact_linkedin_enrichment and remap its outputCoverage, fields, and unit price change

Keep every optional provider field as its own decision row. State the live unit price and expected fill rate before recommending a paid route.

What should not change

Shared invariants

  • Each path runs on its direct CRM extract. The account play matches the company record ID and the contact play matches the contact record ID. A second identity system makes writes look successful while nothing lands.
  • Tools enrich and plays write. Custom tools contain no CRM access. Plays do not duplicate provider connector actions.
  • Fill approved blanks only. Every business-field mapping uses a CRM-native blank-only flag or an equivalent fresh-read guard. Do not overwrite an authoritative CRM value without an explicit field-level refresh approval.
  • Freshness follows a write. Stamp cargo_last_enriched_at and cargo_enrichment_status=succeeded only on successful CRM write paths.
  • Selected plays deploy disabled and use noConcurrency. Removing either expands an unapproved pilot.

Path 1: Account invariants

  • LinkedIn first, domain fallback. One account takes exactly one provider route. Rows without either identifier make no paid call.
  • One account tool and one account play. account_enrichment owns provider routing; enrich_accounts owns the only company write.
  • Account eligibility does not require a blank destination. Populated stale records remain eligible for an explicitly approved refresh policy.

Path 2: Contact invariants

  • One contact play and three tool resources. Keep contact orchestration in enrich_contacts and use only Find Email, Find LinkedIn Profile from Email, and Contact LinkedIn Enrichment.
  • Real branch gating. Find Email runs only for LinkedIn-only rows. Find LinkedIn Profile from Email runs only for email-only rows. Contact LinkedIn Enrichment runs only with a LinkedIn URL.
  • No unresolved write. When email cannot resolve to LinkedIn, stop without custom enrichment, CRM write, or successful freshness.
  • Null-safe blank filters. Pair isNull and isEmpty for every contact string destination.
  • No contact creation or movement tracking. Update only the triggering contact. Do not create, merge, move, or alert on contacts in this path.

Run the compiled graph contract from this skill's folder after every adaptation:

node --import tsx evals/contract.mjs

Done when

Complete only the selected paths. If both were selected, both lists must pass.

Path 1: Account completion

  • The approved account field contract names every destination, type, transformation, and write policy.
  • The plan contains one account model, one account enrichment tool, and one disabled account play.
  • The compiled graph proves one mutually exclusive company provider route and one play-owned CRM write.
  • The account write matches the audited company record ID and preserves populated business values.
  • The account write probe and approved pilot pass.
  • The account report includes route, processed, written, skipped, failed, fill-rate, spend, and direct-link evidence.

Path 2: Contact completion

  • The approved contact field contract names every destination, type, transformation, and write policy.
  • Native tool UUIDs and live schemas are verified and placeholders are gone.
  • The plan contains one contact model, exactly three contact tool targets, and one disabled contact play.
  • The compiled graph proves both native calls are gated and every custom enrichment call has a LinkedIn URL.
  • Unresolved and identifier-free rows stop without custom enrichment, CRM write, or successful freshness.
  • The contact write probe and approved pilot pass.
  • The contact report includes route, processed, written, unresolved, skipped, failed, fill-rate, spend, and direct-link evidence.

The complete evidence checklist is in evals/acceptance.md.

What it costs

Fetch live prices from the workspace before every preview. Cost only the selected paths. A failed CRM write can cause provider calls to be billed again on retry, so probe write capability before a batch.

Path 1: Account cost

Quote both LinkedIn company actions. Multiply the mutually exclusive LinkedIn URL and domain fallback populations by their respective live unit prices. Rows with neither identifier make no paid call.

Path 2: Contact cost

Quote both Cargo-native tools and the LinkedIn profile enrichment action. Per contact, the maximum paid chain is:

  • LinkedIn only: Find Email plus Contact LinkedIn Enrichment.
  • Email only: Find LinkedIn Profile from Email plus Contact LinkedIn Enrichment when resolution succeeds.
  • Both identifiers: Contact LinkedIn Enrichment only.
  • Neither identifier: no paid call.

Show mutually exclusive route counts and maximum spend before requesting approval for either path.

Composes into

crm-deduplication after account enrichment, find-stakeholders for coverage gaps, segment-accounts for activation, and track-job-changes as a separate one-time movement check.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.