Skip to main content

Debugging de Script Activities trabadas

Cuando una Script Activity corrió pasado su presupuesto, terminó sin hacer lo que debía, o reportó 'Completed' mientras saltó silenciosamente la mitad del trabajo, el diagnóstico es el mismo. Seis queries contra los DE de log que el script dejó atrás, que encuentran dónde murió.

Cómo hacerlo·Actualizado 2026-05-13·Escrito por Lira · Editado por German Medina

Algo se torció. Una Script Activity en una Automation nocturna debía escribir 12.000 filas a un DE de log y solo escribió 3.400. O corrió 31 minutos y el paso de la Automation muestra Failed sin un mensaje de error útil. O — el peor — el paso muestra Completed pero nada río abajo recibió el data que esperaba. El flujo de diagnóstico es el mismo cada vez: leer el DE de log que el script tenía que escribir, encontrar la última fila que logró emitir, y leer hacia atrás desde ahí.

Esta página es el playbook de seis queries. Si instrumentaste el script siguiendo el Style GuideRunId + Step + Ts en cada paso significativo, errores capturados a de_log_ssjs_errors — cada query abajo resuelve en segundos. Si no lo hiciste, esta también es la página que te convence de empezar.

El flujo de diagnóstico

[ Activity scheduleada ]
        ↓ runtime arranca
[ Platform.Load + RUN_ID generado ]
        ↓ fila init escrita a de_log_ssjs_runs
[ Step 1 ... Step N ]
        ↓ cada paso escribe su propia fila
[ Completion / Timeout / Error capturado ]

Cada flecha es un lugar donde el script puede morir sin dejar un error en el log de la Activity. Las queries abajo chequean cada nivel leyendo las filas que el script escribió, y después infiriendo lo que falta.

Paso 1 — ¿El script siquiera arrancó?

Si el script nunca escribió su fila init, la falla es antes de que cualquier código tuyo corriera. Platform.Load falló, un SyntaxError de SpiderMonkey 1.7 tiró el bloque entero, o la Activity ni siquiera se disparó.

-- Reemplazá el rango de fechas con la ventana de corrida esperada de la Activity
SELECT
  COUNT(*)                AS InitRows,
  MIN(Ts)                 AS FirstWriteAt,
  COUNT(DISTINCT RunId)   AS DistinctRuns
FROM de_log_ssjs_runs
WHERE Step = 'init'
  AND Ts BETWEEN '2026-05-13 02:00' AND '2026-05-13 03:00';

Tres números, una pregunta:

  • InitRows = 0: el cuerpo del script nunca ejecutó. Chequeá el Run History de la Activity en Automation Studio para ver un mensaje de error (Platform is not defined, SyntaxError, etc.). Si el Run History tampoco muestra nada, la Automation puede no haberse disparado — verificá el schedule y el status del paso upstream.
  • InitRows = 1 pero ninguna otra fila para ese RunId: el script llegó a la línea 1 pero murió antes del primer paso significativo. Usualmente una dependencia faltante (un Code Resource que no cargó, un nombre de DE que no existe).
  • DistinctRuns > 1 en una ventana que debería tener una sola: la Activity reintentó. Leé el Run History para ver por qué.

Paso 2 — ¿Dónde murió?

Si la fila init está, encontrá el último paso que el script logró escribir. Ese es el borde — el bug está en lo que sea que corre después.

-- Reemplazá 'a1b2c3d4-...' con el RUN_ID de la corrida fallada
SELECT
  Step,
  Ts,
  Message
FROM de_log_ssjs_runs
WHERE RunId = 'a1b2c3d4-e5f6-...';
-- Ordenalo alfabéticamente por Step o cronológicamente por Ts según
-- cómo nombraste los pasos. La convención de Cleon prefijar cada
-- paso con un orden de 2 dígitos (01-init, 02-lookup, 03-enrich, ...)
-- hace que alfabético y cronológico coincidan.

El último valor de Step en el resultado es donde el script murió. El próximo pedazo de código en el script — entre ese paso y la próxima llamada a log() — es donde vive el bug.

Una forma común: el script loggea 03-enrich-start pero nunca 03-enrich-done. El bug está adentro del bloque de enriquecimiento — usualmente un cap de LookupRows, un error de auth de WSProxy, o un timeout HTTP contra una API externa. Los pasos 3-5 chequean cada uno.

Paso 3 — ¿Capturó algún error?

Si el script siguió la regla del Style Guide de loggear adentro de los bloques catch, el error está sentado en de_log_ssjs_errors con el mismo RunId.

SELECT
  Step,
  Ts,
  Msg
FROM de_log_ssjs_errors
WHERE RunId = 'a1b2c3d4-e5f6-...';

Qué te dice el mensaje:

  • Token has expired / Unauthorized — timeout de auth de WSProxy. El fix está en WSProxy — re-instanciá el prox cada 15 minutos adentro de loops largos. Ver gotchas — #8.
  • The requested operation could not be performed — usualmente un mismatch de columnas en UpsertData / InsertData. El schema del DE destino cambió, tu lista de columnas no.
  • Object expected / 'Platform' is not defined — Platform.Load falta o el script intentó usar Platform.* antes de la load call. Ver gotchas — #2.
  • Ninguna fila para este RunId: o el script tiene bloques catch {} pelados tragándose errores en silencio (ver gotchas — #7), o la forma de la falla no es una excepción — es una truncación silenciosa (paso 5 abajo).

Paso 4 — ¿Pegó el timeout de 30 minutos?

Calculá la duración entre la primera y la última fila de log para el RunId. Si el Ts de la última fila está más de 28 minutos después del Ts de la fila init, el script casi con seguridad pegó el timeout duro de 30 minutos (ver gotchas — #3).

SELECT
  RunId,
  MIN(Ts)                                AS StartedAt,
  MAX(Ts)                                AS LastWriteAt,
  DATEDIFF(minute, MIN(Ts), MAX(Ts))     AS DurationMinutes
FROM de_log_ssjs_runs
WHERE RunId = 'a1b2c3d4-e5f6-...'
GROUP BY RunId;

Un DurationMinutes de 29 o 30 sin fila de completion es la firma del timeout. El fix es estructural, no en el paso que falla: o partís el trabajo entre Script Activities paralelas (ver gotchas — #10), pivoteás a una SQL Query Activity para la lectura en bulk, o agregás cotas de paginación adentro del loop (ver gotchas — #9).

Paso 5 — ¿LookupRows truncó silenciosamente?

El cap de 2500 de LookupRows no tira error. Devuelve las primeras 2500 filas y tu script procesa solo esas. Para detectar la falla después del hecho, buscá filas en de_log_ssjs_runs que registraron un paso lookup con un row count de exactamente 2500 — esa es la firma del cap.

SELECT
  RunId,
  Step,
  Message,
  Ts
FROM de_log_ssjs_runs
WHERE Step LIKE '%lookup%'
  AND Message LIKE '%count=2500%'
  AND Ts BETWEEN '2026-05-13 02:00' AND '2026-05-13 03:00';

Esto asume que el script loggeó el row count con cada llamada a LookupRows (ej. log("lookup-subs", "count=" + rows.length)). Si no, esta query devuelve nada — y la única manera de confirmar la truncación silenciosa es re-correr el script con una línea de log corregida, o pivotar la lectura a una SQL Query Activity y comparar row counts.

Ver gotchas — #5 para el patrón que previene que esto pase dos veces.

Paso 6 — Escribí el postmortem

Una vez encontrado el bug, escribí el diagnóstico a un de_log_ssjs_postmortems para que la próxima vez que algo se vea raro tengas baselines históricos y un rastro escrito.

INSERT INTO de_log_ssjs_postmortems
SELECT
  GETDATE()                                AS DiagnosedAt,
  'SA_NightlyEnrichment'                   AS ActivityName,
  'a1b2c3d4-e5f6-...'                      AS RunId,
  (SELECT MIN(Ts) FROM de_log_ssjs_runs WHERE RunId = 'a1b2c3d4-e5f6-...')                 AS StartedAt,
  (SELECT MAX(Ts) FROM de_log_ssjs_runs WHERE RunId = 'a1b2c3d4-e5f6-...')                 AS LastWriteAt,
  (SELECT TOP 1 Step FROM de_log_ssjs_runs WHERE RunId = 'a1b2c3d4-e5f6-...' ORDER BY Ts DESC) AS LastStep,
  (SELECT COUNT(*) FROM de_log_ssjs_errors WHERE RunId = 'a1b2c3d4-e5f6-...')              AS ErrorCount,
  'LookupRows cap hit at 03-enrich; pivoted to SQL Query Activity'                         AS RootCause;

Corré esto después de cada diagnóstico. Seis meses después cuando una Activity similar se ponga rara, vas a tener la historia para reconocer el patrón en vez de empezar de cero.

Causas comunes rankeadas por frecuencia

| Causa | Cómo detectarla | Fix en | |---|---|---| | LookupRows devolvió 2500 silenciosamente | El paso 5 encuentra un paso lookup con count=2500 | gotchas — #5; pivotear a SQL Query Activity | | Timeout duro de 30 minutos | DurationMinutes del paso 4 es 29-30 sin fila de completion | gotchas — #3; partir entre Script Activities | | Auth de WSProxy expiró a mitad del loop | El paso 3 muestra Token has expired | WSProxy; re-instanciar cada 15 min | | HTTP.Get / Post pegó el timeout de 60s | El paso 3 muestra un mensaje de request-timeout | gotchas — #9; paginar + capear iteraciones | | catch {} pelado se tragó un error | El paso 3 no devuelve filas pero el paso 2 paró abruptamente | gotchas — #7; loggear adentro de cada catch | | Platform.Load faltante | El paso 1 devuelve cero filas init; el Run History muestra Platform is not defined | gotchas — #2; línea 1 de cada bloque | | SyntaxError de SpiderMonkey 1.7 | El paso 1 devuelve cero filas init; el Run History muestra el parse error | gotchas — #1; nada de JS moderno | | UpsertData contra PK equivocado | El paso 3 puede no mostrar nada; los row counts en DE downstream están mal | gotchas — #4; verificar PK del destino | | Loop while sin cota | DurationMinutes del paso 4 cerca del timeout, último paso adentro de un callout paginado | gotchas — #9; agregar && i < N | | La Automation no se disparó | El paso 1 devuelve cero filas para toda la ventana | Chequear status del paso upstream + schedule de la Automation |

Relacionado

  • Basics — dos contextos (CloudPage vs Script Activity) y cómo difiere el output
  • Platform.Function — las llamadas UpsertData / LookupRows / GUID de las que dependen las queries de diagnóstico
  • WSProxy — patrón de expiración del token de auth (la causa #1 de fallas en loops largos)
  • Gotchas de MC SSJS — las formas de falla referenciadas a lo largo de este playbook
  • Style Guide — la disciplina de instrumentación (RunId + Step + Ts + log-en-catch) que hace que estas queries sean posibles en primer lugar