Salesforce Record Stewardship
Data steward for a single object at a time. Answer two questions and nothing
else: what is wrong with the records on this object, and how do I safely fix
it? Profiling is free; every write is proposed, batched, verified, and
reversible.
This skill does not own query syntax (sf-data does), web research (sf-leads
does), or org-wide scoring (sf-audit does). It owns the records.
Dispatch
| First argument or intent |
Workflow |
inspect, "data health check on X", "what's wrong with our opportunity data", "how bad is it" |
Inspect |
dedupe, "find duplicate contacts", "are these the same account", "merge these" |
Dedupe |
fix, "clean up my accounts", "fix these records", "fill in the gaps" |
Fix |
bulk, "bulk update", "reassign all X to Y", "mass update" |
Bulk |
| An object name alone |
Run Inspect and offer the other three from the findings |
Always name the object explicitly in the first line of output. A stewardship
run that silently drifted from Account to Contact is a run nobody can audit.
Execution modes
See references/execution-modes.md (tool-name mapping preamble and the
headless rule). Initialize the connection first (org_init convention).
Headless = propose-only. In non-interactive runs, Inspect runs fully;
Fix and Bulk stop at the proposal table; Dedupe stops at the candidate list.
Merges never execute headless under any caller-granted permission — a merge
cannot be undone by rerunning the skill.
Phase 0 — Describe first, always
Never assume a field exists. Before any query, sobject_describe the target
object and work from what the org actually has:
Confirm the object. Custom objects, namespaced objects, and objects
renamed in the UI all resolve differently — match on API name.
Inventory the fields that matter for the workflow: type, length,
nillable, updateable, defaultedOnCreate, picklistValues[].active,
calculated (formula fields cannot be written), and unique/external-id
flags (they are your best dedupe keys).
If the connector's describe tool returns a thin field list (some
return only name/label/type/updateable), recover the rest from the
Tooling API rather than assuming —
SELECT QualifiedApiName, DataType, Length, IsCompound, IsNillable, IsCalculated FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'Account'
— and see references/execution-modes.md for the connector variation and
the compound-address blind spot.
Find custom fields shadowing standard ones. Orgs routinely keep the
real value somewhere else: Contract_Value__c beside a stale Amount,
Industry_Segment__c instead of Industry, a custom
Primary_Email__c beside Email. Describe alone never proves shadowing —
populated-rate sampling does. Take the total once and a populated count per
candidate field —
SELECT COUNT() FROM Opportunity
SELECT COUNT() FROM Opportunity WHERE Amount != null
SELECT COUNT() FROM Opportunity WHERE Contract_Value__c != null
— one query per field, because COUNT(Field) is rejected outright on
textarea, long text area, boolean, and encrypted fields, while COUNT()
with a != null filter works on every filterable type. Add a recency
comparison, and when a shadow candidate wins on population, ask which
field is authoritative before proposing any write to either. Managed
packages (CPQ, billing, subscription apps) frequently own the real value
and recompute anything you write into their field.
Note the automation surface: active validation rules, required fields
per record type, and duplicate rules. They decide which of your proposed
fixes will actually land.
Inventory the code automation on the object — Apex triggers and
record-triggered flows, including managed-package ones — before
proposing any write. A managed trigger can mirror or overwrite your fix
(address-sync packages copy billing into shipping), so finding it now is
the difference between an honest proposal and a surprise:
SELECT Id, Name, Status, NamespacePrefix FROM ApexTrigger WHERE TableEnumOrId = 'Account'
(Tooling API; pair it with FlowDefinition / FlowDefinitionView for
active record-triggered flows on the same object.)
Record the describe findings once and reuse them across the run.
Data-quality canon
The universal record-writing rules — active picklists, address/code
conventions, field lengths, anti-fabrication, source attribution, email
verification and opt-out/bounce one-way doors, departed people, and write
discipline — are shared canon at
../../shared/standards/record-data-quality.md. Read it before the first
write of any run and cite it by section (§1–§8) instead of restating it.
Three canon rules are load-bearing here and get named explicitly in output:
§4 (blank beats invented), §6 (opt-out and bounce flags are one-way
doors — never cleared as a side effect of a bulk update), and §8
(propose, batch, verify, never overwrite non-null without instruction).
Inspect — profile an object's data health
Read-only. Produces the map the other three workflows navigate by.
- Volume and shape.
SELECT COUNT() FROM {Object}, plus counts by the
object's primary segmentation field (RecordType, Stage, Status, Type) and
by CreatedDate year. Volume decides whether later phases can query
directly or must chunk.
- Completeness per field. For the fields the playbook flags as
mattering, take the total once (
SELECT COUNT() FROM {Object}) and one
populated count per field
(SELECT COUNT() FROM {Object} WHERE {Field} != null). Use a single
grouped COUNT(Field) query only where every field in it is an
aggregatable type — COUNT(Field) is invalid on textarea, long text
area, boolean, and encrypted fields, which is exactly what the
*_Notes__c / *_Flag__c shadows tend to be, so the COUNT() + null
filter is the portable form. Report as a rate, not a raw number —
"BillingCountry populated on 41% of 12,400 accounts" is actionable;
"5,084" is trivia.
- Validity. Picklist values present in data but no longer active
(canon §1 — GROUP BY the data, diff against the describe's active list;
the authoritative check is the UI API picklist-values endpoint via a
generic REST tool, and when the connector has neither that nor a full
describe, report picklist validity as unverified rather than trusting the
GROUP BY — see
references/execution-modes.md);
country/state codes that violate the org's dominant convention (§2);
emails failing a shape check; numbers and dates outside plausible ranges.
- Consistency. Cross-field contradictions from the object playbook —
a closed stage with a future close date, a contact with no account, a
parent pointing at itself.
- Ownership and staleness. Missing owners, inactive owners
(
Owner.IsActive = false), and owners that are not real people —
integration, system, and Automated Process users are IsActive = true
and pass an inactive-owner check while leaving nobody accountable for the
record. Exclude them from the "owned" count and flag them as their own
finding; the same applies to queue ownership where the object allows it.
Then records untouched past the object's natural clock
(LastModifiedDate, LastActivityDate).
- Duplicate pressure. A cheap aggregate on the object's natural key —
count of keys appearing more than once. Not the full Dedupe run; just the
size of the problem.
Per-object specifics — which fields, which contradictions, which traps — are
in references/object-playbooks.md. Report shape:
## Data health: {Object} ({record count}, {UTC timestamp})
| Finding | Severity | Records | Rate | Fix path |
| ... one row per finding, severity-ranked ...
## Top 5 fixes by value
<what to fix first, why, and which workflow does it>
Severity: CRITICAL = breaks a business process or a downstream system
(orphaned records, cycles, missing required linkage). HIGH = wrong data
that people act on (retired picklist values, invalid amounts, duplicates on a
key). MEDIUM = missing data that limits reporting. LOW = cosmetic or
convention drift.
Dedupe — find duplicates and plan merges
Detection is safe; merging is not. These are separate steps with a user
between them.
Matching strategy, in strength order (details and per-object keys in
references/dedupe-strategies.md):
| Tier |
Method |
Trust |
| 1 |
Exact match on a unique/external-id key (Email, ProductCode, an external system Id) |
Auto-groupable |
| 2 |
Normalized composite — lowercased name stripped of legal suffixes + website/email domain; or LastName + FirstName + AccountId |
Groupable, user reviews each group |
| 3 |
Fuzzy (token overlap, edit distance) on names only |
Last resort; every pair reviewed individually, never auto-grouped |
Between tiers 2 and 3 sits a signal worth naming: one registrable domain,
several different normalized names. That is usually subsidiaries, not
copies — the default recommendation is an account hierarchy, not a merge
(references/dedupe-strategies.md, When not to merge at all).
Normalize before comparing, never after: lowercase, trim, strip punctuation,
legal suffixes (Inc/LLC/Ltd/GmbH/Corp) and trailing geography or division
qualifiers, reduce websites to registrable domain, strip +tags from emails. Report the normalization rules used — a dedupe whose
matching rules aren't stated can't be trusted or repeated.
Do NOT auto-merge, ever:
- Records in different record types, currencies, or business units.
- Records with different owners in a territory-managed org until the owner
question is answered.
- Records where each side has non-null values in the same field that
disagree — that is a data decision, not a match decision.
- Accounts with children (contacts, opportunities, cases, hierarchy children)
until the reparenting consequences are shown.
- Anything matched only at tier 3.
- Person Accounts, or Contacts under different Accounts (Salesforce forbids
or complicates both).
Merge is a Salesforce operation with real consequences. The losing record
is deleted, its child records reparent to the winner, field values from the
loser fill only the winner's blank fields unless overridden, and audit
history from the losing record does not survive intact. Merges are
always user-confirmed, one group at a time or in an explicitly approved
list, and never headless. Before each merge, present:
| Field | Winner (Id) | Loser (Id) | Result after merge |
plus the child-record counts that will reparent, and the master-record
choice with its reason (oldest, most complete, most activity, the one
integrations reference). Ask before executing. After merging, verify the
winner and its reparented children.
When the org has Duplicate Rules and Matching Rules configured, say so and
prefer them — reporting DuplicateRecordSet/DuplicateRecordItem findings
is better stewardship than inventing a parallel matching scheme.
Fix — propose and apply corrections
The default workflow for "clean up my X". Never a single sweeping update.
Scope the fix to one finding from Inspect (or the user's own
criteria). One finding per Fix run keeps the proposal reviewable.
Gather evidence from inside the org: the parent record, a sibling
field, the org's dominant convention, an existing related record. There is
no web research here — that is sf-leads.
Propose. Always this table, never prose:
| Record (Id / Name) |
Field |
Current |
Proposed |
Evidence |
With a count, the batch plan, and any records deliberately excluded and
why. Values that would overwrite a non-null field are listed separately
and are off by default — they only proceed on an explicit instruction
naming that overwrite.
Apply in batches of ≤200 records (the user may raise it; say what
you raised it to and why). Stop on the first batch that errors, report
the error and the partial state, and do not continue automatically.
Verify after write. Re-query the affected records and compare against
the proposal. Automation can accept a write (HTTP 204) and revert it —
when a value reverts, automation owns that field. Report which fields
reverted, don't retry them, and hand the automation question to sf-flow
or sf-apex (canon §8).
Report applied / reverted / failed / skipped with counts.
Bulk — safe mass updates
For deliberate, large, uniform changes: owner reassignment, status
transitions, backfilling a new field, applying a convention.
- Dry-run counts first. Run the selection as
COUNT(Id) before
selecting rows, and show the user the number and the exact criteria.
If the count differs materially from what the user expected, stop and
reconcile — a mass update on a wrong filter is the single most expensive
mistake in this skill's territory.
- Rollback plan before the first write. Query and preserve
Id plus
the prior value of every field being changed, and keep it where the user
can reach it (a file in code-execution modes; an in-context table capped
to a reviewable size otherwise). State plainly what rollback restores
(field values) and what it cannot (deletions, merges, fired automation,
sent emails, downstream integration events).
- Chunk. ≤200 records per call; sequence chunks and log which chunk
ranges succeeded so a partial failure is resumable rather than
re-runnable from zero.
- Guardrail-hook awareness. The plugin's PreToolUse guardrails flag
broad destructive DML and high-risk permission payloads and can turn a
write into an explicit confirmation prompt. Expect it, don't work around
it, and never restructure a call purely to slip under a threshold — if a
guardrail fires, that is the moment to re-confirm scope with the user.
- Never mass-update opt-out, bounce, or consent fields (canon §6);
audit/system fields; or fields owned by a managed package, without a
named, explicit instruction for that specific field.
- Verify a sample of at least 20 records (or all, if fewer) after each
chunk, and the full set at the end.
Deletion is not a Bulk operation in this skill. Stale records get flagged and
dated (canon §7); when the user genuinely wants deletion, hand off to sf-data
with the scoped Id list and the rollback caveat stated.
Pitfalls
| Pitfall |
Handling |
| Field assumed, not described |
Phase 0 is not optional — a proposal citing a nonexistent field discredits the whole run |
| Custom field shadowing the standard one |
Populated-rate comparison, then ask which is authoritative before writing either |
| Managed-package field (CPQ, billing) |
Describe shows the namespace; the package usually recomputes it — propose nothing there without the user's say-so |
| Formula / roll-up / auto-number field in a proposal |
calculated == true fields are not writable — fix the inputs instead |
| Retired picklist value written back |
Diff data values against the describe's active: true list (canon §1) |
| Validation rule rejects the whole batch |
Read active rules in Phase 0; on FIELD_CUSTOM_VALIDATION_EXCEPTION, report the rule name rather than retrying |
| Record types change what's required and allowed |
Verify picklists and required fields against the record's own RecordTypeId |
| Merge across different Accounts / Person Accounts |
Not auto-groupable; explain the constraint instead of attempting it |
| Silent revert after a successful write |
Verify step; attribute to automation, hand to sf-flow / sf-apex |
| Large object (500k+ rows) |
Aggregate-only profiling, chunked selection, and offer to narrow scope before enumerating rows |
Cross-skill handoffs
- Query syntax, selectivity, DML execution mechanics, deletions → sf-data
- Filling gaps from web research (titles, industries, company data) →
sf-leads
- Campaign and member performance analysis → sf-campaigns
- Org-wide quality scoring and client-ready audit documents → sf-audit
(also its
report-template.md §7–8 when the user wants a document)
- The automation that reverts your writes → sf-flow / sf-apex
- Missing fields, picklist values, or duplicate rules that the data needs →
sf-metadata (a metadata change, not a record update)
References
| File |
Read when |
references/object-playbooks.md |
Inspect and Fix — per-object fields, "good", problems, safe fix patterns, traps |
references/dedupe-strategies.md |
Dedupe — matching tiers, normalization, per-object keys, merge mechanics |
references/execution-modes.md |
Start of session — tool mapping, headless rule |
1---2name: sf-records3description: Record stewardship for a Salesforce object — profiles data health field by field, finds and plans duplicate merges, proposes and applies corrections in approved batches, and runs mass updates with a rollback plan. Describe-first against the live org, every write proposed before it lands and verified after. Use when the user says "clean up my accounts", "find duplicate contacts", "what's wrong with our opportunity data", "fix these records", "bulk update", "data health check on X", "merge these duplicates", "our contacts are a mess", or asks how bad the data on an object is. Do NOT use for writing or running queries and DML mechanics (use sf-data), enriching records from web research (use sf-leads), campaign performance analysis (use sf-campaigns), or org-wide quality audits (use sf-audit). Usage: /sf-records [inspect|dedupe|fix|bulk] {object} [criteria] ...4---56# Salesforce Record Stewardship78Data steward for a single object at a time. Answer two questions and nothing9else: **what is wrong with the records on this object, and how do I safely fix10it?** Profiling is free; every write is proposed, batched, verified, and11reversible.1213This skill does not own query syntax (sf-data does), web research (sf-leads14does), or org-wide scoring (sf-audit does). It owns the records.1516## Dispatch1718| First argument or intent | Workflow |19| --- | --- |20| `inspect`, "data health check on X", "what's wrong with our opportunity data", "how bad is it" | Inspect |21| `dedupe`, "find duplicate contacts", "are these the same account", "merge these" | Dedupe |22| `fix`, "clean up my accounts", "fix these records", "fill in the gaps" | Fix |23| `bulk`, "bulk update", "reassign all X to Y", "mass update" | Bulk |24| An object name alone | Run **Inspect** and offer the other three from the findings |2526Always name the object explicitly in the first line of output. A stewardship27run that silently drifted from Account to Contact is a run nobody can audit.2829## Execution modes3031See `references/execution-modes.md` (tool-name mapping preamble and the32headless rule). Initialize the connection first (`org_init` convention).3334**Headless = propose-only.** In non-interactive runs, Inspect runs fully;35Fix and Bulk stop at the proposal table; Dedupe stops at the candidate list.36Merges never execute headless under any caller-granted permission — a merge37cannot be undone by rerunning the skill.3839---4041## Phase 0 — Describe first, always4243Never assume a field exists. Before any query, `sobject_describe` the target44object and work from what the org actually has:45461. **Confirm the object.** Custom objects, namespaced objects, and objects47 renamed in the UI all resolve differently — match on API name.482. **Inventory the fields that matter** for the workflow: type, `length`,49 `nillable`, `updateable`, `defaultedOnCreate`, `picklistValues[].active`,50 `calculated` (formula fields cannot be written), and unique/external-id51 flags (they are your best dedupe keys).52 **If the connector's describe tool returns a thin field list** (some53 return only name/label/type/updateable), recover the rest from the54 Tooling API rather than assuming —55 `SELECT QualifiedApiName, DataType, Length, IsCompound, IsNillable, IsCalculated FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'Account'`56 — and see `references/execution-modes.md` for the connector variation and57 the compound-address blind spot.583. **Find custom fields shadowing standard ones.** Orgs routinely keep the59 real value somewhere else: `Contract_Value__c` beside a stale `Amount`,60 `Industry_Segment__c` instead of `Industry`, a custom61 `Primary_Email__c` beside `Email`. Describe alone never proves shadowing —62 populated-rate sampling does. Take the total once and a populated count per63 candidate field —6465 ```sql66 SELECT COUNT() FROM Opportunity67 SELECT COUNT() FROM Opportunity WHERE Amount != null68 SELECT COUNT() FROM Opportunity WHERE Contract_Value__c != null69 ```7071 — one query per field, because `COUNT(Field)` is rejected outright on72 textarea, long text area, boolean, and encrypted fields, while `COUNT()`73 with a `!= null` filter works on every filterable type. Add a recency74 comparison, and when a shadow candidate wins on population, **ask which75 field is authoritative before proposing any write to either.** Managed76 packages (CPQ, billing, subscription apps) frequently own the real value77 and recompute anything you write into their field.784. **Note the automation surface:** active validation rules, required fields79 per record type, and duplicate rules. They decide which of your proposed80 fixes will actually land.815. **Inventory the code automation on the object** — Apex triggers and82 record-triggered flows, **including managed-package ones** — before83 proposing any write. A managed trigger can mirror or overwrite your fix84 (address-sync packages copy billing into shipping), so finding it now is85 the difference between an honest proposal and a surprise:8687 ```sql88 SELECT Id, Name, Status, NamespacePrefix FROM ApexTrigger WHERE TableEnumOrId = 'Account'89 ```9091 (Tooling API; pair it with `FlowDefinition` / `FlowDefinitionView` for92 active record-triggered flows on the same object.)9394Record the describe findings once and reuse them across the run.9596## Data-quality canon9798The universal record-writing rules — active picklists, address/code99conventions, field lengths, anti-fabrication, source attribution, email100verification and opt-out/bounce one-way doors, departed people, and write101discipline — are shared canon at102`../../shared/standards/record-data-quality.md`. Read it before the first103write of any run and cite it by section (`§1`–`§8`) instead of restating it.104105Three canon rules are load-bearing here and get named explicitly in output:106**§4** (blank beats invented), **§6** (opt-out and bounce flags are one-way107doors — never cleared as a side effect of a bulk update), and **§8**108(propose, batch, verify, never overwrite non-null without instruction).109110---111112## Inspect — profile an object's data health113114Read-only. Produces the map the other three workflows navigate by.1151161. **Volume and shape.** `SELECT COUNT() FROM {Object}`, plus counts by the117 object's primary segmentation field (RecordType, Stage, Status, Type) and118 by `CreatedDate` year. Volume decides whether later phases can query119 directly or must chunk.1202. **Completeness per field.** For the fields the playbook flags as121 mattering, take the total once (`SELECT COUNT() FROM {Object}`) and one122 populated count per field123 (`SELECT COUNT() FROM {Object} WHERE {Field} != null`). Use a single124 grouped `COUNT(Field)` query only where every field in it is an125 aggregatable type — `COUNT(Field)` is invalid on textarea, long text126 area, boolean, and encrypted fields, which is exactly what the127 `*_Notes__c` / `*_Flag__c` shadows tend to be, so the `COUNT()` + null128 filter is the portable form. Report as a rate, not a raw number —129 "BillingCountry populated on 41% of 12,400 accounts" is actionable;130 "5,084" is trivia.1313. **Validity.** Picklist values present in data but no longer active132 (canon §1 — GROUP BY the data, diff against the describe's active list;133 the authoritative check is the UI API picklist-values endpoint via a134 generic REST tool, and when the connector has neither that nor a full135 describe, report picklist validity as unverified rather than trusting the136 GROUP BY — see `references/execution-modes.md`);137 country/state codes that violate the org's dominant convention (§2);138 emails failing a shape check; numbers and dates outside plausible ranges.1394. **Consistency.** Cross-field contradictions from the object playbook —140 a closed stage with a future close date, a contact with no account, a141 parent pointing at itself.1425. **Ownership and staleness.** Missing owners, inactive owners143 (`Owner.IsActive = false`), and owners that are not real people —144 integration, system, and Automated Process users are `IsActive = true`145 and pass an inactive-owner check while leaving nobody accountable for the146 record. Exclude them from the "owned" count and flag them as their own147 finding; the same applies to queue ownership where the object allows it.148 Then records untouched past the object's natural clock149 (`LastModifiedDate`, `LastActivityDate`).1506. **Duplicate pressure.** A cheap aggregate on the object's natural key —151 count of keys appearing more than once. Not the full Dedupe run; just the152 size of the problem.153154Per-object specifics — which fields, which contradictions, which traps — are155in `references/object-playbooks.md`. Report shape:156157```158## Data health: {Object} ({record count}, {UTC timestamp})159160| Finding | Severity | Records | Rate | Fix path |161| ... one row per finding, severity-ranked ...162163## Top 5 fixes by value164<what to fix first, why, and which workflow does it>165```166167Severity: **CRITICAL** = breaks a business process or a downstream system168(orphaned records, cycles, missing required linkage). **HIGH** = wrong data169that people act on (retired picklist values, invalid amounts, duplicates on a170key). **MEDIUM** = missing data that limits reporting. **LOW** = cosmetic or171convention drift.172173## Dedupe — find duplicates and plan merges174175Detection is safe; merging is not. These are separate steps with a user176between them.177178**Matching strategy, in strength order** (details and per-object keys in179`references/dedupe-strategies.md`):180181| Tier | Method | Trust |182| --- | --- | --- |183| 1 | Exact match on a unique/external-id key (Email, ProductCode, an external system Id) | Auto-groupable |184| 2 | Normalized composite — lowercased name stripped of legal suffixes + website/email domain; or LastName + FirstName + AccountId | Groupable, user reviews each group |185| 3 | Fuzzy (token overlap, edit distance) on names only | Last resort; every pair reviewed individually, never auto-grouped |186187Between tiers 2 and 3 sits a signal worth naming: **one registrable domain,188several different normalized names.** That is usually subsidiaries, not189copies — the default recommendation is an account hierarchy, not a merge190(`references/dedupe-strategies.md`, *When not to merge at all*).191192Normalize before comparing, never after: lowercase, trim, strip punctuation,193legal suffixes (`Inc/LLC/Ltd/GmbH/Corp`) and trailing geography or division194qualifiers, reduce websites to registrable domain, strip `+tags` from emails. Report the normalization rules used — a dedupe whose195matching rules aren't stated can't be trusted or repeated.196197**Do NOT auto-merge**, ever:198- Records in different record types, currencies, or business units.199- Records with different owners in a territory-managed org until the owner200 question is answered.201- Records where each side has non-null values in the same field that202 disagree — that is a data decision, not a match decision.203- Accounts with children (contacts, opportunities, cases, hierarchy children)204 until the reparenting consequences are shown.205- Anything matched only at tier 3.206- Person Accounts, or Contacts under different Accounts (Salesforce forbids207 or complicates both).208209**Merge is a Salesforce operation with real consequences.** The losing record210is deleted, its child records reparent to the winner, field values from the211loser fill only the winner's *blank* fields unless overridden, and audit212history from the losing record does not survive intact. Merges are213**always user-confirmed, one group at a time or in an explicitly approved214list, and never headless.** Before each merge, present:215216| Field | Winner (Id) | Loser (Id) | Result after merge |217218plus the child-record counts that will reparent, and the master-record219choice with its reason (oldest, most complete, most activity, the one220integrations reference). Ask before executing. After merging, verify the221winner and its reparented children.222223When the org has Duplicate Rules and Matching Rules configured, say so and224prefer them — reporting `DuplicateRecordSet`/`DuplicateRecordItem` findings225is better stewardship than inventing a parallel matching scheme.226227## Fix — propose and apply corrections228229The default workflow for "clean up my X". Never a single sweeping update.2302311. **Scope** the fix to one finding from Inspect (or the user's own232 criteria). One finding per Fix run keeps the proposal reviewable.2332. **Gather evidence** from inside the org: the parent record, a sibling234 field, the org's dominant convention, an existing related record. There is235 no web research here — that is sf-leads.2363. **Propose.** Always this table, never prose:237238 | Record (Id / Name) | Field | Current | Proposed | Evidence |239 | --- | --- | --- | --- | --- |240241 With a count, the batch plan, and any records deliberately excluded and242 why. Values that would overwrite a non-null field are listed separately243 and are **off by default** — they only proceed on an explicit instruction244 naming that overwrite.2454. **Apply in batches of ≤200 records** (the user may raise it; say what246 you raised it to and why). Stop on the first batch that errors, report247 the error and the partial state, and do not continue automatically.2485. **Verify after write.** Re-query the affected records and compare against249 the proposal. Automation can accept a write (HTTP 204) and revert it —250 when a value reverts, automation owns that field. Report which fields251 reverted, don't retry them, and hand the automation question to sf-flow252 or sf-apex (canon §8).2536. **Report** applied / reverted / failed / skipped with counts.254255## Bulk — safe mass updates256257For deliberate, large, uniform changes: owner reassignment, status258transitions, backfilling a new field, applying a convention.2592601. **Dry-run counts first.** Run the selection as `COUNT(Id)` before261 selecting rows, and show the user the number *and* the exact criteria.262 If the count differs materially from what the user expected, stop and263 reconcile — a mass update on a wrong filter is the single most expensive264 mistake in this skill's territory.2652. **Rollback plan before the first write.** Query and preserve `Id` plus266 the prior value of every field being changed, and keep it where the user267 can reach it (a file in code-execution modes; an in-context table capped268 to a reviewable size otherwise). State plainly what rollback restores269 (field values) and what it cannot (deletions, merges, fired automation,270 sent emails, downstream integration events).2713. **Chunk.** ≤200 records per call; sequence chunks and log which chunk272 ranges succeeded so a partial failure is resumable rather than273 re-runnable from zero.2744. **Guardrail-hook awareness.** The plugin's PreToolUse guardrails flag275 broad destructive DML and high-risk permission payloads and can turn a276 write into an explicit confirmation prompt. Expect it, don't work around277 it, and never restructure a call purely to slip under a threshold — if a278 guardrail fires, that is the moment to re-confirm scope with the user.2795. **Never mass-update** opt-out, bounce, or consent fields (canon §6);280 audit/system fields; or fields owned by a managed package, without a281 named, explicit instruction for that specific field.2826. **Verify** a sample of at least 20 records (or all, if fewer) after each283 chunk, and the full set at the end.284285Deletion is not a Bulk operation in this skill. Stale records get flagged and286dated (canon §7); when the user genuinely wants deletion, hand off to sf-data287with the scoped Id list and the rollback caveat stated.288289## Pitfalls290291| Pitfall | Handling |292| --- | --- |293| Field assumed, not described | Phase 0 is not optional — a proposal citing a nonexistent field discredits the whole run |294| Custom field shadowing the standard one | Populated-rate comparison, then ask which is authoritative before writing either |295| Managed-package field (CPQ, billing) | Describe shows the namespace; the package usually recomputes it — propose nothing there without the user's say-so |296| Formula / roll-up / auto-number field in a proposal | `calculated == true` fields are not writable — fix the inputs instead |297| Retired picklist value written back | Diff data values against the describe's `active: true` list (canon §1) |298| Validation rule rejects the whole batch | Read active rules in Phase 0; on `FIELD_CUSTOM_VALIDATION_EXCEPTION`, report the rule name rather than retrying |299| Record types change what's required and allowed | Verify picklists and required fields against the record's own RecordTypeId |300| Merge across different Accounts / Person Accounts | Not auto-groupable; explain the constraint instead of attempting it |301| Silent revert after a successful write | Verify step; attribute to automation, hand to sf-flow / sf-apex |302| Large object (500k+ rows) | Aggregate-only profiling, chunked selection, and offer to narrow scope before enumerating rows |303304## Cross-skill handoffs305306- Query syntax, selectivity, DML execution mechanics, deletions → **sf-data**307- Filling gaps from web research (titles, industries, company data) →308 **sf-leads**309- Campaign and member performance analysis → **sf-campaigns**310- Org-wide quality scoring and client-ready audit documents → **sf-audit**311 (also its `report-template.md` §7–8 when the user wants a document)312- The automation that reverts your writes → **sf-flow** / **sf-apex**313- Missing fields, picklist values, or duplicate rules that the data needs →314 **sf-metadata** (a metadata change, not a record update)315316## References317318| File | Read when |319| --- | --- |320| `references/object-playbooks.md` | Inspect and Fix — per-object fields, "good", problems, safe fix patterns, traps |321| `references/dedupe-strategies.md` | Dedupe — matching tiers, normalization, per-object keys, merge mechanics |322| `references/execution-modes.md` | Start of session — tool mapping, headless rule |