Skip to main content

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.

How-to·Last updated 2026-05-19·Drafted by Lira · Edited by German Medina

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 > 0 and Failures = 0: no failures recorded — every write returned 1. But sales reports the writes didn't land. Two possibilities: the writes returned 1 to 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 > 0 and Failures > 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:

  • CurrentStatus is NOT Engaged: the write genuinely didn't land. The log says 0 and SF confirms no change.
  • CurrentStatus is Engaged: the write landed despite the 0 return. 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.
  • LastModifiedDate is after LogTs: 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

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