AMPscript de Marketing Cloud: Style Guide
Las reglas opinionadas que Cleon aplica a cada bloque AMPscript que entrega en Marketing Cloud — naming, formato, comentarios, patrones a preferir, anti-patrones a rechazar — destiladas de los gotchas y las páginas de referencia en un solo documento de disciplina. Espeja las Style Guides de SQL y SSJS.
Esta es la página donde Cleon deja de describir lo que AMPscript es y empieza a decir lo que hacemos con él. Salesforce define lo que funciona. Las páginas de referencia documentan la superficie de funciones. Los gotchas documentan lo que rompe en render time. Esta Style Guide es la disciplina que mantiene un email o una CloudPage mantenibles un año después de que la entregamos.
Usala como checklist antes de mergear cualquier bloque AMPscript nuevo a producción. Las reglas son cortas a propósito — cuando una regla necesita explicación, la explicación está en la página que linkea.
Naming
Las variables locales siguen una convención consistente
Decidida una vez, antes de escribir el primer content block. Las variables AMPscript llevan prefijo @; el resto del nombre sigue la convención de case del proyecto.
| Convención | Se ve como | Cuándo |
|---|---|---|
| @camelCase | @firstName, @subscriberKey, @orderTotal | Default para proyectos nuevos |
| @PascalCase | @FirstName, @SubscriberKey | Tenants MC legacy donde las columnas de DE usan PascalCase |
| @snake_case | @first_name, @subscriber_key | Algunos equipos alineándose con las convenciones de SQL |
El patrón: elegí una, documentala en el repo del proyecto, nunca mezcles. La falla del hand-off es leer un bloque y no saber si @FirstName es la misma cosa conceptual que @firstName de otro bloque distinto — la próxima persona asume que sí, refactoriza, y o rompe la personalización o duplica el trabajo.
Las variables llevan intención
Una variable llamada @subscriberKey lleva la decisión; una llamada @k no lleva nada. Siempre nombrá las variables por lo que contienen, no por el índice del loop en el que viven.
%%[
/* EVITAR */
FOR @i = 1 TO RowCount(@r) DO
SET @k = Field(Row(@r, @i), "k")
NEXT @i
/* PREFERIR */
FOR @i = 1 TO RowCount(@rows) DO
SET @subscriberKey = Field(Row(@rows, @i), "SubscriberKey")
NEXT @i
]%%@i está bien como índice numérico. @r y @k no, ni con un comentario al lado.
Fuentes de data nombradas distinto
Las tres fuentes de las que AMPscript lee — columnas de DE, Subscriber Attributes, variables locales — tienen que nombrarse distinto cuando tienen el mismo valor conceptual. Ver gotchas — #4 para la justificación completa.
| Fuente | Convención de naming |
|---|---|
| Columna del DE sendable | El nombre del schema del DE como está (FirstName) |
| Subscriber Attribute (perfil) | Prefijo o sufijo para desambiguar (ProfileFirstName) |
| Variable local | Local con prefijo @ (@firstName) |
La convención Cleon: cuando una columna de DE y un Subscriber Attribute tienen el mismo valor lógico, renombrá el Subscriber Attribute (o no lo uses para nada). No te apoyes en las reglas implícitas de resolución de AMPscript.
El naming de DE se alinea con el Style Guide de SQL
La convención de prefijos de DE del Style Guide de SQL aplica a los DEs que escribe AMPscript también — de_log_* para writes de log, de_stg_* para staging, de_email_* para DEs sendable / pre-formateados de contexto-email. Los scripts AMPscript son consumidores pesados de DE; el naming consistente entre SQL Activities y reads de AMPscript es la disciplina que escala.
Formato
Delimitadores de bloque en sus propias líneas
%%[
/* lógica multi-línea — abrí y cerrá en sus propias líneas */
SET @firstName = AttributeValue("FirstName")
IF Empty(@firstName) THEN
SET @firstName = "Friend"
ENDIF
]%%Los bloques de una sola línea están bien para one-liners triviales (%%[ SET @x = 1 ]%%), pero cualquier cosa con ramificación, lookups, o múltiples SETs va vertical.
Indentación a 2 espacios, nunca tabs
Misma razón que SQL y SSJS: los tabs renderean distinto entre el editor de AMPscript, los diffs de GitHub, y nuestros docs. Dos espacios es la convención.
Formas inline — %%=v(@x)=%% para locales, %%[Col]%% para columnas de DE
No hay ambigüedad si te quedás con dos formas:
| Lo que querés renderear | Usá |
|---|---|
| Variable local seteada en un bloque AMPscript | %%=v(@x)=%% (inline) o OUTPUT(@x) (adentro de un bloque) |
| Columna del DE sendable | %%ColName%% (inline, sin @) o %%[ColName]%% (forma bracket, también inline) |
| Subscriber Attribute (perfil) | AttributeValue("AttrName") siempre; nunca la forma inline pelada |
Evitá %%@x%% para locales — no resuelve variables locales en muchos casos (ver gotchas — #1).
IF / ELSE / ENDIF indentados a 2 espacios por nivel
%%[
IF Empty(@tier) THEN
IF NOT Empty(@country) THEN
SET @tier = "RegionDefault"
ELSE
SET @tier = "Standard"
ENDIF
ENDIF
]%%ENDIF se alinea con el IF que lo abre. Lo mismo con NEXT alineando con FOR. Sin la alineación, la ramificación anidada profundamente se vuelve ilegible adentro de un bloque.
Funciones de write formateadas par-por-línea
InsertData / UpdateData / UpsertData toman pares de argumentos. El formato vertical hace visible la alineación al code review.
%%[
/* EVITAR — fácil de contar mal o desalinear */
InsertData("de_log_writes", "RunId", @runId, "Step", "process", "Message", "completed", "Ts", Now())
/* PREFERIR — par-por-línea; la estructura es la validación */
InsertData(
"de_log_writes",
"RunId", @runId,
"Step", "process",
"Message", "completed",
"Ts", Now()
)
]%%Ver funciones de Data Extension para el patrón completo.
Strings mágicos extraídos a constantes al tope
Nombres de DE, nombres de columna, valores de status, strings de locale — sacalos a locales nombradas al tope del script. Un rename se vuelve un cambio en vez de quince.
%%[
SET @DE_SUBSCRIBERS = "master_subscribers"
SET @STATUS_ACTIVE = "Active"
SET @DEFAULT_TIER = "Standard"
SET @tier = Lookup(@DE_SUBSCRIBERS, "Tier", "SubscriberKey", _subscriberKey)
IF Empty(@tier) THEN
SET @tier = @DEFAULT_TIER
ENDIF
]%%Capturá Now() una vez al tope del email
Now() se evalúa por-bloque por-destinatario. Para consistencia a lo largo de múltiples writes de log y timestamps rendereados dentro del mismo email, capturá una vez y reusá — misma disciplina RUN_ID-style que los scripts SSJS.
%%[
SET @renderedAt = Now()
/* Cada referencia subsecuente usa @renderedAt, no Now() */
]%%Comentarios
No comentes lo que el código hace — comentá el por qué
El código dice qué. Los comentarios agregan el contexto que el código no puede llevar.
%%[
/* INÚTIL — repite lo que el código ya dice */
/* Conseguir el tier de subscribers activos */
SET @tier = Lookup("master_segments", "Tier", "SubscriberKey", _subscriberKey)
/* ÚTIL — explica la decisión detrás del default */
/* Default al tier Standard cuando falta la fila del segment — pasa
durante las primeras 24h después del sign-up, antes de que el job
nocturno de enriquecimiento pueble de_log_segments. */
SET @tier = Lookup("master_segments", "Tier", "SubscriberKey", _subscriberKey)
IF Empty(@tier) THEN
SET @tier = "Standard"
ENDIF
]%%Comentá el recibo para elecciones no-obvias
Cuando cortás un atajo, comentalo con la fecha y la razón. El próximo dev (seguido vos, seis meses después) necesita el recibo.
%%[
/* TEMPORAL — reemplazar antes del 2026-06-15
(recordatorio en calendario seteado el 2026-05-19)
Razón: la columna "Tier" del DE de segments se está renombrando
a "TierName" en el PR #142; este script lee la columna vieja hasta
que aterrice la migración. */
SET @tier = Lookup("master_segments", "Tier", "SubscriberKey", _subscriberKey)
]%%Ver Principios de Marketing Cloud — #11 para la disciplina de código temporal.
Documentá la fuente de cada valor cross-context
Cuando leés de una columna de DE o un Subscriber Attribute, comentá la fuente para que la próxima persona no tenga que grepear por todo el proyecto para encontrar de dónde viene el valor.
%%[
/* Fuente: de_email_<send>_subscriber.Tier — populada por la SQL Activity
de audience-build que corre upstream */
SET @tier = AttributeValue("Tier")
/* Fuente: atributo del perfil All Subscribers, mantenido manualmente
por el equipo de customer success en Email Studio */
SET @lifecycleStage = AttributeValue("LifecycleStage")
]%%Patrones a preferir
Gate _messagecontext == "Send" alrededor de cada bloque con side-effects
Cada llamada InsertData, UpdateData, UpsertData, DeleteData, UpdateSingleSalesforceObject, CreateSalesforceObject, y ClaimRow va adentro de un solo gate perímetro. Ver funciones Cloud-write y funciones de Subscriber + Profile.
%%[
IF Lowercase(_messagecontext) == "send" THEN
/* Todos los side effects van acá */
ENDIF
]%%Empty() y un default en cada read de Lookup y AttributeValue
Las dos funciones devuelven NULL en silencio cuando nada matchea. Cada read se defaultea. Ver gotchas — #2 y funciones de validación.
HTMLEncode en cada render de input de usuario en CloudPage
Las CloudPages que leen RequestParameter o cualquier valor de fuente externa pasan ese valor por HTMLEncode antes de renderearlo a HTML. Ver funciones Encoding + Hashing.
Matcheá los valores de flag de Base64Encode y Base64Decode
Cuando entregás un Base64Encode con una flag, entregá el Base64Decode con la misma flag en el mismo cambio, con un comentario linkeándolos. Ver funciones Encoding + Hashing.
Pre-formateá data upstream en DEs de_email_*
LookupRows pesados, joins multi-DE, agregaciones, y lookups per-destinatario todos pertenecen a la SQL Activity de audience-build. AMPscript lee un DE fino, pre-formateado. Ver funciones de Data Extension y Style Guide de SQL.
Usá _subscriberKey no _emailaddress para joins
Los emails cambian; los subscriber keys están pensados para ser estables. Ver funciones de Subscriber + Profile.
Loggeá cada resultado de Cloud-write a de_log_sf_writes
Cada llamada UpdateSingleSalesforceObject / CreateSalesforceObject está seguida de un InsertData que registra el JobID, SubscriberKey, objeto SF, operación, y @result. Auditá después de cada Send. Ver funciones Cloud-write.
Concat para armar strings — nunca +
AMPscript no tiene operador + para strings. Ver funciones de string.
Substring e IndexOf son 1-based — nunca 0 como start
%%[ SET @firstChar = Substring(@firstName, 1, 1) ]%% /* NO 0, 1 */Ver funciones de string y gotchas — #7.
Gate de IsNumeric antes de cualquier math sobre input de string
Las funciones de math coercen strings no-numéricas a 0 en silencio. Validá primero. Ver funciones de math y funciones de validación.
Verificá la primary key del DE destino antes de que cualquier UpsertData llegue a producción
UpsertData devuelve 1 tanto para inserts como updates. El PK equivocado duplica filas en silencio. Ver funciones de Data Extension y gotchas — #4.
Cota arriba en cada loop FOR que se espera que itere sobre un resultado de LookupRows
LookupRows capea en 2000. Si tu loop asume más, ya está mal. Agregá una aserción explícita de que RowCount está por debajo de 2000, o pre-formateá upstream. Ver gotchas — #3.
Patrones a rechazar
UpsertData contra un DE cuyo primary key no verificaste
Duplicados silenciosos. El patrón operacional más caro solo. Ver funciones de Data Extension y gotchas — #4.
Código con side-effects fuera de un gate _messagecontext == "Send"
Cada render de preview escribe a producción. El code review rechaza cualquier write a DE o Cloud-write que no esté adentro del gate. Ver funciones Cloud-write.
Lookup o AttributeValue sin defaulting de Empty()
NULLs en render time dejan blancos en el cuerpo del email. Nunca confíes en el resultado de cualquiera de las dos funciones sin un default.
LookupRows como iterador de "todas las filas"
Capea silenciosamente en 2000. O afirmás explícitamente el cap y alertás cuando se dispara, o pivoteás a una SQL Activity. Ver gotchas — #3.
TreatAsContent sobre input de usuario
Template injection. Whitelist-only. Ver funciones Encoding + Hashing.
HTMLEncode salteado en cualquier render de input de usuario en CloudPage
Exposición a XSS. Cada RequestParameter y cada valor de fuente externa pasa por HTMLEncode. Ver funciones Encoding + Hashing.
MD5 como firma
No es una firma. Usá HMACSHA256(payload, key) si está disponible, si no hacé la crypto afuera de MC. Ver funciones Encoding + Hashing.
Substring(@s, 0, ...) (asunción 0-based de JS)
Devuelve resultado equivocado / vacío en silencio. AMPscript es 1-based. Ver funciones de string y gotchas — #7.
Math sobre input de string sin validación de IsNumeric
Coerción silenciosa de input malo a 0. Output de math equivocado, sin warning. Ver funciones de math.
%%@x%% para variables locales
No resuelve en muchos contextos. Usá %%=v(@x)=%% en su lugar. Ver gotchas — #1.
Replace asumiendo "solo el primer match" (hábito de JS)
AMPscript reemplaza todas las ocurrencias por default. Pasá el 4to arg para capear. Ver funciones de string.
== contra tipos mezclados sin Lowercase / coerción explícita
Las comparaciones de tipos sueltos a veces matchean valores que no pretendías. Ver gotchas — #6.
AMPscript con más de ~30 líneas en un solo email
El trabajo va upstream en SQL. AMPscript debería ser fino: leer columnas, interpolar, loggear. Ver funciones de Data Extension.
Confiar el preview de personalización como QA final
Preview ≠ send. Test send real a un subscriber real en una Business Unit de staging es la única verificación verdadera. Ver gotchas — #9.
El check de disciplina antes de mergear
Antes de que cualquier bloque AMPscript nuevo pase de borrador a un email publicado o una CloudPage activada, recorré este checklist:
- [ ] El naming de variables locales sigue la convención del proyecto (una de
@camelCase/@PascalCase/@snake_case, nunca mezcladas) - [ ] Cada variable lleva un nombre significativo, no una sola letra (excepto
@ipara índices de loop) - [ ] Los nombres de DE / columna / Subscriber Attribute son lo suficientemente distintos como para que no haya ambigüedad sobre qué fuente se está leyendo
- [ ] Los strings mágicos (nombres de DE, valores de status, locales) están en constantes al tope
- [ ]
Now()capturado una vez al tope y reusado; estiloRUN_ID/@renderedAt - [ ] El gate
_messagecontext == "Send"envuelve el bloque entero con side-effects; sin writes afuera - [ ] Cada
LookupyAttributeValuetiene un default deEmpty() - [ ] El primary key de cada DE destino de
UpsertDataestá verificado - [ ] Cada resultado de
LookupRowsestá acotado (esperás menos de 2000 y lo afirmás, o pre-formateaste upstream) - [ ] Cada
UpdateSingleSalesforceObject/CreateSalesforceObjectestá seguido de una fila de logInsertDataade_log_sf_writes - [ ] Cada valor
RequestParameterde CloudPage pasa porHTMLEncodeantes de renderearse a HTML - [ ] Las flags de
Base64EncodeyBase64Decodematchean dentro del mismo cambio, con un comentario linkeándolos - [ ] El math sobre input de string está precedido por
IsNumeric(o el input está pre-validado upstream) - [ ]
SubstringeIndexOfusan posiciones 1-based - [ ]
Concatusado para armar strings; sin intentos de operador+ - [ ] Comparaciones equivalentes-a-
===forzadas víaLowercase()/ coerción explícita cuando el tipo importa - [ ] Sin
TreatAsContentsobre input controlado por el usuario - [ ] Sin
MD5usado como firma;HMACSHA256o crypto-fuera-de-MC cuando la tamper-resistance importa - [ ] AMPscript por email está bajo ~30 líneas; lógica más pesada movida a SQL Activity upstream
- [ ] Verificado vía test send real a una Business Unit de staging, no solo preview
- [ ] Los comentarios explican el por qué de cualquier elección no-obvia
- [ ] Cualquier código "temporal" tiene fecha de expiración y recordatorio en calendario
Cuando los veintidós se disparan, el bloque está listo para entregar.
Relacionado
- Principios de Marketing Cloud desde producción — las meta-reglas arriba de los específicos de AMPscript
- Gotchas de MC AMPscript — las formas de falla que esta Style Guide está diseñada para prevenir
- Style Guide de MC SQL — la página de disciplina hermana para SQL Activities
- Style Guide de MC SSJS — la página de disciplina hermana para scripts SSJS
- Cada página de referencia de este catálogo — Basics · String · Date · Math · Validación · Data Extension · Subscriber + Profile · Cloud-write · Encoding + Hashing
Progreso del catálogo: con esta Style Guide, las 9 páginas de referencia + decision-framework de la sección AMPscript están entregadas, junto con la production-note de gotchas. El trabajo restante del catálogo son 3 snippets how-to de debugging.
Si encontrás una regla que falte — o una de estas reglas siendo violada en nuestro trabajo público — escribinos a hello@wearecleon.com. La agregamos, o la corregimos y lo decimos.