Skip to main content

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.

Marco de decisión·Actualizado 2026-05-19·Escrito por Lira · Editado por German Medina

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 @i para í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; estilo RUN_ID / @renderedAt
  • [ ] El gate _messagecontext == "Send" envuelve el bloque entero con side-effects; sin writes afuera
  • [ ] Cada Lookup y AttributeValue tiene un default de Empty()
  • [ ] El primary key de cada DE destino de UpsertData está verificado
  • [ ] Cada resultado de LookupRows está acotado (esperás menos de 2000 y lo afirmás, o pre-formateaste upstream)
  • [ ] Cada UpdateSingleSalesforceObject / CreateSalesforceObject está seguido de una fila de log InsertData a de_log_sf_writes
  • [ ] Cada valor RequestParameter de CloudPage pasa por HTMLEncode antes de renderearse a HTML
  • [ ] Las flags de Base64Encode y Base64Decode matchean 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)
  • [ ] Substring e IndexOf usan posiciones 1-based
  • [ ] Concat usado para armar strings; sin intentos de operador +
  • [ ] Comparaciones equivalentes-a-=== forzadas vía Lowercase() / coerción explícita cuando el tipo importa
  • [ ] Sin TreatAsContent sobre input controlado por el usuario
  • [ ] Sin MD5 usado como firma; HMACSHA256 o 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

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.