CRM enrichment
State: 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.
The outcome
CRM accounts stay filled. New records get approved blanks written from LinkedIn; a record whose
successful fill is older than six months comes back. The play runs on crm_accounts — the CRM
account extract — and writes back with that row's CRM record id. It does not overwrite a value
that is already there.
The checked example in infra/index.ts is HubSpot (hs_object_id, companies
object, updateRecords + skipIfExist). Salesforce and Attio are the same file adapted:
swap the connector, extractor, record-id field, write action, and fill-blank guard. Do not add
a second CRM branch.
Two failure modes worth knowing before you start. If the matching record-id field is wrong,
the run looks successful and every write targets nothing. If a field's approved write policy is
lost, a refresh overwrites CRM-authoritative data or preserves the stale value it was meant to
replace.
The starting recommendation is identity and size. On HubSpot that is
linkedin_company_id, name, domain, website, linkedin_company_page, and
numberofemployees. LinkedIn company ID is the provider's company_id and is recommended as a
durable matching key. Reuse the best compatible CRM property when one exists; otherwise propose
the string property linkedin_company_id and wait for approval to create it. Do not prepend
cargo_ to proposed CRM properties. Before counting
the target population or editing CDK, the agent derives the other LinkedIn fields that can map to
live CRM properties and presents them with their type and transformation costs. The operator
approves the field contract. That approved contract, not the repository default, controls the
mappings, per-field write policies, and final cost preview. The audit JSON contract lives in
references/audit.md; provider field paths and the selection gate in
references/configure.md.
Present exactly one row per provider property in the field-selection table. Never combine fields
into a grouped row: each property names the actual provider used, its own type, provider-route
availability, CRM destination, fill rate, transformation, recommendation, and operator decision.
Derive the provider name from the live connector and action instead of hard-coding it. The checked
example uses LinkedIn.
The duplicate-property audit is about genuine customer-managed duplicates. Do not present CRM
system properties, HubSpot hs_* fields, or generic native properties as duplicates merely because
they could hold similar data. If no customer-managed duplicate group exists, say
No duplicate properties detected.
Guide the operator through every phase
flowchart LR
audit["1. Audit and enrichment recommendation"] -->|"Approve fields and disabled build"| build["2. Build disabled play and tool"]
build -->|"Review Cargo links and approve cost"| run["3. Run enrichment"]
run --> report["Results report and recommended next step"]
Every substantive message starts with the current phase and ends with a Next step section. Give
the operator one concrete decision or action, say what the agent will do after approval, and name
what remains blocked. During an in-progress operation, say No action needed and name the next
checkpoint. Never end with a generic offer to help.
- Audit and enrichment recommendation. Present the CRM gaps, duplicate-property result, and
one-row-per-property recommendation. End by asking the operator to approve the complete field
contract and authorize building and deploying the resulting Cargo resources in a disabled state.
No paid call or CRM write occurs in this phase.
- Disabled play and tool. After that approval, adapt, type, check, plan, and deploy the reusable
enrichment tool and the play with the play still disabled. The tool normalizes identifiers and
returns provider data without CRM access. The play calls the tool, applies the approved write
policy, and pushes results to the CRM. Inspect the compiled graph before deployment: the tool
starts with an identifier Branch, and the play starts with one Tool node targeting
account_enrichment, followed by the only CRM update. Send a direct Cargo UI link for each resource. Show the exact
eligible population, mutually exclusive provider routes, unit prices, and total estimated
credits. End by asking the operator to review those links and approve the enrichment run at that
stated maximum cost. Do not run or enable anything without that second approval.
- Enrichment and report. Run only the approved population, monitor completion, and report the
before-and-after fill rate for every approved property, processed and outcome counts, failures,
actual credits against estimate, and direct Cargo links. End with a recommended next action:
remediate failures, approve recurring daily coverage, or move to account deduplication.
Put it in your project
This folder is a worked example: real CDK resources written for some other company. The job
is to end up with the code your company would have written, in your project, and an agent does the
adapting.
Install the required authoring skill first. If cargo-cdk is absent, run:
npx skills add getcargohq/cargo-skills --skill cargo-cdk
Then read .agents/skills/cargo-cdk/SKILL.md directly; no session reload is needed. Complete its
bootstrap and use its authoring, state, plan, and deployment rules throughout this pipeline. Stop
before audit or template work if the skill cannot be installed or read.
- Install it — the CLI does the copy. From inside the CDK project,
cargo-ai cdk add cookbook/crm-enrichment writes this example to infra/crm-enrichment/ and
this procedure to .claude/skills/crm-enrichment/. No project yet?
cargo-ai cdk init <dir> --cookbook crm-enrichment && 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.
- Reconcile it with what is already declared. For every model or connector this example
carries that the project already has (a HubSpot connector, an account extract), rewire the
imports to the existing one and drop the copy. Two resources with one slug is a collision at
deploy. The play must keep running on that CRM account model (
crm_accounts in the example).
Append this folder's .env needs to the project's .env.example; never overwrite it.
- Audit and recommend. Re-read the live provider output and CRM property schemas. Follow the
field-selection gate in
references/configure.md: present the CRM
gaps, duplicate-property result, starting recommendation, optional fields, transformations, and
unsupported fields with reasons. Stop for approval of the complete field contract and explicit
authorization to deploy the resulting resources disabled. Do not calculate the final target,
edit CDK, deploy, make a paid call, or write to the CRM while approval is pending.
- Adapt and deploy disabled. After approval, work the sections below in order: What should not
change is what you argue back
about (say what breaks, then do it if they still want it); What you can change is what you
offer unprompted (nobody asks for a variant they do not know exists); What you will be asked
is the floor, and you derive before you ask. If you are asking more than about four questions
you have skipped lookups. Record what you changed and why under a
## Decisions section in
your copy of this file. From the copied skill folder, run
node --import tsx evals/contract.mjs, then run
cargo-ai cdk types && cargo-ai cdk check && cargo-ai cdk plan. Show the diff, and deploy under
the phase-one authorization with isEnabled: false. Never run cargo-ai cdk init --force in a
non-empty directory.
- Hand off for cost approval. Resolve the workspace, play, and tool UUIDs. Send clickable Cargo
UI links using the patterns in
references/run.md, then show the final
target and exact estimated credits. Stop for explicit approval of that run and maximum cost.
- Run and report. Execute only after the second approval. Monitor the approved scope and return
the result report from
references/run.md. Walk Done when line by line.
Deployed cleanly and produced nothing is the normal failure.
What you will be asked
Derive before you ask. An input with a lookup is looked up, not asked. Only the ones marked
asked genuinely live in the operator's head.
| Input |
Kind |
How it is answered |
Why it matters |
crm |
derived |
Inspect authenticated connectors and existing CDK resources |
Reusing them prevents a second CRM connector and a second extract |
field_candidates |
derived |
Join live LinkedIn paths and types to live CRM properties, fill rates, and compatible transformations |
The operator should choose from evidence, not recall provider fields |
approved_field_contract |
asked |
Review the recommended base fields, optional candidates, transformations, destinations, and exclusions |
It defines every write mapping and authorizes the disabled build |
target_population |
derived |
Count eligible rows by mutually exclusive route after field approval |
It makes the cost estimate reproducible |
approved_run |
asked |
Review the disabled Cargo links, target, and exact maximum credits |
It is the explicit gate before any enrichment call |
Checked before moving on, not after the deploy:
crm: one authenticated CRM connector, and the play model is that connector's account extract
approved_field_contract: every selected provider path has a live destination, compatible type
or explicit transformation, fill-blank policy, and recorded operator approval
target_population: eligibility uses identifier, freshness, and approved governance filters;
LinkedIn and domain route counts are mutually exclusive and reproduce the credit estimate
approved_run: direct Cargo UI links resolve, the play is disabled, and the operator approved
the stated population and maximum cost
The first operator question comes after the field candidates are derived. Do not ask whether they
want "more fields" without showing the choices. Present a concise field-contract table and ask
which recommended and optional rows to include. Do not treat silence as approval of the defaults.
Refreshing populated business fields is outside the base template. If requested, that is
approved_refresh_behavior below, not a silent default.
What you can change
The code is a worked example. These reshapes are expected, and the agent offers them rather than
waiting to be asked. Every one costs something; that is what makes it a variation and not the default.
| Variation |
When it is right |
How |
What it costs |
crm |
The consumer uses Salesforce or Attio instead of HubSpot |
Keep one CRM shape in infra/index.ts. The file is the HubSpot example. Salesforce: generated Account update matching Id; there is no skipIfExist — read the Account first and omit any field that is already populated, including numeric zero. Attio: generated company-record update matching the record id; same read-then-omit guard. Do not copy HubSpot's flag. |
Live generated types must be rechecked; a guessed flag writes or no-ops silently |
selected_fields |
The approved contract differs from the starting recommendation |
Present every live candidate at the field-selection gate. After approval, change the result schema, destinations, per-field write policy, and both provider mappings in infra/index.ts. Industry requires an approved array-to-enum transformation when the CRM destination is a single enum. |
Each added field expands mapping and type review |
eligibility |
Only a governed subset should be enriched |
Intersect the play filter with approved lifecycle, tier, ownership, or gap conditions (infra/index.ts enrichAccounts) |
Narrower scope reduces coverage and paid calls |
approved_refresh_behavior |
Populated fields must be refreshed after explicit approval |
Drop skipIfExist / the read-then-omit guard on the approved fields only, preview the replacements, and compare against a fresh CRM read (infra/index.ts) |
Refresh can overwrite CRM-authoritative values if the preview and the write disagree |
What should not change
However far you adapt, these hold. Ask for one anyway and the agent tells you what breaks, then does
it if you still want it, and records why under ## Decisions in your copy of this file.
- The play runs on
crm_accounts and matches the CRM record id. (infra/index.ts) HubSpot's example uses hs_object_id. Sending a Cargo row id, or a native accounts id, to a CRM action targets the wrong identifier system; the run looks successful and nothing lands.
- The tool enriches; the play orchestrates and writes. (
infra/index.ts) account_enrichment accepts provider identifiers, normalizes them, and returns company data without a CRM connector or write. Its defineWorkflow body first branches around rows with no identifier, then routes each eligible row to exactly one provider action. enrich_accounts starts with one Tool node targeting account_enrichment, applies the approved per-field policy, and owns the only CRM update. A tool that writes to the CRM is not reusable; a play that repeats the provider action bypasses the reviewed tool. Run node --import tsx evals/contract.mjs after every adaptation to enforce this compiled graph.
- One CRM shape in the file. (
infra/index.ts) The checked example is HubSpot. Adapt that one file for Salesforce or Attio. Parallel HubSpot/Salesforce/Attio branches drift from the generated types of the CRM that is actually connected.
- At most one paid route per row, LinkedIn URL first. (
infra/index.ts enrichCrmAccount) A row without a handle or a domain makes no paid call. A handle that is already an http URL is used as-is; otherwise it is prefixed as https://www.linkedin.com/company/<handle>.
- Destinations are live properties on the connected CRM. (
infra/index.ts) The HubSpot example writes linkedin_company_id, name, domain, website, linkedin_company_page, numberofemployees, cargo_last_enriched_at, and cargo_enrichment_status. Provider-derived business properties keep neutral names; Cargo-owned operational stamps use the cargo_ prefix. Leaving another CRM's names in the file can write provider data into the wrong property.
- Fill approved blanks only. (
infra/index.ts skipIfExist or the Salesforce/Attio read-then-omit guard) A stale snapshot overwrites authoritative CRM data, including numeric zero.
- Eligibility and freshness live in the play trigger. (
infra/index.ts enrichAccounts) Require an identifier and freshness null or older than six months in the managed segment. Destination fill-state is not an eligibility condition: an approved refresh must be able to re-enrich populated stale fields. The row workflow starts with the reusable tool call instead of repeating trigger conditions as branches. A standalone defineSegment or duplicate workflow gate drifts from the play.
- The first play is disabled and
noConcurrency. (infra/index.ts) Removing those expands an unapproved pilot.
- No credentials, deploy commands, or customer data in this repository.
Done when
- the audit JSON, Markdown, and chat summary agree on every count
- the audit records an operator-approved field contract with provider paths, live CRM destinations,
types, transformations, write policies, and reasons for every exclusion
- the CDK plan contains one CRM account model (
crm_accounts) and no native accounts unification
- the agent installed and read the
cargo-cdk skill before auditing or adapting the template
- the CDK plan contains the reusable
account_enrichment tool and disabled enrich_accounts play;
the play contains one Tool node targeting account_enrichment, followed by the only CRM update
node --import tsx evals/contract.mjs passes against the adapted compiled graph; the tool begins
with an identifier Branch and contains no CRM action, while the play contains no provider action
- generated consumer types confirm the selected provider fields, CRM destinations, write action,
and fill-blank semantics
- every destination is a live property on the connected CRM
- the managed segment excludes records without an identifier but allows populated stale records;
the approved per-field write policy decides fill blank versus refresh
- the play targets
crm_accounts and the write matches the audited CRM record id
- the managed segment uses the null-or-six-month freshness rule, daily evaluation, and
changeKinds: ["added"], with no destination fill-state filter
- the first plan shows
isEnabled: false and runCreationRule: noConcurrency
- LinkedIn and domain route counts are mutually exclusive and reproduce the credit estimate
- the phase-two handoff contains working Cargo UI links for the disabled play and tool, plus the
exact target and estimated credits
- the operator explicitly approved the run after reviewing the links and cost
- the post-run report shows before-and-after fill rates, every outcome count, failures, actual
credits against estimate, and one recommended next step
What it costs
Immediately before every preview, run cargo-ai connection integration get linkedin. Read the
applicable current entries from integration.actions.enrichCompany.credits.costs and
integration.actions.enrichCompanyFromDomain.credits.costs. Record the CLI version, lookup time,
action slugs, and selected unit costs. Then preview
linkedin_url_path * linkedin_url_unit_credits + domain_path * domain_unit_credits.
This play runs on eligible crm_accounts rows and updates that same CRM record. Eligibility means
the row has an identifier and passes freshness and governance filters. Destination fill-state does
not control enrollment. The approved per-field write policy decides whether populated stale values
are preserved or refreshed. Recompute the target and credit preview after approval, then report the
segment count.
Deduplication follows enrichment because the new matching keys improve duplicate detection.
Do not enable the daily schedule until the disabled pilot has passed. Enabling is not an
input; it is the last yes after Done when.
Composes into
account-scoring (a filled book is what the scorer can cite), find-stakeholders (the buyers at
every filled account), tam-building (the universe these records join).
1---2name: crm-enrichment3description: Keep CRM accounts filled and refresh them when they go stale: a deployed play that fills approved blank firmographics from LinkedIn and re-enrolls a record after six months. Triggers: "keep our CRM accounts filled", "keep our CRM companies filled", "enrich my CRM", "CRM enrichment", "old firmographics keep going stale", "every new CRM account", "every new CRM company", "nobody refreshes the company records", "refresh stale firmographics". HubSpot, Salesforce, Attio, Cargo CDK. Skip when: the records are not in a CRM. A supplied company list is enrich-company-data.4---56# CRM enrichment78**State: to-be-approved.** Deploy-verified against a live workspace: not yet. Treat `Done when`9below as the acceptance test and review `cargo-ai cdk plan` before deploying. Make no outcome claim10for this skill until it is approved.1112## The outcome1314CRM accounts stay filled. New records get approved blanks written from LinkedIn; a record whose15successful fill is older than six months comes back. The play runs on `crm_accounts` — the CRM16account extract — and writes back with that row's CRM record id. It does not overwrite a value17that is already there.1819The checked example in `infra/index.ts` is HubSpot (`hs_object_id`, companies20object, `updateRecords` + `skipIfExist`). Salesforce and Attio are the same file adapted:21swap the connector, extractor, record-id field, write action, and fill-blank guard. Do not add22a second CRM branch.2324**Two failure modes worth knowing before you start.** If the matching record-id field is wrong,25the run looks successful and every write targets nothing. If a field's approved write policy is26lost, a refresh overwrites CRM-authoritative data or preserves the stale value it was meant to27replace.2829The starting recommendation is identity and size. On HubSpot that is30`linkedin_company_id`, `name`, `domain`, `website`, `linkedin_company_page`, and31`numberofemployees`. LinkedIn company ID is the provider's `company_id` and is recommended as a32durable matching key. Reuse the best compatible CRM property when one exists; otherwise propose33the string property `linkedin_company_id` and wait for approval to create it. Do not prepend34`cargo_` to proposed CRM properties. Before counting35the target population or editing CDK, the agent derives the other LinkedIn fields that can map to36live CRM properties and presents them with their type and transformation costs. The operator37approves the field contract. That approved contract, not the repository default, controls the38mappings, per-field write policies, and final cost preview. The audit JSON contract lives in39[`references/audit.md`](references/audit.md); provider field paths and the selection gate in40[`references/configure.md`](references/configure.md).4142Present exactly one row per provider property in the field-selection table. Never combine fields43into a grouped row: each property names the actual provider used, its own type, provider-route44availability, CRM destination, fill rate, transformation, recommendation, and operator decision.45Derive the provider name from the live connector and action instead of hard-coding it. The checked46example uses LinkedIn.4748The duplicate-property audit is about genuine customer-managed duplicates. Do not present CRM49system properties, HubSpot `hs_*` fields, or generic native properties as duplicates merely because50they could hold similar data. If no customer-managed duplicate group exists, say51`No duplicate properties detected`.5253## Guide the operator through every phase5455```mermaid56flowchart LR57 audit["1. Audit and enrichment recommendation"] -->|"Approve fields and disabled build"| build["2. Build disabled play and tool"]58 build -->|"Review Cargo links and approve cost"| run["3. Run enrichment"]59 run --> report["Results report and recommended next step"]60```6162Every substantive message starts with the current phase and ends with a `Next step` section. Give63the operator one concrete decision or action, say what the agent will do after approval, and name64what remains blocked. During an in-progress operation, say `No action needed` and name the next65checkpoint. Never end with a generic offer to help.66671. **Audit and enrichment recommendation.** Present the CRM gaps, duplicate-property result, and68 one-row-per-property recommendation. End by asking the operator to approve the complete field69 contract and authorize building and deploying the resulting Cargo resources in a disabled state.70 No paid call or CRM write occurs in this phase.712. **Disabled play and tool.** After that approval, adapt, type, check, plan, and deploy the reusable72 enrichment tool and the play with the play still disabled. The tool normalizes identifiers and73 returns provider data without CRM access. The play calls the tool, applies the approved write74 policy, and pushes results to the CRM. Inspect the compiled graph before deployment: the tool75 starts with an identifier Branch, and the play starts with one Tool node targeting76 `account_enrichment`, followed by the only CRM update. Send a direct Cargo UI link for each resource. Show the exact77 eligible population, mutually exclusive provider routes, unit prices, and total estimated78 credits. End by asking the operator to review those links and approve the enrichment run at that79 stated maximum cost. Do not run or enable anything without that second approval.803. **Enrichment and report.** Run only the approved population, monitor completion, and report the81 before-and-after fill rate for every approved property, processed and outcome counts, failures,82 actual credits against estimate, and direct Cargo links. End with a recommended next action:83 remediate failures, approve recurring daily coverage, or move to account deduplication.8485## Put it in your project8687This folder is a **worked example**: real CDK resources written for some other company. The job88is to end up with the code your company would have written, in your project, and an agent does the89adapting.9091**Install the required authoring skill first.** If `cargo-cdk` is absent, run:9293```sh94npx skills add getcargohq/cargo-skills --skill cargo-cdk95```9697Then read `.agents/skills/cargo-cdk/SKILL.md` directly; no session reload is needed. Complete its98bootstrap and use its authoring, state, plan, and deployment rules throughout this pipeline. Stop99before audit or template work if the skill cannot be installed or read.1001011. **Install it — the CLI does the copy.** From inside the CDK project,102 `cargo-ai cdk add cookbook/crm-enrichment` writes this example to `infra/crm-enrichment/` and103 this procedure to `.claude/skills/crm-enrichment/`. No project yet?104 `cargo-ai cdk init <dir> --cookbook crm-enrichment && cd <dir> && npm install` does both; this105 folder never ships a shell. **If you are reading this from the project's `.claude/skills/`, the106 install already happened — start at step 2.** On a CLI too old to have `add`, copy this folder107 in as a sibling of what is there by hand; everything below is unchanged.1082. **Reconcile it with what is already declared.** For every model or connector this example109 carries that the project already has (a HubSpot connector, an account extract), rewire the110 imports to the existing one and drop the copy. Two resources with one slug is a collision at111 deploy. The play must keep running on that CRM account model (`crm_accounts` in the example).112 Append this folder's `.env` needs to the project's `.env.example`; never overwrite it.1133. **Audit and recommend.** Re-read the live provider output and CRM property schemas. Follow the114 field-selection gate in [`references/configure.md`](references/configure.md): present the CRM115 gaps, duplicate-property result, starting recommendation, optional fields, transformations, and116 unsupported fields with reasons. Stop for approval of the complete field contract and explicit117 authorization to deploy the resulting resources disabled. Do not calculate the final target,118 edit CDK, deploy, make a paid call, or write to the CRM while approval is pending.1194. **Adapt and deploy disabled.** After approval, work the sections below in order: _What should not120 change_ is what you argue back121 about (say what breaks, then do it if they still want it); _What you can change_ is what you122 offer unprompted (nobody asks for a variant they do not know exists); _What you will be asked_123 is the floor, and you derive before you ask. If you are asking more than about four questions124 you have skipped lookups. Record what you changed and why under a `## Decisions` section in125 your copy of this file. From the copied skill folder, run126 `node --import tsx evals/contract.mjs`, then run127 `cargo-ai cdk types && cargo-ai cdk check && cargo-ai cdk plan`. Show the diff, and deploy under128 the phase-one authorization with `isEnabled: false`. Never run `cargo-ai cdk init --force` in a129 non-empty directory.1305. **Hand off for cost approval.** Resolve the workspace, play, and tool UUIDs. Send clickable Cargo131 UI links using the patterns in [`references/run.md`](references/run.md), then show the final132 target and exact estimated credits. Stop for explicit approval of that run and maximum cost.1336. **Run and report.** Execute only after the second approval. Monitor the approved scope and return134 the result report from [`references/run.md`](references/run.md). Walk _Done when_ line by line.135 Deployed cleanly and produced nothing is the normal failure.136137## What you will be asked138139**Derive before you ask.** An input with a lookup is looked up, not asked. Only the ones marked140_asked_ genuinely live in the operator's head.141142| Input | Kind | How it is answered | Why it matters |143| ------------------------- | ------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |144| `crm` | derived | Inspect authenticated connectors and existing CDK resources | Reusing them prevents a second CRM connector and a second extract |145| `field_candidates` | derived | Join live LinkedIn paths and types to live CRM properties, fill rates, and compatible transformations | The operator should choose from evidence, not recall provider fields |146| `approved_field_contract` | asked | Review the recommended base fields, optional candidates, transformations, destinations, and exclusions | It defines every write mapping and authorizes the disabled build |147| `target_population` | derived | Count eligible rows by mutually exclusive route after field approval | It makes the cost estimate reproducible |148| `approved_run` | asked | Review the disabled Cargo links, target, and exact maximum credits | It is the explicit gate before any enrichment call |149150Checked before moving on, not after the deploy:151152- `crm`: one authenticated CRM connector, and the play model is that connector's account extract153- `approved_field_contract`: every selected provider path has a live destination, compatible type154 or explicit transformation, fill-blank policy, and recorded operator approval155- `target_population`: eligibility uses identifier, freshness, and approved governance filters;156 LinkedIn and domain route counts are mutually exclusive and reproduce the credit estimate157- `approved_run`: direct Cargo UI links resolve, the play is disabled, and the operator approved158 the stated population and maximum cost159160The first operator question comes after the field candidates are derived. Do not ask whether they161want "more fields" without showing the choices. Present a concise field-contract table and ask162which recommended and optional rows to include. Do not treat silence as approval of the defaults.163164Refreshing populated business fields is outside the base template. If requested, that is165`approved_refresh_behavior` below, not a silent default.166167## What you can change168169The code is a worked example. These reshapes are expected, and the agent offers them rather than170waiting to be asked. Every one costs something; that is what makes it a variation and not the default.171172| Variation | When it is right | How | What it costs |173| --------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |174| `crm` | The consumer uses Salesforce or Attio instead of HubSpot | Keep one CRM shape in `infra/index.ts`. The file is the HubSpot example. **Salesforce:** generated Account update matching `Id`; there is no `skipIfExist` — read the Account first and omit any field that is already populated, including numeric zero. **Attio:** generated company-record update matching the record id; same read-then-omit guard. Do not copy HubSpot's flag. | Live generated types must be rechecked; a guessed flag writes or no-ops silently |175| `selected_fields` | The approved contract differs from the starting recommendation | Present every live candidate at the field-selection gate. After approval, change the result schema, destinations, per-field write policy, and both provider mappings in `infra/index.ts`. Industry requires an approved array-to-enum transformation when the CRM destination is a single enum. | Each added field expands mapping and type review |176| `eligibility` | Only a governed subset should be enriched | Intersect the play filter with approved lifecycle, tier, ownership, or gap conditions (`infra/index.ts` `enrichAccounts`) | Narrower scope reduces coverage and paid calls |177| `approved_refresh_behavior` | Populated fields must be refreshed after explicit approval | Drop `skipIfExist` / the read-then-omit guard on the approved fields only, preview the replacements, and compare against a fresh CRM read (`infra/index.ts`) | Refresh can overwrite CRM-authoritative values if the preview and the write disagree |178179## What should not change180181However far you adapt, these hold. Ask for one anyway and the agent tells you what breaks, then does182it if you still want it, and records why under `## Decisions` in your copy of this file.183184- **The play runs on `crm_accounts` and matches the CRM record id.** (`infra/index.ts`) HubSpot's example uses `hs_object_id`. Sending a Cargo row id, or a native `accounts` id, to a CRM action targets the wrong identifier system; the run looks successful and nothing lands.185- **The tool enriches; the play orchestrates and writes.** (`infra/index.ts`) `account_enrichment` accepts provider identifiers, normalizes them, and returns company data without a CRM connector or write. Its `defineWorkflow` body first branches around rows with no identifier, then routes each eligible row to exactly one provider action. `enrich_accounts` starts with one Tool node targeting `account_enrichment`, applies the approved per-field policy, and owns the only CRM update. A tool that writes to the CRM is not reusable; a play that repeats the provider action bypasses the reviewed tool. Run `node --import tsx evals/contract.mjs` after every adaptation to enforce this compiled graph.186- **One CRM shape in the file.** (`infra/index.ts`) The checked example is HubSpot. Adapt that one file for Salesforce or Attio. Parallel HubSpot/Salesforce/Attio branches drift from the generated types of the CRM that is actually connected.187- **At most one paid route per row, LinkedIn URL first.** (`infra/index.ts` `enrichCrmAccount`) A row without a handle or a domain makes no paid call. A handle that is already an `http` URL is used as-is; otherwise it is prefixed as `https://www.linkedin.com/company/<handle>`.188- **Destinations are live properties on the connected CRM.** (`infra/index.ts`) The HubSpot example writes `linkedin_company_id`, `name`, `domain`, `website`, `linkedin_company_page`, `numberofemployees`, `cargo_last_enriched_at`, and `cargo_enrichment_status`. Provider-derived business properties keep neutral names; Cargo-owned operational stamps use the `cargo_` prefix. Leaving another CRM's names in the file can write provider data into the wrong property.189- **Fill approved blanks only.** (`infra/index.ts` `skipIfExist` or the Salesforce/Attio read-then-omit guard) A stale snapshot overwrites authoritative CRM data, including numeric zero.190- **Eligibility and freshness live in the play trigger.** (`infra/index.ts` `enrichAccounts`) Require an identifier and freshness null or older than six months in the managed segment. Destination fill-state is not an eligibility condition: an approved refresh must be able to re-enrich populated stale fields. The row workflow starts with the reusable tool call instead of repeating trigger conditions as branches. A standalone `defineSegment` or duplicate workflow gate drifts from the play.191- **The first play is disabled and `noConcurrency`.** (`infra/index.ts`) Removing those expands an unapproved pilot.192- **No credentials, deploy commands, or customer data in this repository.**193194## Done when195196- the audit JSON, Markdown, and chat summary agree on every count197- the audit records an operator-approved field contract with provider paths, live CRM destinations,198 types, transformations, write policies, and reasons for every exclusion199- the CDK plan contains one CRM account model (`crm_accounts`) and no native `accounts` unification200- the agent installed and read the `cargo-cdk` skill before auditing or adapting the template201- the CDK plan contains the reusable `account_enrichment` tool and disabled `enrich_accounts` play;202 the play contains one Tool node targeting `account_enrichment`, followed by the only CRM update203- `node --import tsx evals/contract.mjs` passes against the adapted compiled graph; the tool begins204 with an identifier Branch and contains no CRM action, while the play contains no provider action205- generated consumer types confirm the selected provider fields, CRM destinations, write action,206 and fill-blank semantics207- every destination is a live property on the connected CRM208- the managed segment excludes records without an identifier but allows populated stale records;209 the approved per-field write policy decides fill blank versus refresh210- the play targets `crm_accounts` and the write matches the audited CRM record id211- the managed segment uses the null-or-six-month freshness rule, daily evaluation, and212 `changeKinds: ["added"]`, with no destination fill-state filter213- the first plan shows `isEnabled: false` and `runCreationRule: noConcurrency`214- LinkedIn and domain route counts are mutually exclusive and reproduce the credit estimate215- the phase-two handoff contains working Cargo UI links for the disabled play and tool, plus the216 exact target and estimated credits217- the operator explicitly approved the run after reviewing the links and cost218- the post-run report shows before-and-after fill rates, every outcome count, failures, actual219 credits against estimate, and one recommended next step220221## What it costs222223Immediately before every preview, run `cargo-ai connection integration get linkedin`. Read the224applicable current entries from `integration.actions.enrichCompany.credits.costs` and225`integration.actions.enrichCompanyFromDomain.credits.costs`. Record the CLI version, lookup time,226action slugs, and selected unit costs. Then preview227`linkedin_url_path * linkedin_url_unit_credits + domain_path * domain_unit_credits`.228229This play runs on eligible `crm_accounts` rows and updates that same CRM record. Eligibility means230the row has an identifier and passes freshness and governance filters. Destination fill-state does231not control enrollment. The approved per-field write policy decides whether populated stale values232are preserved or refreshed. Recompute the target and credit preview after approval, then report the233segment count.234Deduplication follows enrichment because the new matching keys improve duplicate detection.235236Do not enable the daily schedule until the disabled pilot has passed. Enabling is not an237input; it is the last yes after _Done when_.238239## Composes into240241`account-scoring` (a filled book is what the scorer can cite), `find-stakeholders` (the buyers at242every filled account), `tam-building` (the universe these records join).