Skip to main content

WSProxy — Referencia de SSJS en Marketing Cloud

El wrapper de la SOAP API para SSJS — cuando Platform.Function no alcanza. Retrieve / Create / Update / Delete con soporte completo de filtros, paginación más allá de 2500 filas, y el patrón de refresh de auth-token que mantiene los scripts largos vivos.

Referencia·Actualizado 2026-05-08·Escrito por Lira · Editado por German Medina

WSProxy es el puente entre SSJS y la SOAP API de Marketing Cloud. La usás cuando Platform.Function.* no tiene lo que necesitás — retrieves bulk pasados los 2500 rows, expresiones de filtro completas (combinaciones de LogicalOperator), operaciones a nivel Subscriber, introspección de schema, y cualquier tipo de objeto que el wrapper de alto nivel no expone. El trade-off: sintaxis más verbose, paginación manual, y una expiración de token de 20 minutos que tenés que manejar vos.

Sintaxis oficial

El proxy se instancia desde Script.Util.WSProxy después de Platform.Load("Core","1.1.5"):

Platform.Load("Core", "1.1.5");
var prox = new Script.Util.WSProxy();

// === RETRIEVE ===

// Retrieve simple con filtro de una sola property
var result = prox.retrieve(
  "Subscriber",                                  // tipo de objeto
  ["EmailAddress", "Status", "SubscriberKey"],   // properties a recuperar
  {                                              // filtro
    Property: "SubscriberKey",
    SimpleOperator: "equals",
    Value: "12345"
  }
);

// Forma del result:
// result.Status     → "OK" | "MoreDataAvailable" | error
// result.Results    → array de objetos result
// result.HasMoreRows → booleano (paginá si es true)
// result.RequestID  → token opaco para continuación

// Filtro compuesto con AND
var resultActive = prox.retrieve(
  "Subscriber",
  ["EmailAddress", "Status"],
  {
    LeftOperand: {
      Property: "Status",
      SimpleOperator: "equals",
      Value: "Active"
    },
    LogicalOperator: "AND",
    RightOperand: {
      Property: "EmailAddress",
      SimpleOperator: "like",
      Value: "%@enterprise.com"
    }
  }
);

// === PAGINACIÓN (pasados los 2500 rows) ===

var allResults = [];
var moreData = true;
var reqID = null;

while (moreData) {
  var page = reqID
    ? prox.getNextBatch("Subscriber", reqID)
    : prox.retrieve("Subscriber", ["EmailAddress","Status"], filter);

  for (var i = 0; i < page.Results.length; i++) {
    allResults.push(page.Results[i]);
  }

  moreData = page.HasMoreRows;
  reqID = page.RequestID;
}

// === CREATE / UPDATE / DELETE ===

prox.createItem("DataExtensionObject[my_de]", {
  SubscriberKey: "12345",
  EmailAddress: "user@example.com"
});

prox.updateItem("DataExtensionObject[my_de]", {
  CustomerKey: "12345",       // PK en el destino
  EmailAddress: "new@example.com"
});

prox.deleteItem("DataExtensionObject[my_de]", {
  CustomerKey: "12345"
});

// === EJECUTAR REQUESTS CUSTOM ===

// Disparar una Send Definition
prox.execute("Perform", {
  Action: "start",
  Definitions: [{
    "@xsi:type": "TriggeredSendDefinition",
    CustomerKey: "TS_my_send"
  }]
});

Las operaciones más usadas:

| Método | Qué hace | Notas | |---|---|---| | retrieve(type, props, filter) | Lee objetos que matcheen el filtro (max 2500 por llamada) | Devuelve Results + HasMoreRows + RequestID | | getNextBatch(type, reqID) | Continuar retrieve pasados los primeros 2500 | Pasá el RequestID de la llamada previa | | createItem(type, props) | Crear un objeto | Create de una sola fila | | updateItem(type, props) | Actualizar un objeto por PK | Match en el campo de primary key del tipo | | deleteItem(type, props) | Borrar un objeto por PK | Match en el campo de primary key del tipo | | execute(reqType, props) | Perform / Configure requests custom | TriggeredSends, ImportDefinitions, etc. | | describe(type) | Conseguir metadata de schema para un tipo de objeto | Útil para introspección | | getSystemStatus() | Health check | Probe rápido de "está la API arriba" |

Tipos de objeto comunes: Subscriber, List, DataExtension, DataExtensionField, DataExtensionObject[<key>] (para lecturas/escrituras de filas de DE), Send, TriggeredSendDefinition, Email, Folder, Account.

Valores de SimpleOperator: equals, notEquals, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual, isNull, isNotNull, between, IN, like.

Referencia:

Lo que sobrevive en producción

Refrescá el proxy cada ~15 minutos para scripts largos

Los tokens de auth de WSProxy expiran cerca de la marca de los 20 minutos. Un loop que recupera un millón de subscribers en 400 páginas va a pegar la expiración a mitad de corrida y empezar a devolver errores de auth. La defensa es trackear cuándo instanciaste el proxy última vez y re-instanciarlo antes de que el token expire.

Platform.Load("Core", "1.1.5");
var prox = new Script.Util.WSProxy();
var lastRefresh = Now();

var moreData = true;
var reqID = null;
var allRows = [];

while (moreData) {
  // Refrescar proxy cada 15 min (margen de 5 min bajo la expiración de 20 min)
  if (DateDiff(lastRefresh, Now(), "Minute") > 15) {
    prox = new Script.Util.WSProxy();
    lastRefresh = Now();
  }

  var page = reqID
    ? prox.getNextBatch("Subscriber", reqID)
    : prox.retrieve("Subscriber", ["EmailAddress"], filter);

  // ... procesar página ...

  moreData = page.HasMoreRows;
  reqID = page.RequestID;
}

Ver gotchas SSJS — #8.

HasMoreRows + RequestID es el único contrato de paginación

No hay offset/limit. La primera llamada devuelve hasta 2500 filas más un RequestID si hay más. Cada llamada subsiguiente usa getNextBatch(objectType, requestID) con el mismo RequestID para conseguir el siguiente batch. Cuando HasMoreRows es false, terminaste.

La trampa: si tu loop se olvida de actualizar reqID desde page.RequestID después de cada getNextBatch, vas a re-fetchear la misma página para siempre.

// EN RIESGO — typo en nombre de variable causa re-fetch infinito
while (moreData) {
  var page = reqID
    ? prox.getNextBatch("Subscriber", reqID)
    : prox.retrieve("Subscriber", ["EmailAddress"], filter);
  // ...
  moreData = page.HasMoreRows;
  // se olvidó: reqID = page.RequestID;
}

// SEGURO — siempre actualizá reqID, más un cap superior de iteraciones
var maxIterations = 1000;  // 1000 × 2500 = 2.5M filas de techo
while (moreData && maxIterations-- > 0) {
  var page = reqID
    ? prox.getNextBatch("Subscriber", reqID)
    : prox.retrieve("Subscriber", ["EmailAddress"], filter);
  // ...
  moreData = page.HasMoreRows;
  reqID = page.RequestID;   // crítico
}

if (maxIterations <= 0) {
  Platform.Function.UpsertData(
    "de_log_errors",
    ["RunId"], [runId],
    ["Step","Msg","Ts"],
    ["wsproxy-page-cap-hit", "excedió 1000 iteraciones de página", Now()]
  );
}

El cap de maxIterations es lo que te salva cuando algo legítimamente produce más data de la que esperabas.

El campo Status te dice qué pasó realmente

WSProxy no tira en fallas parciales como esperarías. El campo result.Status es la señal canónica — "OK" significa éxito, "MoreDataAvailable" significa que existen más páginas, cualquier otra cosa es error. Siempre chequealo.

var result = prox.retrieve("Subscriber", ["EmailAddress"], filter);

if (result.Status !== "OK" && result.Status !== "MoreDataAvailable") {
  Platform.Function.UpsertData(
    "de_log_errors",
    ["RunId"], [runId],
    ["Step","Msg","Ts"],
    ["wsproxy-retrieve",
     "Status: " + result.Status + " | ErrorMessage: " + (result.ErrorMessage || "n/a"),
     Now()]
  );
  // Decidí: tirar, retry con backoff, o fallar la Activity
}

Los filtros en WSProxy son objetos, no strings — matcheá el schema SOAP exactamente

La estructura del objeto filtro no perdona. Un typo en Property (ej. Properties) o SimpleOperator (ej. Operator) no tira en momento de parse del script — manda un request SOAP malformado, el server lo rechaza, y el Status del result reporta un error oscuro.

// EN RIESGO — typo en "SimpleOperator" → falla SOAP silenciosa
var result = prox.retrieve("Subscriber", ["EmailAddress"], {
  Property: "Status",
  Operator: "equals",   // key equivocada — debería ser SimpleOperator
  Value: "Active"
});

// CORRECTO — matcheá los nombres de campo del schema SOAP exactamente
var result = prox.retrieve("Subscriber", ["EmailAddress"], {
  Property: "Status",
  SimpleOperator: "equals",
  Value: "Active"
});

Cross-checkeá los nombres de campo contra la referencia de objetos de la SOAP API (link arriba) antes de asumir que una forma de filtro va a funcionar. Los mensajes de error no siempre son útiles.

DataExtensionObject[<key>] para ops de fila de DE vía WSProxy

Cuando Platform.Function.LookupRows no alcanza — necesitás más de 2500 filas, filtros complejos, o comportamiento async — bajá a WSProxy con el tipo de objeto especial DataExtensionObject[<de-customer-key>]. El customer key es la property "External Key" del DE, no su nombre.

// Recuperar todas las filas de un DE que matcheen un filtro complejo
var result = prox.retrieve(
  "DataExtensionObject[my_de_customer_key]",
  ["SubscriberKey", "EmailAddress", "LoyaltyTier"],
  {
    LeftOperand: {
      Property: "Status",
      SimpleOperator: "equals",
      Value: "Active"
    },
    LogicalOperator: "AND",
    RightOperand: {
      Property: "LastPurchase",
      SimpleOperator: "greaterThan",
      Value: "2026-01-01"
    }
  }
);

El customer key del DE es visible en el panel de Properties del DE en Email Studio bajo "External Key". Conseguí la key equivocada y result.Status te va a decir que el tipo de objeto no existe.

Decisión rápida

Usá WSProxy.retrieve en lugar de LookupRows cuando:

  • Necesitás más de 2500 filas.
  • Necesitás un filtro compuesto (combinaciones AND/OR de LogicalOperator).
  • Necesitás recuperar objetos de Salesforce más allá de filas de Data Extension (Subscribers, Lists, Sends, Folders, etc.).

Quedate con Platform.Function.LookupRows cuando:

  • El resultado esperado está cómodamente bajo los 2500 rows.
  • El filtro es simple (1–2 columnas, igualdad).
  • El costo de performance de la verbosidad de WSProxy no se justifica.

Usá WSProxy.createItem / updateItem / deleteItem cuando:

  • Estás trabajando con objetos no-DE (Subscriber, List, Send, etc.).
  • Necesitás semántica explícita de insert-vs-update (el create de SOAP tira en duplicates en lugar de insertarlos silenciosamente).

Quedate con Platform.Function.InsertData / UpdateData / UpsertData cuando:

  • Estás escribiendo a un DE y el wrapper de alto nivel hace lo que necesitás.
  • Querés la conveniencia de upsert (con la salvedad de la trampa de PK — ver gotchas — #4).

Refrescá el proxy cuando:

  • El runtime esperado del script es más de 15 minutos.
  • Estás paginando result sets grandes en un loop.

Relacionado

  • Basics — qué es SSJS y los dos contextos
  • Platform.Function — el wrapper de alto nivel que es suficiente para la mayoría de los scripts
  • MC SSJS gotchas — ver #5 (cap de LookupRows, la razón para bajar a WSProxy), #8 (expiración de auth token)
  • MC SQL — INSERT INTO — modelo mental hermano de target-action

Próximas páginas de referencia SSJS: Variable + Util · Categorías de funciones String / Date / Encoding / Hashing · Style Guide.

Más snippets how-to para patrones comunes de producción — secuencias DE add/update/upsert, manejo de errores, paginación de callouts, etc.