Debugging silent Cloud-write failures
An AMPscript Cloud-write reported success at render time. The email shipped. Days later, the sales team notices the Salesforce records the email was supposed to update are stale. UpdateSingleSalesforceObject returned 0 silently — and 0 means seven different things. Five queries against de_log_sf_writes that separate the seven.
The email shipped. The Send Activity reported Completed. The AMPscript that was supposed to fire UpdateSingleSalesforceObject("Lead", @leadId, "Status", "Engaged") for every recipient ran, returned a number, and moved on. The email body rendered fine — the recipient saw exactly what they were supposed to see. Days later, the sales team asks why the lead statuses on the campaign target list haven't moved. The answer is that UpdateSingleSalesforceObject returned 0 (or worse, the AMPscript didn't even log the return value), and a 0 can mean seven different things with no distinguishing message — see gotchas — #10 and Cloud-write functions.
This page is the diagnostic playbook for that exact shape. Five queries against de_log_sf_writes (the log table the Style Guide discipline mandates) that separate "didn't fire" from "fired but blocked", and the recovery procedure that re-runs the writes against the affected subscribers.
The Cloud-write lifecycle and where it fails
[ AMPscript renders email for recipient ]
↓ messagecontext gate evaluates
[ Gate true (Send) → fire UpdateSingleSalesforceObject ]
↓ synchronous call across MC Connect
[ Salesforce processes the write attempt ]
↓ many failure modes return 0 silently:
↓ • Record not found (wrong Id or deleted since audience built)
↓ • API user lacks object permission
↓ • Field-level security blocks the field
↓ • Validation rule rejects
↓ • Record locked (approval, sharing)
↓ • MC Connect session expired mid-send
↓ • Field value doesn't match field type (bad picklist value)
[ Function returns 1 or 0; AMPscript continues ]
↓ Style Guide: log to de_log_sf_writes
[ Diagnostic happens here — days later ]Each failure mode at the "many failure modes" arrow looks identical from AMPscript's vantage point: 0. The diagnostic queries below separate them by reading the log table the script left behind plus cross-referencing with Salesforce's own audit data.
Step 1 — Is there a log at all?
The Cleon Style Guide rule is: every UpdateSingleSalesforceObject and CreateSalesforceObject is followed by an InsertData write to de_log_sf_writes recording the JobID, SubscriberKey, SF object type, SF record Id, operation, return value, and timestamp.
If the AMPscript followed the rule, the log exists. If it didn't, the diagnostic is much harder — start by reading the script to confirm whether logging was in place.
-- Confirm rows exist for the affected JobID
SELECT
COUNT(*) AS TotalAttempts,
SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) AS Successes,
SUM(CASE WHEN Result = 0 THEN 1 ELSE 0 END) AS Failures
FROM de_log_sf_writes
WHERE JobID = 'JOB-2026-05-19-01';Three failure shapes:
TotalAttempts = 0: no log rows. Either the AMPscript didn't run the Cloud-write at all (gate evaluated false, branch hidden, JobID typo), or the logging itself wasn't implemented. Check the AMPscript code; if logging isn't in place, the rest of this page can't help — add the logging discipline and the next send becomes diagnosable.TotalAttempts > 0andFailures = 0: no failures recorded — every write returned1. But sales reports the writes didn't land. Two possibilities: the writes returned1to AMPscript but Salesforce rejected them downstream (rare but happens with async triggers), or the script logged the wrong subscribers / wrong JobID. Cross-reference with the audience DE.TotalAttempts > 0andFailures > 0: the writes attempted but a portion failed. Continue to step 2 to bucket the failures.
Step 2 — Which operations fail more than others?
If failures are concentrated on one operation (say, Lead.Status update fails 80% of the time while Task.Create succeeds 100%), the failure mode is specific to that operation — usually FLS or a validation rule on the affected field.
SELECT
Operation,
COUNT(*) AS Attempts,
SUM(CASE WHEN Result = 1 THEN 1 ELSE 0 END) AS Successes,
SUM(CASE WHEN Result = 0 THEN 1 ELSE 0 END) AS Failures,
CAST(SUM(CASE WHEN Result = 0 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) AS DECIMAL(5,2)) AS FailurePct
FROM de_log_sf_writes
WHERE JobID = 'JOB-2026-05-19-01'
GROUP BY Operation
ORDER BY FailurePct DESC;What the breakdown tells you:
- One operation at near-100% failure: the operation is broken at the SF side. Likely a validation rule, FLS, or required-field error. Open the SF debug log for any one of those records (using the SfId from the log table) and check what the rule fired.
- One operation at moderate failure rate (say 10-30%): the failure is data-dependent. Some records don't have the prerequisite the write needs, or some records are in a state (locked, in approval) that blocks the write.
- All operations fail roughly equally: not operation-specific. MC Connect session was probably expired, or the API user lost a critical permission, or there's a tenant-wide rate-limit hit.
Step 3 — Failure distribution over time
The failure pattern over time tells you whether MC Connect's session went down mid-send.
SELECT
DATEPART(hour, Ts) AS Hour,
DATEPART(minute, Ts) AS Minute,
COUNT(*) AS Attempts,
SUM(CASE WHEN Result = 0 THEN 1 ELSE 0 END) AS Failures
FROM de_log_sf_writes
WHERE JobID = 'JOB-2026-05-19-01'
GROUP BY DATEPART(hour, Ts), DATEPART(minute, Ts)
ORDER BY Hour, Minute;Three patterns:
- Failures spread evenly across the whole send window: the failure cause is per-record, not session-level. Move to step 4 to bucket by subscriber.
- Failures clustered at a specific minute, after which the rate spikes to ~100%: MC Connect's session token expired or was revoked at that minute. Every write after that point failed. The fix is on the MC Connect side (re-authenticate the connection, audit token rotation policy); the immediate recovery is to re-run the writes against the failed subscribers via SSJS Activity batch (see Cloud-write functions — "Move to SSJS Script Activity instead").
- Failures clustered in bursts: rate-limit throttling. Salesforce's API rejected calls in chunks. For high-volume sends, this means AMPscript Cloud-writes were the wrong architecture — move to bulk SSJS or SQL-Activity-against-synced-DEs.
Step 4 — Cross-reference failures with the SF record state
For a sample of failed writes, fetch the current SF record state to see if the intended change is missing.
-- The log records the SfId; use it to query the synced SF DE.
-- (Replace 'Lead_Salesforce' with your synced DE name; the name
-- depends on MC Connect's configuration.)
SELECT
l.SubscriberKey,
l.SfId,
l.Operation,
l.Result AS LogResult,
l.Ts AS LogTs,
lead.Status AS CurrentStatus,
lead.LastModifiedDate
FROM de_log_sf_writes l
LEFT JOIN Lead_Salesforce lead
ON l.SfId = lead.Id
WHERE l.JobID = 'JOB-2026-05-19-01'
AND l.Result = 0
AND l.Operation = 'Lead.Status=Engaged'
ORDER BY l.Ts;Three confirmations:
CurrentStatusis NOTEngaged: the write genuinely didn't land. The log says0and SF confirms no change.CurrentStatusisEngaged: the write landed despite the0return. Rare but possible with async SF processing — the function reported failure before the SF backend finished committing. Investigate the SF debug log for the affected record to see whether the write succeeded with a warning the function didn't surface.LastModifiedDateis afterLogTs: someone or something else updated the record after the AMPscript attempt. The current state is unrelated to what the AMPscript tried; you need the SF audit history to know.
Step 5 — Recovery: re-run against the failed subscribers
Once the failure pattern is understood, recover by re-issuing the writes against the failed-subscriber list. Don't re-send the email; just re-issue the SF writes via an SSJS Script Activity that batches more reliably than AMPscript inline.
-- Build the recovery list: subscribers whose Cloud-write failed
SELECT
l.SubscriberKey,
l.SfId,
l.Operation
INTO de_recovery_sf_writes_<jobid>
FROM de_log_sf_writes l
LEFT JOIN Lead_Salesforce lead
ON l.SfId = lead.Id
AND lead.Status = 'Engaged' /* exclude rows where the write actually landed */
WHERE l.JobID = 'JOB-2026-05-19-01'
AND l.Result = 0
AND lead.Id IS NULL;Hand the recovery DE to an SSJS Script Activity (using Platform.Function.UpdateData against synced DEs, or WSProxy for direct SF updates) that batches the retries with proper logging — see MC SSJS — debugging WSProxy auth for the auth-refresh pattern needed in any long retry loop.
After recovery, write the postmortem to de_log_ampscript_postmortems (same pattern as the other AMPscript debugging snippets):
INSERT INTO de_log_ampscript_postmortems
SELECT
GETDATE() AS DiagnosedAt,
'WelcomeEmail_LeadStatusUpdate' AS AssetName,
'JOB-2026-05-19-01' AS JobID,
NULL AS AffectedSubscriberKey,
'14% of UpdateSingleSalesforceObject(Lead.Status) calls returned 0; sales saw stale data' AS Symptom,
'FLS on Status changed for the API user profile two days before the send; writes silently failed' AS RootCause,
'Restored FLS; re-ran via SSJS batch from de_recovery_sf_writes_*; updated permission-rotation runbook' AS Fix;The RootCause column forces clarity. "FLS changed two days ago" is the root cause; the symptom was stale data. The Fix includes both the immediate recovery and the runbook update so the rotation that caused this gets handled differently next time.
Common causes ranked by frequency
| Cause | How to spot | Fix in |
|---|---|---|
| FLS or permission change on the API user | Step 2 shows one operation at near-100% failure | Audit MC Connect's API user profile in Salesforce Setup |
| MC Connect session expired mid-send | Step 3 shows clean cutoff after which failure rate spikes to ~100% | Re-authenticate MC Connect; audit token rotation policy |
| Validation rule on the SF object | Step 2 shows one operation at moderate failure rate; SF debug log reveals the rule | Coordinate with SF admin team; either adjust the rule or pre-validate upstream |
| Rate-limit throttling at SF API | Step 3 shows clustered failure bursts | Move from AMPscript inline to SSJS batch (see Cloud-write reference) |
| Wrong SfId in the audience DE | Step 4 shows LEFT JOIN returning NULL for many failed writes | Audit audience-build SQL; ensure SfId comes from the right source |
| Required field on Create missing | Step 2 shows CreateSalesforceObject failing | Audit the field-pair count and required-field list against SF schema |
| AMPscript logging missing | Step 1 returns no rows but writes were attempted | Add the InsertData log row pattern to every Cloud-write call site |
| Cloud-write outside the _messagecontext gate | Preview test renders + log rows appear from QA previews | Style Guide gate audit; never write outside the perimeter |
Related
- Basics — the runtime contexts AMPscript runs in
- Cloud-write functions — the function inventory and the patterns this page diagnoses
- Data Extension functions —
InsertDatafor the log row pattern - Subscriber + Profile functions —
_messagecontextfor the perimeter gate every Cloud-write needs - MC AMPscript gotchas — see #10 (silent Cloud-write failure pattern, the gotcha this page diagnoses)
- Style Guide — the merge-time discipline that mandates
de_log_sf_writesfor every Cloud-write - Debugging render-time blanks — sibling debugging snippet (failures inside the email body)
- Debugging preview-vs-send mismatch — sibling debugging snippet (failures from context divergence)
- MC SSJS — Debugging WSProxy auth — the SSJS pattern for long-running retry loops needed when recovering from Cloud-write failures
Catalog progress with this page: AMPscript section closes at 14 page-pairs — 1 production-note + 9 reference + 1 decision-framework + 3 how-to debugging. Same shape as the SQL and SSJS catalogs. Next: Config subcategory (currently empty).