Playbook · Write Access · Draft for Review

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.

Pattern: dry-run + confirm + threshold + audit + Slack, in a sandbox first
Baseline: Brian's "Granting AI write access without giving up control" (2026-05-28)
Today: read-only Salesforce API is live (Jason). Two team members read. Goal: expand to write.
Status: DRAFT for Jason + Brian review. Answer the open questions inline (section 09).

TL;DR

  1. The five guardrails carry over from Meta unchanged: dry-run, threshold, Google Sheet audit log, Slack DM, platform revocation.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

The Salesforce wrinkle (read this before anything else)

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.

Layer 1 Dry-run by default

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.

Layer 2 Threshold on the blast radius

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.

Layer 3 Google Sheet audit log

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.

Layer 4 Slack DM on every write

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.

Layer 5 Platform identity and one-step revocation

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.

Belt + suspenders If the audit append fails, the write is NOT rolled back

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.

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.

What this depends on

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.

PieceWhere in SetupWhat 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.
Why this is a permissions change, not a license purchase

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

  1. Setup › Users › Permission Sets › New. Name it PO Write (Integration).
  2. Object Settings › the objects to make writable. Check Edit (and Create if needed). Leave Delete unchecked.
  3. Assign it to the integration user (per-person or shared, per section 05).
  4. Point the scripts at the sandbox and run the full test cycle (section 07).
  5. Only after the sandbox cycle passes end to end, enable against production.
One thing to verify (Q7)

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.

OptionWhat it meansTrade-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.

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:

  1. 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.
  2. Place credentials at a path with mode 600 (owner-read-only).
  3. Run the smoke-test sequence (section 07) against the sandbox.
  4. Once the sandbox cycle passes, they are cleared for production writes.
Secret transfer rule (carried over from Brian)

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.

TEST 1 — AUTH WORKS "List the objects I can access in Salesforce." Expect: returns the PO and Product objects. Confirms the connection and scope. TEST 2 — IDENTITY "Which Salesforce user is this API acting as?" Expect: returns the integration user (per-person name, or apireadonly if shared). TEST 3 — READ SCOPE "Show me the 3 most recent Purchase Orders." Expect: 3 PO rows with IDs + fields. No write attempt. TEST 4 — DRY-RUN PREVIEW "Update the status on PO #### to ." Expect: DRY-RUN preview with before/after. No change in Salesforce. No Slack ping. No audit row. TEST 5 — LIVE CONFIRM (SANDBOX) "Now run it for real." Expect: the PO actually updates in the sandbox. Audit row appears in the Sheet within seconds with the real human's email in the actor column. Slack DM lands with before -> after. TEST 6 — THRESHOLD BREACH "Change on PO #### past the limit." Expect: first confirm=true call returns blocked: true with the delta. No write. To execute, re-call with both confirm=true AND over_threshold_confirm=true. TEST 7 — AUDIT LOG INTEGRITY Open the Sheet, scroll to today's rows, verify the columns line up. Expect: timestamp, actor (real human email), object, record ID, before/after all populated.
Promotion gate

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 revokeWhere
Write capability onlySetup › 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 onceRevoke or delete the External Client App SFOAuthEG. Every OAuth token issued from it dies.
Why the recipient's local disk doesn't matter

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.

(so we know who answered what)
Q1Jason

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.

Saved ✓
Q2Jason

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.

Saved ✓
Q3Jason

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.

Saved ✓
Q4Jason

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.

Saved ✓
Q5Jason

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.

Saved ✓
Q6Jason

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).

Saved ✓
Q7Jason

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.

Saved ✓
Q8Brian

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.

Saved ✓
Q9Brian

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.

Saved ✓
Q10Anyone

General comments on this draft. What's wrong, what's missing, what you'd cut.

Saved ✓