Cloud-write functions — Marketing Cloud AMPscript reference
AMPscript's bridge to Sales/Service Cloud — UpdateSingleSalesforceObject, CreateSalesforceObject, RetrieveSalesforceObjects. The highest-stakes surface in the language: writes are inline, return 1/0 without throwing, and can hit live production CRM records from a preview if you forget the messagecontext gate.
The Cloud-write functions are the highest-stakes surface in AMPscript. They reach across Marketing Cloud Connect (MC Connect) into Sales/Service Cloud and create, read, or update records there. Three things make this dangerous: the writes are synchronous and inline with email render, they return 1/0 without throwing on any failure (permission, validation, missing field), and they can hit live production CRM records from a preview if the _messagecontext gate is missing. A single forgotten gate plus a curious team member previewing an email is enough to update real Opportunities, Cases, or Leads with test data.
This page is the inventory plus the patterns that keep these calls safe. The "log every result" discipline from the Data Extension page applies here even more strictly — a failed Cloud-write is invisible without the receipt.
Official syntax
%%[
/* UpdateSingleSalesforceObject — update one record. Returns 1/0. */
SET @result = UpdateSingleSalesforceObject(
"Lead", /* SF object type */
@leadId, /* SF record Id */
"Status", "Engaged", /* field-value pair */
"LastEmailOpened__c", Now() /* another pair */
)
/* CreateSalesforceObject — create one record.
2nd arg is the *count of field pairs* — easy to miscount. */
SET @newId = CreateSalesforceObject(
"Task",
4, /* 4 field pairs follow */
"WhoId", @leadId,
"Subject", "Followed email click",
"Status", "Not Started",
"ActivityDate", Now()
)
/* RetrieveSalesforceObjects — read records. Returns a rowset
like LookupRows; Field/Row/RowCount apply. */
SET @rows = RetrieveSalesforceObjects(
"Lead", /* SF object type */
"Id, Status, Owner.Name", /* comma-separated field list */
"Email", /* filter field */
"=", /* operator */
@subscriberEmail /* filter value */
)
SET @n = RowCount(@rows)
IF @n > 0 THEN
SET @lead = Row(@rows, 1)
SET @leadId = Field(@lead, "Id")
SET @status = Field(@lead, "Status")
ENDIF
]%%The supported set:
| Function | Purpose | Notes |
|---|---|---|
| UpdateSingleSalesforceObject(type, id, f1, v1, f2, v2, ...) | Update one SF record | Returns 1/0; silent on permission/validation failure |
| CreateSalesforceObject(type, nFields, f1, v1, f2, v2, ...) | Create one SF record | 2nd arg is field-pair count; returns new record Id on success, NULL/empty on failure |
| RetrieveSalesforceObjects(type, fieldList, filterField, op, filterVal) | Read SF records | Returns rowset; RowCount = 0 means no match or read failure (no distinction) |
| RetrieveSalesforceObjects(type, fieldList, ff1, op1, fv1, ff2, op2, fv2, ...) | Multi-filter variant | Pairs of filter triplets; same return shape |
Reference:
- AMPscript Guide — function reference (Salesforce section) ↗
- Salesforce Help — Marketing Cloud Connect ↗
What survives in production
_messagecontext == "Send" is mandatory before any Cloud-write
A UpdateSingleSalesforceObject call inside an email body runs every time the email is rendered — including in Email Studio's preview. Without the gate, every preview-render fires a real write against production Salesforce records. The same applies to test sends.
%%[
IF Lowercase(_messagecontext) == "send" THEN
/* Only fires on real production sends */
SET @result = UpdateSingleSalesforceObject(
"Lead", @leadId,
"Status", "Engaged",
"LastEmailOpened__c", Now()
)
InsertData(
"de_log_sf_writes",
"JobID", _jobid,
"SubscriberKey", _subscriberKey,
"SfObject", "Lead",
"SfId", @leadId,
"Operation", "Status=Engaged",
"Result", @result,
"Ts", Now()
)
ENDIF
]%%The hand-off failure: someone removes the gate "to test in preview", forgets to put it back, ships the email. Every QA preview after that point silently overwrites real Lead Statuses with the test value. The cost is paid by sales and customer success teams who see corrupted CRM data with no clear trigger.
The Cleon convention: the _messagecontext gate goes around the entire side-effecting block, not around individual write calls. The gate is the perimeter; the writes are inside.
UpdateSingleSalesforceObject returns 1/0 — and 0 means many things
A 0 return value can mean:
- The record doesn't exist (wrong Id, or deleted since the audience was built)
- The API user doesn't have permission to update the object
- The API user doesn't have permission to update one of the specific fields
- A validation rule fired on the SF side
- The record is locked (approval process, sharing, record-locking)
- MC Connect lost its session token mid-send
- Field-level security blocks the field on this user's profile
- The value doesn't match the field's data type (e.g. picklist value not in the list)
The function returns 0 for all of them with no distinguishing message. The log is the only diagnostic.
%%[
IF Lowercase(_messagecontext) == "send" THEN
SET @result = UpdateSingleSalesforceObject(
"Lead", @leadId,
"Status", "Engaged"
)
/* DURABLE — log every call with enough context to diagnose later */
InsertData(
"de_log_sf_writes",
"JobID", _jobid,
"SubscriberKey", _subscriberKey,
"SfObject", "Lead",
"SfId", @leadId,
"Operation", "Status=Engaged",
"Result", @result,
"Ts", Now()
)
ENDIF
]%%Audit de_log_sf_writes after every Send: SELECT COUNT(*) WHERE Result = 0 GROUP BY Operation tells you which writes are failing and how many. Without the log, the only signal is sales noticing CRM data is stale.
CreateSalesforceObject's field-count argument is a footgun
The 2nd argument is "how many field-value pairs follow." Miscount it and AMPscript either silently truncates the input list or returns NULL with no useful error.
%%[
/* AT RISK — the 4 is wrong; there are actually 3 pairs.
AMPscript may silently skip the last pair, may return NULL,
may write garbage. Behavior is tenant-dependent. */
SET @newId = CreateSalesforceObject(
"Task",
4, /* ← wrong; only 3 pairs follow */
"WhoId", @leadId,
"Subject", "Followed email click",
"Status", "Not Started"
)
/* DURABLE — count the pairs visually with one-per-line formatting
and align them so the count is verifiable in code review */
SET @newId = CreateSalesforceObject(
"Task",
4, /* 4 pairs ↓ */
"WhoId", @leadId,
"Subject", "Followed email click",
"Status", "Not Started",
"ActivityDate", Now()
)
IF Empty(@newId) THEN
/* Creation failed — log and don't trust @newId */
InsertData(
"de_log_sf_writes",
"JobID", _jobid,
"SfObject", "Task",
"Operation", "Create",
"Result", 0,
"Ts", Now()
)
ENDIF
]%%The Cleon convention: vertical one-pair-per-line formatting with a comment counting them at the top. Code review catches a mismatch immediately.
RetrieveSalesforceObjects is paginated under the hood — but the page size is opaque
RetrieveSalesforceObjects returns a rowset, but how many rows that rowset contains depends on the SF query plan, the API limit, and the tenant. There's no documented row cap for AMPscript's Cloud reads the way LookupRows has 2000 — but assume a similar truncation can occur silently.
%%[
SET @rows = RetrieveSalesforceObjects(
"Opportunity",
"Id, Amount, StageName",
"AccountId", "=", @accountId
)
SET @n = RowCount(@rows)
/* For high-value accounts with many opportunities, @n may be
truncated. Audit at scale before relying on full result sets. */
/* The defense matches DE: shape the data upstream when the audience
has many rows. A SQL Activity that joins MC audience × SF data
via the synchronized DEs runs once, produces the result set,
and AMPscript reads a pre-shaped DE column. */
]%%If the work requires reading more than ~100 SF records per recipient, the right tool isn't AMPscript — it's SQL Activity against the synchronized DEs (the DEs Marketing Cloud Connect populates from SF data). The synced DEs are large, queryable with SQL, and don't have the per-recipient cost.
MC Connect must be configured before any Cloud function call
These functions require the tenant to have Marketing Cloud Connect installed and configured against a specific Salesforce org. Without that:
UpdateSingleSalesforceObjectreturns0immediately with no SF call attemptedCreateSalesforceObjectreturns NULLRetrieveSalesforceObjectsreturns an empty rowset
The functions don't tell you "MC Connect isn't configured" — they just behave as if the SF side accepted-and-rejected every call. If you're inheriting an MC tenant and a previously-working Cloud-write block suddenly returns all 0s, check whether MC Connect is still authenticated against the SF org before debugging the script.
Cross-BU / cross-org scenarios change which SF org receives the write
If the MC tenant has multiple Business Units connected to different Salesforce orgs, the Cloud-write function fires against the org connected to the BU running the send. This is usually right, but the failure shape: an email moved from BU A (connected to Sandbox) to BU B (connected to Production) silently shifts every Cloud-write from sandbox CRM to production CRM. There's no warning at send time.
%%[
/* The function uses MC Connect for the current BU's connected org.
If you migrated the email between BUs, audit the writes after
the first send in the new BU before trusting the pattern. */
]%%The Cleon convention: before promoting any Cloud-write email between Business Units, document the connected SF org explicitly in the run-history DE and verify the first production send wrote to the expected org. The cost of a mismatch is unwinding bad updates in production CRM.
Rate limits are a real constraint at scale
Salesforce API limits apply to AMPscript Cloud-write calls. A send to 50,000 recipients with one UpdateSingleSalesforceObject per recipient is 50,000 SF API calls — well into rate-limit territory for many tenants. The function doesn't queue, doesn't retry, doesn't slow itself down. Calls past the rate limit return 0.
%%[
/* For high-volume sends, pre-compute the writes in a single
synchronized DE update upstream, OR batch via Salesforce's
bulk-API-friendly tooling. AMPscript Cloud-writes are for
low-volume, high-stakes patterns (transactional emails,
personalized follow-ups), not bulk updates. */
]%%If your send is over a few thousand recipients and every one needs a Cloud-write, the architecture is wrong — move the write to an SSJS Script Activity that batches, or to a SQL Activity against the synced DE that updates the SF object via MC Connect's bulk path.
Quick decision
Use UpdateSingleSalesforceObject when:
- Updating one SF record per recipient in a low-volume send (transactional, triggered, sub-1000 audience). Always inside a
_messagecontext == "Send"gate, always logged.
Use CreateSalesforceObject when:
- Creating one SF record per send event (a Task, a Note, an event log row). Count field pairs in vertical formatting. Gate. Log.
Use RetrieveSalesforceObjects when:
- Reading a small number of SF records per recipient (the latest Opportunity, the Owner of a Lead). For larger reads, use the synced DEs via SQL Activity upstream.
Move to SSJS Script Activity / SQL Activity instead when:
- The audience is over a few thousand and every recipient triggers a write. Rate limits will throttle.
- The same SF update applies to every recipient — pre-compute in SQL against the synced DE, run as a single Activity step before the send.
- You need retry logic or batch semantics. AMPscript Cloud-writes are fire-and-(maybe-)forget.
- You're reading many SF records per recipient. The synced DEs are designed for this.
Related
- Basics — supported language surface
- Subscriber + Profile functions —
_messagecontextis the gate every Cloud-write needs - Data Extension functions —
InsertDatafor logging Cloud-write results tode_log_sf_writes - Validation functions —
Empty()for guarding the result ofCreateSalesforceObject(returns NULL on failure) - MC AMPscript gotchas — see #10 (silent Cloud-write failure pattern, the failure shape this page diagnoses)
- MC SSJS — WSProxy — the SSJS equivalent for batch SF operations; what you reach for when AMPscript Cloud-writes hit rate limits
More AMPscript reference pages incoming: Encoding/Hashing · Style Guide.