Granting AI write access to Salesforce without giving up control.
Brian's Meta write-access playbook, adapted to Salesforce. The pattern carries over. One thing does not, and it is the thing the whole rollout turns on: who the audit log names.
TL;DR
- The five guardrails carry over from Meta unchanged: dry-run, threshold, Google Sheet audit log, Slack DM, platform revocation.
- The break: today's Salesforce API runs on one shared integration user (
apireadonly). Give that single user write access and Salesforce's own history stamps "apireadonly" on every change, not the person who made it. Brian's whole model depends on the opposite. - So the first decision is identity: one Integration-license user per teammate (Brian-consistent), or keep the shared user and make our own audit log the system of record for "who." That answer (from Jason) sets the spine of everything below.
- Salesforce gives us one thing Meta did not: a full sandbox. Every write test runs there before production. That is a real upgrade to the testing protocol.
- Every grant and every write is logged, and a scheduled Claude job reads those logs for us, flagging anomalies to Slack. The audit is automatic, not a human remembering to check a sheet.
- Going to write is a permissions change on the existing integration, not a new license. Add an Edit-enabled permission set, assign it, test in sandbox, promote.
01 — The principleAudit comes first, capability comes second
If the change shows up in a log with a real human's name on it within seconds, you can ship write tools. If it does not, you can't ship any.
This is Brian's rule, lifted straight from the Meta playbook: "If the platform's audit log shows 'the API user did X' with the API user being identifiable, go per-user. If audit logging is broken or absent, share." Write access without an answer to "who did this and when" is the actual risk. Everything else is protection against operator mistakes.
Meta hands you per-actor audit for free: each teammate gets their own System User, so the log reads "Hannah did X." Salesforce, as set up today, does not. The live integration runs on a single shared user, apireadonly@emazinglights.com. Salesforce's native audit (LastModifiedBy, Field History Tracking, Setup Audit Trail) will attribute every write to that one user, no matter which human triggered it.
By Brian's own override rule ("write access is always per-user"), that is a hard stop until we decide how identity works. Section 05 lays out the two ways to clear it.
There are two honest ways to satisfy the principle on Salesforce:
- Per-person Salesforce identity. One integration user per teammate, each on a Salesforce Integration license. The native audit log then reads like a sentence, exactly like Meta. Depends on license cost and availability (open question for Jason).
- Shared user, audit moved up a layer. Keep one integration user, but our own Google Sheet audit log becomes the system of record for "who," because Claude passes the real human's email on every write. This is Brian's "GA4 shared service account" pattern applied to writes. It is a deliberate deviation from his rule and needs explicit sign-off, because Salesforce-native history alone can no longer tell you who did it.
02 — The guardrailsFive layers, plus one Salesforce bonus
Each layer catches a different class of mistake. None is optional. Salesforce adds a sixth that Meta never had.
Every write takes confirm = false. The first call returns a structured preview (before, after, summary string) and never touches Salesforce. The operator reads it, then re-issues with confirm = true to execute. No write path skips this, even for "obvious" updates.
Meta refused any budget change over 20% without a second override flag, to catch the "$200 to $2,000" typo. Salesforce needs its own number tied to what a bad write would cost. Candidate: a PO total or quantity above a set dollar/percent bound requires over_threshold_confirm = true. The exact metric and number are open (Q6). The audit row records the override when it fires.
Every confirmed write appends one row: timestamp_utc | actor | object_type | record_id | action | before | after | sf_api_response | notes. The actor column is the real human's email, passed by Claude at call time. On a shared integration user, this row is the only place "who" is reliably recorded. Brian, Jason, and Anthony have writer access. Sheet ID lives in the audit script, not in the recipient's environment.
Best-effort post to a dedicated channel (e.g. #salesforce-writes) with action summary, before/after diff, actor, and a link to the audit Sheet. Both the operator and a reviewer are members. If Slack is down the write still executes; we never roll back over a notification failure.
The lock lives in Salesforce, not on anyone's laptop. The integration user's profile plus permission sets decide what the API can do. Pull the write permission-set assignment and write capability dies instantly, org-wide. The External Client App (SFOAuthEG) can be revoked to kill all OAuth tokens at once.
The Salesforce change is already real on Salesforce's side. We surface audit_log: "FAILED: ..." in the response so the operator logs it manually. Better a visible gap than a fabricated rollback that may not work.
Meta has no test environment, so Brian tested against real ad accounts using control campaigns. Salesforce gives us a full sandbox: a real copy of the org where writes hurt nothing. Every write test in this playbook runs in the sandbox first. Production write access is only enabled after the full test cycle passes in sandbox. This is a genuine upgrade over the Meta protocol, not a nice-to-have.
03 — The auto-auditorClaude reviews the log, not a human
An audit log only works if someone reads it. Nobody reads it reliably. So a scheduled Claude job reads it for us and flags what looks wrong, before it becomes a problem.
This is the piece that makes write access safe at scale: every grant and every write is logged, and a Claude job audits those logs on a schedule. No team member has to monitor themselves, and no human has to babysit the sheet.
Two things get logged
- Access changes. Every time the write permission set is assigned or removed: who got write access, to which objects, who approved it, and when. Salesforce Setup Audit Trail already records permission-set assignment changes, so the auditor reads it directly. We always know who can write, and since when.
- Writes. The Google Sheet audit log from Layer 3: actor (real human), object, record, before/after, and any threshold override.
The review job
A scheduled job (e.g. daily) runs headless Claude over the new rows since its last run, pulling from the write log, Salesforce Setup Audit Trail, and Field History. It flags:
- A write with no matching audit row (someone bypassed the guardrails).
- Any threshold override, and whether it looked justified.
- Any delete (should never happen).
- A write to an object outside the approved scope.
- A write by someone whose grant is not on the approved-access list.
- Off-hours activity or volume spikes.
- Audit-append failures (the belt-and-suspenders gap from Layer 3).
Then it posts to Slack: a green "clean" check when nothing is wrong, or a flagged list with record links when something is. Routed to you, and optionally Jason and Brian.
One scheduled auditor owns the review and alerts the group, so we do not get three Claudes triple-pinging the same issue. Any of us can still ask our own Claude to review the log on demand ("anything off in the Salesforce writes this week?"). The scheduled job is the safety net; the ad-hoc check is the spot inspection.
The auditor is only as good as the logs feeding it. That makes Field History Tracking and Setup Audit Trail retention (Q4) load-bearing, not optional. If a target object is not tracked, the auditor is blind to any write that bypasses our Sheet.
04 — Salesforce setupWhat exists today, and the one piece to add
Four pieces are already built and verified by Jason. Write access adds exactly one more.
| Piece | Where in Setup | What it does |
|---|---|---|
Integration userapireadonly@emazinglights.com |
Setup › Users › Users | The API identity. License = Salesforce Integration (API-only, no human seat, no extra cost). Profile = a custom read-only profile. |
Read-only profileMinimum Access - API Read Only |
Setup › Users › Profiles | Enforces read-only. Read checked, Edit/Create/Delete unchecked. This is the current lock. |
Read permission setread-only field access |
Setup › Users › Permission Sets | Additive grant on top of the profile. Field-level Read on the Purchase Order (header + lines) and Product objects. Read only. |
External Client AppSFOAuthEG |
Setup › Platform Tools › Apps › External Client Apps | Issues the OAuth credentials the scripts use. Note: this is NOT under "App Manager." External Client Apps live in their own section. |
Write permission set (NEW)PO Write (Integration) |
Setup › Users › Permission Sets › New | The only new piece. Object settings: check Edit (and Create if adding records) on the chosen objects. Leave Delete unchecked. Assign to the integration user. Remove the assignment to revert. |
The Integration license already allows API writes. Read-only today is a permissions choice, not a license limit. Going to write reuses the same user, the same External Client App, and the same credentials, and adds one Edit-enabled permission set on top. The read-only side stays exactly as it is.
The write path, step by step
- Setup › Users › Permission Sets › New. Name it
PO Write (Integration). - Object Settings › the objects to make writable. Check Edit (and Create if needed). Leave Delete unchecked.
- Assign it to the integration user (per-person or shared, per section 05).
- Point the scripts at the sandbox and run the full test cycle (section 07).
- Only after the sandbox cycle passes end to end, enable against production.
In Salesforce, an additive permission set can grant Edit even when the base profile has Edit unchecked. We are relying on that. Jason to confirm nothing at the org or profile level overrides it for this integration user.
05 — Shared vs per-userThe one decision the rollout turns on
Shared by default for reads. Per-user when the platform gives you the audit trail for free. Write access is always per-user. Salesforce makes us choose how to honor that.
| Option | What it means | Trade-off |
|---|---|---|
| A. Per-person integration users | One Salesforce Integration-license user per teammate (e.g. "Ross - SF Ops"). Native LastModifiedBy and Field History read like a sentence. Matches Brian's Meta model exactly. |
Depends on how many Integration licenses are free and what extras cost (Q1). Slightly more setup per person. |
| B. Shared user + app-layer audit | Keep the one integration user. The Google Sheet audit log (Layer 3) becomes the system of record for "who," since Claude passes the human's email on every write. | Salesforce-native history shows "apireadonly" for every change. If our audit log is bypassed or fails, attribution is lost. Deviates from Brian's rule. |
Go Option A (per-person) if Salesforce Integration licenses are free or cheap, because it keeps the audit trail at the platform layer where it cannot be bypassed. That is the whole point of the model. Fall back to Option B only if extra Integration licenses cost real money, and if so, harden the app-layer audit (no write path that skips the Sheet, alert on any audit failure). The number that decides this is Q1.
06 — The handoffWhat the team member actually does
Same spirit as Meta: no jargon, secure credential transfer, smoke tests before live. One difference: Salesforce is a direct API integration, not an MCP (Q8).
Today's Salesforce access is direct API scripts, not an MCP server, so the handoff differs from Brian's drag-a-folder flow. Whether we keep scripts or wrap them in a Salesforce MCP (with Brian's reusable dry-run helper) is an open question for Brian (Q8). Either way, the recipient experience is:
- Receive the integration package and the credentials. Credentials go via Signal, a disappearing Slack DM, 1Password share, or a private Drive link that gets revoked after they grab the value. Never plaintext email, never a shared channel.
- Place credentials at a path with
mode 600(owner-read-only). - Run the smoke-test sequence (section 07) against the sandbox.
- Once the sandbox cycle passes, they are cleared for production writes.
Tokens never travel by plaintext email or a shared channel. Signal, disappearing Slack DM, 1Password share, or a revoke-after-read Drive link only.
07 — Testing protocolThe exact sequence, in the sandbox
Read first. Then dry-run preview. Then a live write in the sandbox. Then a deliberate threshold breach. Each step verifies a different guardrail. Nothing runs against production until this passes.
All seven pass in the sandbox, then and only then do we assign the write permission set against production and re-run Test 5 once on a low-stakes production record. Do not skip Test 6, the threshold breach. That is the one that catches a misconfigured guardrail.
08 — RevocationWhen someone leaves, or loses access
Every entry is reversible at the Salesforce layer. We never have to chase a token on someone's laptop.
| To revoke | Where |
|---|---|
| Write capability only | Setup › Permission Sets › PO Write (Integration) › remove the assignment. Write dies instantly, read stays. |
| One person (if per-person users) | Setup › Users › freeze or deactivate that integration user. Their tokens go inert. |
| All API access at once | Revoke or delete the External Client App SFOAuthEG. Every OAuth token issued from it dies. |
Credentials on their Mac are inert bytes the moment the Salesforce-side permission set or user is revoked. We don't chase the laptop. This is the entire reason the lock lives in Salesforce, not in the token.
09 — Open questionsAnswer inline, and we move
These are the gaps that block a final version. Jason owns the Salesforce mechanics; Brian owns the pattern calls. Type your answer and hit submit. Answers collect automatically.
How many Salesforce Integration User licenses do we have available, and do additional ones cost anything?
This decides per-person vs shared identity (section 05), which is the spine of the whole playbook.
Which objects and fields should be writable? Is Create allowed or Edit-only on existing records? Delete stays off, correct?
Defines the blast radius and what the write permission set actually grants.
Is there a Salesforce sandbox we can point the write integration at for testing? What is the login / instance URL?
The whole testing protocol (section 07) runs in the sandbox before production.
Is Field History Tracking enabled on the target objects, and how long does Setup Audit Trail retain entries?
The auto-auditor (section 03) reads these directly. If a target object is not tracked, the auditor is blind to any write that bypasses our Sheet, so this is load-bearing, not optional.
Write via REST sObject calls or Bulk API? Any validation rules, required fields, triggers, or approval processes on the PO objects that a write could trip?
These are the landmines that make a "simple" write fail or fire side effects. Need them mapped before we test.
What dollar amount or percent change on a PO should require a second explicit override (the threshold guardrail)?
Meta used 20% on budget. Salesforce needs its own number tied to what a bad write would cost (Layer 2).
Does anything at the org or profile level override an additive permission set, i.e. will a new Edit-enabled permission set actually grant write on the read-only profile?
The entire write path (section 04) assumes the additive permission set wins. Need that confirmed.
Do we build a Salesforce MCP with the dry-run tools, or keep the existing direct-API scripts and bolt the guardrails on? Does your preview_then_execute() helper port cleanly?
Decides the integration shape (section 06) and how much of your Meta tooling we reuse.
Anything in the five-guardrail model you'd change for a system-of-record database (Salesforce) versus an ad platform (Meta)? Especially around the shared-user audit deviation.
You wrote the rule. We're proposing to bend it (Option B). Tell us if that's acceptable or a hard no.
General comments on this draft. What's wrong, what's missing, what you'd cut.