Skip to main content

SSJS de Marketing Cloud: Style Guide

Las reglas opinionadas que Cleon aplica a cada bloque de Server-Side JavaScript 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.

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

Esta es la página donde Cleon deja de describir lo que MC SSJS es y empieza a decir lo que hacemos con él. Salesforce define lo que funciona. Las páginas de referencia documentan la superficie de Platform.*. Los gotchas documentan lo que rompe a escala. Esta Style Guide es la disciplina que mantiene una CloudPage o una Script Activity mantenible un año después de que la entregamos.

Usala como checklist antes de mergear cualquier bloque SSJS 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

Code Resources, CloudPages, Script Activities siguen una convención de prefijo

Decidida una vez, antes de crear el primer asset. Renombrarlos después significa cazarlos por Automations, Journeys, y referencias en URLs.

| Prefijo | Contiene | |---|---| | CR_ | Code Resource (bloque SSJS reusable, incluido vía Platform.Load) | | CP_ | CloudPage (página renderizada pública o gated) | | SA_ | Script Activity (paso de Automation) | | Auto_ | Automation que contiene Script Activities | | J_ | Journey que llama a SSJS | | de_log_ssjs_ | DE de log de corrida — append-only, indexada por RunId + Ts | | de_log_errors_ | DE de log de errores — escrita desde bloques catch | | de_lookup_ssjs_ | DE de config que los scripts leen en runtime (Active, Mode, etc.) |

El patrón: el prefijo comunica qué tipo de asset, el resto del nombre comunica propósito. SA_NightlyEnrichment se lee de una pasada.

RunId y Step son instrumentación first-class, no algo de último momento

Cada SSJS de producción abre con:

Platform.Load("Core", "1.1.5");
var RUN_ID = Platform.Function.GUID();

function log(step, message) {
  Platform.Function.UpsertData(
    "de_log_ssjs_runs",
    ["RunId","Step","Ts"], [RUN_ID, step, Now()],
    ["Message"], [message]
  );
}

log("init", "started");

El RunId ata cada fila de cada log a una única ejecución. El label Step te dice dónde estaba el script. Sin esas dos columnas, tu DE de log es ruido. Ver gotchas — #6.

Los nombres de variable 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 (var i = 0; i < r.length; i++) {
  var k = r[i].k;
  // ...
}

// PREFERIR
for (var i = 0; i < rows.length; i++) {
  var subscriberKey = rows[i].SubscriberKey;
  // ...
}

i está bien como índice numérico. r y k no, ni con un comentario al lado.

Formato

Platform.Load("Core", "1.1.5") es la línea 1, siempre

Sin él el namespace Platform.* no existe. El error que recibís no dice nada de la load call faltante — vas a perder una hora mirando el code path equivocado. Ver gotchas — #2 y Basics.

<script runat="server">
Platform.Load("Core", "1.1.5");
// todo lo demás
</script>

Pineá la versión (1.1.5). Platform.Load("Core") pelado funciona pero se ata a los defaults del tenant, que se movieron entre ediciones.

Indentación a 2 espacios, nunca tabs

Misma razón que SQL: los tabs renderean distinto entre el editor de UI de SSJS, los diffs de GitHub, y nuestros docs. Dos espacios es la convención.

Punto y coma obligatorio, una sentencia por línea

La automatic semicolon insertion de SpiderMonkey 1.7 es poco confiable cuando los scripts se concatenan vía Platform.Load en un solo bloque. Siempre terminá las sentencias. Siempre una sentencia por línea.

// EN RIESGO — ASI puede mergear entre líneas
var a = lookup()
var b = lookup()

// DURABLE
var a = lookup();
var b = lookup();

Las declaraciones var agrupadas al tope de su scope

SpiderMonkey 1.7 hoistea var al tope de la función o bloque de script que la contiene; ponerlas arriba matchea el modelo de runtime y previene confusiones de orden de lectura accidentales.

function processBatch(batch) {
  var rows = [];
  var errors = 0;
  var i, row, result;

  for (i = 0; i < batch.length; i++) {
    row = batch[i];
    // ...
  }
}

Strings mágicos extraídos a constantes al tope del script

Nombres de DE, nombres de columna, valores de status — sacalos para que un rename sea un cambio en vez de quince.

// EVITAR
var rows = Platform.Function.LookupRows("master_subscribers", "Status", "Active");
Platform.Function.UpsertData("master_subscribers",
  ["SubscriberKey"], [k], ["Status"], ["Inactive"]);

// PREFERIR
var DE_SUBSCRIBERS  = "master_subscribers";
var STATUS_ACTIVE   = "Active";
var STATUS_INACTIVE = "Inactive";

var rows = Platform.Function.LookupRows(DE_SUBSCRIBERS, "Status", STATUS_ACTIVE);
Platform.Function.UpsertData(DE_SUBSCRIBERS,
  ["SubscriberKey"], [k], ["Status"], [STATUS_INACTIVE]);

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 subscribers activos
var rows = Platform.Function.LookupRows("master_subs", "Status", "Active");

// ÚTIL — explica la decisión detrás del cap
// Capeado en 2500 por diseño; esperamos <500 cuentas activas en este
// paso del Journey. Si esto alguna vez devuelve 2500, alertá y pivoteá
// a SQL — ver SSJS gotchas #5.
var rows = Platform.Function.LookupRows("master_subs", "Status", "Active");
if (rows.length === 2500) { log("limit-hit", "LookupRows truncated"); }

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-15)
// Razón: WSProxy retrieve falla el SSL handshake para nuestro SOAP
// namespace hasta que aterrice el upgrade de MC Connect; LookupRows
// es el workaround mientras esperamos.
var rows = Platform.Function.LookupRows(DE_SUBSCRIBERS, "Status", STATUS_ACTIVE);

Ver Principios de Marketing Cloud — #11 para la disciplina de código temporal.

Patrones a preferir

Platform.Load("Core", "1.1.5") y después todo lo demás

Sin él nada de Platform.* resuelve. Ver Basics y Platform.Function.

Un RunId por ejecución, loggeado en cada paso significativo

Generá el GUID una vez al inicio del script, propagalo por cada write a de_log_ssjs_runs. El log queda a una query de un timeline de ejecución completo. Ver gotchas — #6.

try { ... } catch (e) { log(e) } — nunca catch pelado

Un catch pelado se traga el error y la Activity reporta success. Capturá y escribí e.message a de_log_errors_* antes de decidir si re-lanzar o continuar. Ver gotchas — #7.

Re-instanciá WSProxy cada 15 minutos en scripts largos

Los tokens de auth de WSProxy expiran a los ~20 minutos. Reseteá el prox cada 15 para mantenerte lejos del borde. Ver WSProxy y gotchas — #8.

Platform.Function.ParseJSON sobre JSON.parse

Los tenants SFMC más viejos no exponen JSON.parse / JSON.stringify nativo en SSJS. Platform.Function.ParseJSON funciona en todos lados; JSON.parse puede silenciosamente ser undefined. Ver Platform.Function.

Listas de columnas explícitas en cada UpsertData / InsertData / UpdateData

Mismo principio que SELECT * en SQL: listas implícitas o copy-pasteadas dejan que un cambio de schema en el destino re-forme tus writes en silencio. Siempre pasá la lista completa, leída de una constante. Ver Platform.Function y SQL Style Guide.

Stagéa, validá, después promové

Para cualquier transformación no trivial, partila en:

  1. Leer origen → escribir a de_stg_*
  2. Paso de verificación: conteo de filas, check de sanidad, alerta si está fuera del rango esperado
  3. Promover de_stg_* → DE de producción

Tres pasos, tres checkpoints, recuperable. Misma forma que el Style Guide de SQL.

Usá una SQL Query Activity, no LookupRows, para lecturas en bulk

LookupRows capea en 2500. Cualquier cosa que necesite más filas es un loop de retrieve con WSProxy o — usualmente mejor — una SQL Query Activity hacia un DE de staging que el script después lee con LookupRows acotado. Ver gotchas — #5.

El trabajo paralelo va entre Script Activities, no adentro de una

SSJS es single-threaded synchronous. No existe Promise.all. Si necesitás concurrencia, abrí fan-out en la capa de Automation Studio: múltiples Script Activities scheduleadas en paralelo, cada una manejando una porción. Ver gotchas — #10.

Loops acotados arriba en cada callout paginado

Cualquier while (hasMore) { ... } contra una API externa lleva un && iterations < N duro. El timeout de 60 segundos por llamada combinado con el timeout de 30 minutos de Activity hace que un loop fuera de control se coma todo el presupuesto. Ver gotchas — #9.

Instrumentá el runtime en cada Script Activity larga

Capturá Now() al tope y en cada milestone; escribí los deltas a de_log_ssjs_runs. Cuando eventualmente pegues el timeout de 30 minutos, vas a saber qué paso creció. Ver gotchas — #3.

Patrones a rechazar

let, const, arrow functions, template literals, destructuring, spread

SpiderMonkey 1.7 no parsea nada de eso. El autocomplete de tu editor te miente sobre lo que corre. Usá var, function, concatenación de strings tradicional. Ver Basics y gotchas — #1.

try { ... } catch (e) {} pelado

Falla silenciosa que reporta como success. Siempre loggeá adentro del catch — escribí a de_log_errors_* con RunId, Step, e.message, Ts. Ver gotchas — #7.

LookupRows como iterador de "todas las filas"

Capea silenciosamente en 2500. O afirmás explícitamente el cap y alertás cuando se dispara, o pivoteás a una SQL Query Activity / WSProxy. Ver gotchas — #5.

console.log

No hay consola. Usá Write() para CloudPages o writes a DE de log para Script Activities. Ver gotchas — #6.

Saltarte Platform.Load

El namespace Platform.* entero queda undefined sin él. Cada bloque — hasta los snippets cortos — arranca con la load call. Ver gotchas — #2.

Promise, async, await, setTimeout

Ninguno existe en SpiderMonkey 1.7. Código que los usa es un SyntaxError (o un undefined silencioso si el parser es indulgente). Para concurrencia, partí el trabajo entre Script Activities orquestadas por Automation Studio. Ver gotchas — #10.

Loops while / for sin cota contra APIs externas

while (hasMore) sin && iterations < N es un fuera-de-control esperando para pasar. Siempre capealo. Ver gotchas — #9.

UpsertData contra un DE cuyo primary key no verificaste

La función inserta duplicados en silencio en vez de actualizar cuando el PK del destino está mal o falta. Verificá el PK del destino antes de que el upsert llegue a producción. Ver gotchas — #4.

JSON.parse sobre input que no controlás sin try/catch

Un payload mal-formado tira y — si no se captura — falla el paso de la Activity en silencio. Envolvelo, loggeá la falla, caé a un default seguro. Preferí Platform.Function.ParseJSON igual (seguridad para tenants legacy). Ver Platform.Function.

== / != contra tipos mezclados

SpiderMonkey 1.7 tiene la superficie completa de type-coercion de ES5. "0" == 0 es true, "" == 0 es true, null == undefined es true. Siempre usá === / !== salvo que específicamente quieras coerción (raro y digno de un comentario).

Mezclar AMPscript y SSJS sin comentar la frontera

En CloudPages y bloques de email, las variables de AMPscript y las de SSJS pueden pasarse de un lado al otro con Variable.GetValue / Variable.SetValue. La frontera es invisible en el diff renderizado. Siempre comentá cuando la cruzás. Ver Util / Variable / Request.

El check de disciplina antes de mergear

Antes de que cualquier bloque SSJS nuevo pase de preview a una CloudPage publicada o una Automation activada, recorré este checklist:

  • [ ] El nombre del asset sigue la convención de prefijo (CR_ / CP_ / SA_ / Auto_ / J_ / de_log_ssjs_ / de_log_errors_ / de_lookup_ssjs_)
  • [ ] Platform.Load("Core", "1.1.5") está en la línea 1
  • [ ] RUN_ID generado una vez vía Platform.Function.GUID() al tope
  • [ ] Cada paso significativo escribe a de_log_ssjs_runs con RunId, Step, Ts, Message
  • [ ] Todas las declaraciones var agrupadas al tope de su scope
  • [ ] Todos los nombres de DE, columnas, y valores de status están en constantes
  • [ ] Sin let / const / arrow / template literal / destructuring (SpiderMonkey 1.7 solo parsea ES5)
  • [ ] Sin console.log, sin Promise, sin setTimeout
  • [ ] Cada try tiene un catch que escribe a de_log_errors_* antes de decidir qué hacer
  • [ ] Cada UpsertData / InsertData / UpdateData tiene listas de columnas explícitas
  • [ ] El primary key del DE de destino verificado antes de que cualquier UpsertData llegue a producción
  • [ ] Las llamadas LookupRows están acotadas (esperás menos de 2500 y lo afirmás, o ya pivoteaste a SQL/WSProxy)
  • [ ] WSProxy re-instanciado cada 15 minutos en cualquier loop que se espere que corra más de 10 minutos
  • [ ] Todas las llamadas HTTP externas con cota superior por conteo de iteraciones
  • [ ] Runtime instrumentado en cualquier Script Activity con lecturas en bulk o callouts externos
  • [ ] === / !== usados salvo que la coerción de tipos sea explícitamente intencional y comentada
  • [ ] 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 dieciocho se disparan, el script 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 SSJS están entregadas, junto con la production-note de gotchas. El trabajo restante del catálogo son 3 snippets how-to de debugging (Script Activities trabadas, problemas de auth en WSProxy, duplicados silenciosos de UpsertData).

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.