Skip to main content

Gotchas de MC AMPscript: lo que sobrevive un hand-off

AMPscript parece un lenguaje de template simple. La realidad a escala son tres sintaxis distintas para variables, tres chequeos de NULL que significan cosas distintas, una API de lookup que falla en silencio, y un preview que no renderea el mismo code path que el envío. Diez gotchas anclados a la próxima persona heredando tu email.

Nota de producción·Actualizado 2026-05-13·Escrito por Lira · Editado por German Medina

AMPscript parece un lenguaje de template simple — %%=v(@firstName)=%% interpola un valor, %%[ ]%% corre lógica, listo. La realidad a escala es un lenguaje con tres formas distintas de escribir una variable, una superficie de NULL que tiene tres funciones distintas para "nada acá", una API de lookup que falla en silencio, y un preview de personalización que usa un render path distinto al del envío real.

El framing de hand-off no es decoración. Los diez ítems abajo son los que la próxima persona heredando un email AMPscript — o la misma persona seis meses después — necesita saber antes de cambiar nada, porque ninguno se manifiesta como error hasta que producción se prende a las 11pm y una Send Activity reporta Completed mientras la mitad de los destinatarios recibió un saludo en blanco.


Los gotchas

1. Tres sintaxis de variable, y la que silenciosamente no interpola

Hay varias formas inline:

  • %%=v(@firstName)=%% — resuelve una variable local seteada en un bloque AMPscript. La forma durable.
  • %%firstName%% — resuelve un atributo del DataExtension del DE sendable (sin @).
  • %%@firstName%% — también intenta resolver desde el DE sendable; no resuelve variables locales que seteaste en bloques %%[ ]%%.

La falla del hand-off: alguien lee %%@firstName%% y asume que está leyendo la var local definida tres bloques arriba. No — está leyendo la columna del DE llamada firstName, y si esa columna no existe en el DE sendable, renderea en blanco.

%%[
  SET @firstName = "Mariana"
]%%

<!-- BLANCO — @firstName no es un atributo de DE -->
Hola, %%@firstName%%

<!-- "Mariana" — v() resuelve la variable local -->
Hola, %%=v(@firstName)=%%

Elegí una forma por proyecto. La convención de Cleon: %%=v(@x)=%% para variables locales, %%[Column]%% (con sintaxis de brackets adentro de un bloque AMPscript) para columnas de DE. No mezcles.

2. Lookup() devuelve NULL cuando nada matchea — sin error

Un Lookup() contra un DE devuelve el valor de la columna cuando el filtro matchea una fila, o NULL cuando nada matchea. La función nunca tira. Un cuerpo de email que interpola el resultado renderea en blanco donde el valor debía ir.

%%[
  SET @segment = Lookup("master_segments", "Tier", "SubscriberKey", _subscriberKey)
]%%

<!-- Si ninguna fila matchea, @segment es NULL — el email renderea
     un blanco donde esperabas "Gold" o "Silver" -->
Perteneces al tier %%=v(@segment)=%%.

El patrón defensivo es Empty() y un default, siempre:

%%[
  SET @segment = Lookup("master_segments", "Tier", "SubscriberKey", _subscriberKey)
  IF Empty(@segment) THEN
    SET @segment = "Standard"
  ENDIF
]%%

Mismo fingerprint que la forma de falla de Platform.Function.LookupRows sin-error-en-vacío de SSJS — ver Gotchas de MC SSJS — #5.

3. LookupRows() está silenciosamente capeado en 2000 filas

Como Platform.Function.LookupRows en 2500 en SSJS, el LookupRows() de AMPscript capea en 2000. Lógica de email que itera sobre el resultado pierde a todos los que pasan la fila 2000 y el email sale con la lista truncada, sin warning.

%%[
  SET @rows = LookupRows("active_promotions", "Region", "LATAM")
  SET @n = RowCount(@rows)
  
  /* @n es como máximo 2000 — si hay 5000 promos LATAM en el DE,
     mandaste las primeras 2000 a todos e ignoraste el resto */
]%%

La defensa no está adentro de AMPscript: está upstream. Hacé que una SQL Activity escriba un de_email_<proposito> DE ya capeado y formateado para las necesidades del email (TOP 100 con el orden correcto), y que AMPscript lea ese DE pre-formateado. No le pidas a AMPscript que acote por vos.

4. AttributeValue() lee Subscriber Attributes — no columnas de DE ni variables locales

Las tres fuentes "de dónde vienen los atributos" son fáciles de confundir:

  • Columnas de DE del DE sendable: %%[Column]%% adentro de un bloque AMPscript, o %%Column%% inline.
  • Variables locales de AMPscript: %%=v(@var)=%% inline, @var adentro de un bloque.
  • Subscriber Attributes definidos en el perfil All Subscribers de Email Studio: AttributeValue("AttrName").

Estas tres a veces se superponen — una columna llamada FirstName en el DE sendable, un Subscriber Attribute también llamado FirstName, una @FirstName local. Cuál gana en %%FirstName%% depende del contexto y es una de las fuentes de bug silencioso más longevas en MC.

%%[
  /* Esto lee desde la definición del **Subscriber Attribute**
     en Email Studio. NO la columna del DE sendable con el
     mismo nombre. */
  SET @profileRegion = AttributeValue("Region")
]%%

La regla de hand-off: nombrá las tres distinto. [DERegion] para la columna, @LocalRegion para la local, AttributeValue("ProfileRegion") para el Subscriber Attribute. El naming previene la confusión.

5. Los bloques AMPscript se evalúan en orden de documento — pero la herencia de content blocks rompe el modelo

En un solo bloque %%[ ]%%, las sentencias corren de arriba a abajo. Entre bloques adentro de un área de contenido, también de arriba a abajo. Entre content blocks en un email basado en template, el orden no es siempre lo que muestra el WYSIWYG. Los content blocks pueden renderear en un orden distinto al que aparecen visualmente por las reglas de herencia de regiones del template y el orden en que el renderer de MC camina el árbol del template.

La falla del hand-off: alguien setea @discount en el content block A (visualmente primero), el block B lee @discount y lo renderea (visualmente segundo). Meses después un cambio de template mueve al block B adentro de una región padre distinta y ahora se renderea antes que el block A. @discount es NULL, el email renderea en blanco, nadie nota por una semana.

%%[
  /* DURABLE — seteá cada variable al tope de cada bloque donde
     se lee, con un Lookup o default. No te apoyes en la
     propagación de variables entre bloques. */
  SET @discount = Lookup("de_promotions", "Amount", "SubscriberKey", _subscriberKey)
  IF Empty(@discount) THEN
    SET @discount = 0
  ENDIF
]%%

Tratá cada content block como su propio scope. Re-fetcheá lo que necesités.

6. Las comparaciones son de tipos sueltos y case-insensitive por default

%%[
  IF @status == "Active" THEN
    /* matchea "Active", "ACTIVE", "active", y " Active " (trimmed) */
  ENDIF
]%%

Para la mayoría de la personalización esto está bien. Las trampas:

  • Un espacio al final en el data ("Active ") igual matchea "Active" — == trimea y case-foldea antes de comparar. Si tu lógica downstream espera el literal "Active" sin whitespace, la comparación pasa y el próximo paso falla.
  • Comparar un string de fecha contra un valor de fecha — conversión implícita, puede matchear en formas sorprendentes.

Cuando el tipo importa, anclá explícitamente: forzá lowercase con Lowercase(@status) antes de comparar, o parseá ambos lados con DateParse() para fechas. Lo explícito le gana a la conversión implícita cada vez.

7. Substring() e IndexOf() son 1-based, no 0-based

Substring("Hello", 1, 4) devuelve "Hell", no "ello". Cualquiera que viene de JavaScript, Python, o Go agarra Substring(@s, 0, n) y obtiene el resultado equivocado en silencio.

%%[ 
  /* Tomar el primer carácter del primer nombre */
  SET @firstChar = Substring(@firstName, 1, 1)  /* NO 0, 1 */
]%%

Lo mismo con IndexOf() — devuelve posición 1-based, o 0 si no encuentra (0 significa "no encontrado", 1 significa "encontrado en el primer carácter"). El bug del primer-nombre-trimeado ("ariana" en vez de "Mariana") es el artefacto AMPscript más común en envíos de producción.

8. Empty() vs IsNull() vs == "" — tres conceptos para "nada acá"

Las tres funciones se comportan distinto:

  • Empty(@x) — true para NULL, string vacío, o variable no seteada. El chequeo más amplio.
  • IsNull(@x) — true solo para NULL.
  • @x == "" — true solo para string vacío después de la comparación de tipos sueltos de AMPscript.
%%[
  SET @firstName = AttributeValue("FirstName")
  
  /* DURABLE — cubre NULL, "", y "no seteado" en un solo chequeo */
  IF Empty(@firstName) THEN
    SET @firstName = "Friend"
  ENDIF
]%%

Usá Empty() por default. Reservá IsNull() solo para cuando específicamente necesités distinguir NULL de string vacío (raro en contexto de AMPscript). Evitá == "" — no cubre el caso de NULL y la próxima persona heredando el código va a pensar que sí.

9. El preview de personalización usa un code path distinto al del envío real

El preview de Email Studio:

  • Lee _subscriberKey del target de test send elegido.
  • Lee columnas de DE de la fila del DE sendable del subscriber de test.
  • Lee AttributeValue() de los valores de Subscriber Attribute del subscriber de test.
  • No corre el link wrapping de envío, la instrumentación completa de tracking, ni algunas funciones que dependen del JobID real.

Un preview que renderea correctamente es necesario, pero no suficiente. El envío falla cuando:

  • Un Lookup() contra un DE en el que el subscriber de test del preview no existe devuelve NULL en producción para ese subscriber.
  • AMPscript que branchea sobre _jobid hace algo distinto en preview.
  • Una función AMPscript que depende del data de perfil real del subscriber rendereado obtiene un valor distinto en send-time.

Defensa: mandá un test send real a un subscriberKey real (una audiencia de staging en una Business Unit de staging) antes de mandar a producción. El test send corre el mismo renderer que el envío de producción. El preview es para iteración rápida; el test send es para confianza.

10. UpdateSingleSalesforceObject y funciones de Cloud-write pueden fallar en silencio

Funciones AMPscript que escriben de vuelta a Sales / Service Cloud (UpdateSingleSalesforceObject, CreateSalesforceObject, etc.) devuelven un valor de status — pero no tiran en falla. Un ID equivocado, un problema de permisos, un registro lockeado — el cuerpo del email renderea bien, el objeto de cloud no se actualiza, y la falla se manifiesta una semana después cuando ventas pregunta por qué el status del lead está mal.

%%[
  SET @result = UpdateSingleSalesforceObject(
    "Lead", @leadId,
    "Status", "Engaged"
  )
  
  /* Loggear cada llamada a un DE — @result es 1 en éxito, 0 en falla */
  InsertData(
    "de_log_ampscript_writes",
    "JobID", jobid,
    "SubscriberKey", _subscriberKey,
    "Operation", "Lead.Status=Engaged",
    "Result", @result,
    "Ts", Now()
  )
]%%

@result = 0 significa que el write no pasó. Auditá de_log_ampscript_writes después de cada Send. El patrón es el mismo que la regla de SSJS de loggear adentro de los catch — la falla silenciosa solo es aceptable después de que la falla quedó registrada en algún lado.


Cierre

Estos diez son las formas de falla que se comieron la mayor cantidad de horas de producción cuando Cleon estuvo heredando AMPscript de otros equipos o haciendo crecer programas de email más allá de sus primeros envíos. El patrón en todos es el mismo que en SQL y SSJS: AMPscript reporta success buena parte del tiempo cuando lo que pretendías no pasó, y la única defensa es instrumentación + defaults explícitos + tratar al preview como necesario-pero-no-suficiente.

Si encontrás un gotcha que mordió a tu equipo y no está acá — escribinos a hello@wearecleon.com. Lo agregamos, con crédito.

Referencia: