Differs from Official Docs
Every SSJS function, object, and property whose runtime behavior in the SFMC engine differs from the official Salesforce documentation — return types, required arguments, null vs empty-string results, and more. Each entry is runtime-verified on a live CloudPage.
The official Salesforce SSJS documentation contains a number of inaccuracies: wrong return types, arguments listed as optional that are actually required, properties that exist but are undocumented, and members that behave differently than described. Every entry below has been runtime-verified on a live CloudPage and is flagged in the reference pages with such a note:
…
This page is the single, growing catalog of those discrepancies. Each row links to the method’s main reference page, where the same discrepancy is documented inline.
This differs from Known Bugs: entries here are cases where the docs are simply inaccurate about how a working feature behaves. Known Bugs covers features that are outright broken or that do not exist at runtime despite being documented.
Discrepancy type
Error — new Error("msg") leaves .message unset; call-form sets it; instanceof always false
Unlike standard JavaScript, new Error("msg") in the SFMC Jint engine does not populate .message — it reads back undefined (not an own property). Recover with String(err) or ("" + err); err.toString() yields "Error: undefined". Call-form Error("msg") does set .message, and engine-raised errors set .message plus .description. instanceof Error is always false (even for new Error(...)); for JS-constructed errors use err.constructor === Error or err.name. The same new vs call-form split applies to every legacy subtype (EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError).
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// new Error — message unset; String recovers it
try {
var err = new Error("boom");
Platform.Response.Write("new.message: " + err.message + "\n");
Platform.Response.Write("new.String: " + String(err) + "\n");
Platform.Response.Write("new.toString: " + err.toString() + "\n");
Platform.Response.Write("new instanceof Error: " + (err instanceof Error) + "\n");
Platform.Response.Write("new.constructor===Error: " + (err.constructor === Error) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// call-form — message IS set
try {
var c = Error("call-boom");
Platform.Response.Write("call.message: " + c.message + "\n");
Platform.Response.Write("call.Stringify: " + Platform.Function.Stringify(c) + "\n");
} catch (e2) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e2) + "\n"); }
</script>
Platform.Response — null returns, opaque property reads, and ineffective RemoveCookie
Response mutators including SetResponseHeader, RemoveResponseHeader, SetCookie, and RemoveCookie return JavaScript null, not void. ContentType and CharacterSet assignments work, but reads and calls expose opaque CLR values instead of the configured strings. In a published CloudPage GET with the named request cookie present, RemoveCookie emitted no deletion header. Use SetCookie(name, "", new Date(1970, 0, 1), true) to emit an expired cookie; the removal takes effect on a subsequent browser request.
Show test script
<script runat="server">
/*
* Differs-from-docs: Platform.Response
*
* Proves (body-assertable):
* 1. DEV: SetResponseHeader / RemoveResponseHeader / SetCookie /
* RemoveCookie return strict JS null (official docs: void).
* 2. ContentType / CharacterSet assignment does not throw.
* 3. DEV: ContentType / CharacterSet reads and calls return opaque
* CLR values, not the configured strings.
* 4. Expired-cookie workaround SetCookie("", past Date) returns null.
*
* NON-ASSERTABLE in body alone (use HttpWebRequest AllowAutoRedirect=false):
* - outbound header presence after SetResponseHeader
* - header absence after RemoveResponseHeader
* - Set-Cookie emission / RemoveCookie emitting no deletion header
* - expired-cookie workaround actually clearing on a later request
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function capture(fn) {
try { return { threw: false, value: fn() }; } catch (ex) { return { threw: true, message: "" + ex.message }; }
}
var setHdr = Platform.Response.SetResponseHeader("X-Dfd-Response", "set-value");
assert("DEV SetResponseHeader returns null (official docs: void)", setHdr === null ? "null" : "other", "null");
var remHdr = Platform.Response.RemoveResponseHeader("X-Dfd-Response");
assert("DEV RemoveResponseHeader returns null (official docs: void)", remHdr === null ? "null" : "other", "null");
var setCk = Platform.Response.SetCookie("dfdResponseCookie", "ck-value");
assert("DEV SetCookie returns null (official docs: void)", setCk === null ? "null" : "other", "null");
var remCk = Platform.Response.RemoveCookie("dfdResponseCookie");
assert("DEV RemoveCookie returns null (official docs: void)", remCk === null ? "null" : "other", "null");
Platform.Response.ContentType = "text/plain";
Platform.Response.CharacterSet = "UTF-8";
var ctRead = capture(function () { return Platform.Response.ContentType; });
var ctCall = capture(function () { return Platform.Response.ContentType(); });
var csRead = capture(function () { return Platform.Response.CharacterSet; });
var csCall = capture(function () { return Platform.Response.CharacterSet(); });
assert("ContentType assignment + read does not throw", ctRead.threw ? "threw" : "returned", "returned");
assert("DEV ContentType read is CLR (official docs: configured string)", "" + (typeof ctRead.value), "clr");
assert("ContentType call does not throw", ctCall.threw ? "threw" : "returned", "returned");
assert("DEV ContentType call is CLR (official docs: configured string)", "" + (typeof ctCall.value), "clr");
assert("CharacterSet assignment + read does not throw", csRead.threw ? "threw" : "returned", "returned");
assert("DEV CharacterSet read is CLR (official docs: configured string)", "" + (typeof csRead.value), "clr");
assert("CharacterSet call does not throw", csCall.threw ? "threw" : "returned", "returned");
assert("DEV CharacterSet call is CLR (official docs: configured string)", "" + (typeof csCall.value), "clr");
var fallback = Platform.Response.SetCookie("dfdResponseExpire", "", new Date(1970, 0, 1), true);
assert("expired SetCookie workaround returns null", fallback === null ? "null" : "other", "null");
</script>
Platform.Function.HTTPGet — returns the body, and all 6 args are required
The return value is the response body as a string, not a numeric status — the official docs’ claim that this returns a numeric status is wrong (typeof body === "string", and the body contains the fetched response). Two further points about the signature also differ from the docs:
- A 1-argument call
HTTPGet(url)succeeds and returns a string, so it is not true that all six arguments are required. (A 2-argument callHTTPGet(url, false)throws the generic “Unable to retrieve security descriptor for this frame.” error — the signal for an argument count the engine does not accept — while the 6-arg form succeeds; valid arities are exactly {1, 6}, not a simple “all required”.) - The
statusVariableout-parameter is not populated in a CloudPage context: after a 6-arg call,status.length === 0andstatus[0] === undefined. Do not rely onstatusVariable[0]for the numeric status here.
Prefer the 6-argument form for the response body, but read the status from the body/HTTP response itself rather than the out-parameter.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// HTTPGet returns the response BODY as a string. Live retest checks arg-count and statusVariable behaviour.
// Test A: 1-arg call (claim said all 6 required)
try {
var b1 = Platform.Function.HTTPGet("https://httpbin.org/status/200");
Platform.Response.Write("A 1-arg: OK typeof=" + (typeof b1) + " len=" + String(b1).length + "\n");
// OBSERVED: 1-arg succeeds, typeof=string, len=0 (CONTRADICTS \"all six args required\")
} catch (e) { Platform.Response.Write("A 1-arg ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// Test B: 6-arg call with a status array; inspect whether the out-parameter is populated
try {
var status = [];
var b6 = Platform.Function.HTTPGet("https://httpbin.org/status/200", false, 0, null, null, status);
Platform.Response.Write("B 6-arg: typeof body=" + (typeof b6) + "\n");
Platform.Response.Write("B status.length=" + status.length + " status[0]=" + status[0] + "\n");
// OBSERVED: typeof body=string; status.length=0, status[0]=undefined (statusVariable NOT populated)
} catch (e) { Platform.Response.Write("B 6-arg ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// Test C: 2-arg call
try {
var b2 = Platform.Function.HTTPGet("https://httpbin.org/status/200", false);
Platform.Response.Write("C 2-arg: OK typeof=" + (typeof b2) + "\n");
} catch (e) { Platform.Response.Write("C 2-arg ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// OBSERVED: C 2-arg throws \"Unable to retrieve security descriptor for this frame.\"
</script>
Platform.Function.Lookup — returns native types and null (not string / "")
The official docs type the return as a string, but at runtime Lookup returns the column’s native type: Number/Decimal → number, Boolean → boolean, Date → a real Date object, Text/EmailAddress → string.
There are three distinct empty-ish returns — a strict === null check catches only one of them:
| Situation | Value | typeof |
=== null |
== null |
truthiness | String() |
|---|---|---|---|---|---|---|
| No matching row | genuine JS null |
object |
true |
false |
falsy | "null" |
| Row exists, field is empty/NULL | CLR null | "clr" |
false |
throws | throws | "" |
Row exists, field holds "" |
empty string | string |
false |
false |
falsy | "" |
| Field is populated | native value | native | false |
false |
truthy | value |
The empty/NULL-field case is the trap: it is not === null, its typeof is the SFMC-only "clr", and every coercion of it throws — value == null throws “Value cannot be null.” and a boolean context (if (value)) throws “Object cannot be cast from DBNull to other types.” Neither a loose == null nor a truthiness check is a safe guard; coerce with String() first and test the resulting string against "" and "null".
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Lookup returns the column's NATIVE type; empty/NULL field returns a CLR null (== null but NOT === null).
// No-match branch VERIFIED against the empty SSJSGUIDE_TYPES fixture DE (read-only).
try {
var val = Platform.Function.Lookup("SSJSGUIDE_TYPES", "Txt", "Pk", "no-such-key");
// OBSERVED (no matching row): typeof "object", === null true, == null true, String() = "null".
Platform.Response.Write("no-match typeof: " + (typeof val) + "\n");
Platform.Response.Write("no-match === null (strict): " + (val === null) + "\n");
Platform.Response.Write("no-match == null (loose): " + (val == null) + "\n");
Platform.Response.Write("no-match String(): '" + String(val) + "'\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e).substring(0,180) + "\n"); }
// NOTE: the native-type branch (Number->number, Date->Date object, etc.) and the empty/NULL-field
// CLR-null trap (typeof "clr", == null true, === null FALSE) require a SEEDED row in the fixture DE.
// Those were NOT re-verified in this run because seeding rows mutates tenant state; seed
// SSJSGUIDE_TYPES with one row (all field types populated + one left blank) to re-prove them:
// var row = Platform.Function.Lookup("SSJSGUIDE_TYPES", "Num", "Pk", "<seededPk>"); // typeof "number"
// var blank = Platform.Function.Lookup("SSJSGUIDE_TYPES", "Txt", "Pk", "<seededPkWithBlankTxt>"); // typeof "clr", === null FALSE
</script>
Platform.Function.LookupRows / LookupOrderedRows — null on no-match, plus system fields
Reference: LookupRows · LookupOrderedRows
Both return null (not an empty array []) when no row matches — guard before reading .length. Each returned row object also carries two undocumented system fields: _CustomObjectKey (a number) and _CreatedDate (a string). LookupOrderedRows additionally normalizes a NULL Text field in a returned row to an ordinary empty string: strict and loose null comparisons are false, a truthiness test is safely falsy, and String() yields ""; this differs from scalar Lookup, whose NULL field is a hazardous CLR null. When rows do match, the collection is a genuine JavaScript Array (object[]): Array.isArray() returns true and its own .push/.slice/.sort methods work — but instanceof Array is unreliable in the SFMC engine (it returns false even for a plain array literal), so test with the Array.isArray polyfill rather than instanceof. Note that borrowed Array.prototype methods fail on it: Array.prototype.slice.call(rows) throws “Index was outside the bounds of the array.” — call the array’s own rows.slice(...) instead.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// LookupRows/LookupOrderedRows return null (NOT []) on no match.
// No-match branch VERIFIED against the empty SSJSGUIDE_TYPES fixture DE (read-only).
try {
var rows = Platform.Function.LookupRows("SSJSGUIDE_TYPES", "Pk", "no-such-key");
// OBSERVED: typeof "object", === null true, == null true (genuine JS null, not []).
Platform.Response.Write("no-match typeof: " + (typeof rows) + "\n");
Platform.Response.Write("no-match === null: " + (rows === null) + "\n");
Platform.Response.Write("no-match == null: " + (rows == null) + "\n");
} catch (e) { Platform.Response.Write("LookupRows ERROR: " + Platform.Function.Stringify(e).substring(0,180) + "\n"); }
try {
var ordered = Platform.Function.LookupOrderedRows("SSJSGUIDE_TYPES", 10, "Pk asc", "Pk", "no-such-key");
// OBSERVED: typeof "object", === null true.
Platform.Response.Write("ordered no-match typeof: " + (typeof ordered) + " === null: " + (ordered === null) + "\n");
} catch (e) { Platform.Response.Write("LookupOrderedRows ERROR: " + Platform.Function.Stringify(e).substring(0,180) + "\n"); }
// NOTE: the matched-rows branch (undocumented _CustomObjectKey/_CreatedDate system fields, genuine
// Array.isArray true, own .slice works but borrowed Array.prototype.slice.call throws "Index was
// outside the bounds of the array") requires a SEEDED row in the fixture DE and was NOT re-verified
// here (seeding rows mutates tenant state). Seed one row into SSJSGUIDE_TYPES and re-run:
// var rows = Platform.Function.LookupRows("SSJSGUIDE_TYPES", "Pk", "<seededPk>");
// rows[0]["_CustomObjectKey"]; rows[0]["_CreatedDate"]; Array.isArray(rows); rows.slice(0,1);
</script>
Platform.Function.UpsertData — requires arrays for every filter and field name/value argument
The official reference permits scalar strings for a single whereFieldNames / whereFieldValues pair, and also shows a flat/variadic form (UpsertData(deName, field1, value1, …, filterField, filterValue)). Neither works at runtime. Only the array-based five-argument signature is accepted: whereFieldNames, whereFieldValues, fieldNames and fieldValues must all be nonempty, positionally aligned arrays, and every scalar position throws. Wrap single columns and values in one-element arrays. The Data Extension is also resolved by Name only — passing the external key / CustomerKey throws.
Show test script
<script runat="server">
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
var field = Platform.Function.CreateObject("DataExtensionField");
Platform.Function.SetObjectProperty(field, "Name", name);
Platform.Function.SetObjectProperty(field, "FieldType", "Text");
Platform.Function.SetObjectProperty(field, "MaxLength", len);
Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey ? "true" : "false");
Platform.Function.SetObjectProperty(field, "IsRequired", isKey ? "true" : "false");
Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
var de = Platform.Function.CreateObject("DataExtension");
Platform.Function.SetObjectProperty(de, "CustomerKey", key);
Platform.Function.SetObjectProperty(de, "Name", name);
addField(de, "Id", "50", true); addField(de, "Txt", "100", false);
var status = [0, 0]; return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
var de = Platform.Function.CreateObject("DataExtension");
Platform.Function.SetObjectProperty(de, "CustomerKey", key);
var status = [0, 0]; return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_up_arr_2247_name", deKey = "ssjsg_up_arr_2247_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("control: Name and CustomerKey differ", deName === deKey ? "same" : "different", "different");
// Every scalar position is rejected with the engine's generic unaccepted-signature signal.
assertThrows("DEV scalar whereFieldNames throws (docs: string or string[])", function () { return Platform.Function.UpsertData(deName, "Id", ["a"], ["Txt"], ["wrong"]); });
assertThrows("DEV scalar whereFieldValues throws (docs: string or array)", function () { return Platform.Function.UpsertData(deName, ["Id"], "a", ["Txt"], ["wrong"]); });
assertThrows("scalar fieldNames throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["a"], "Txt", ["wrong"]); });
assertThrows("scalar fieldValues throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["a"], ["Txt"], "wrong"); });
assertThrows("DEV the CustomerKey form throws (docs: data extension name)", function () { return Platform.Function.UpsertData(deKey, ["Id"], ["a"], ["Txt"], ["wrong"]); });
// The workaround: one-element arrays in all four positions.
assert("workaround: one-element arrays insert a new row", Platform.Function.UpsertData(deName, ["Id"], ["a"], ["Txt"], ["inserted"]), 1);
assert("workaround: one-element arrays update the existing row", Platform.Function.UpsertData(deName, ["Id"], ["a"], ["Txt"], ["updated"]), 1);
assert("the workaround update committed", String(Platform.Function.Lookup(deName, "Id", "Txt", "updated")), "a");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>
Platform.Function.HTTPPost — only 3 or 6 arguments; error statuses throw instead of being returned
Two corrections. Arity: the docs list headerNames, headerValues and response as independently optional, but the argument count is a discontinuous overload — only a 3-argument call or the full 6-argument call work. Passing 4 or 5 arguments throws Unable to retrieve security descriptor for this frame. Error statuses: the docs present the return value as the HTTP status code of whatever the server answered (their own example branches on statusCode == 200), but only a successful status is ever returned. A 4xx or 5xx response throws An error occurred when attempting to evaluate a HTTPPost function call. instead, so a failing status can never be read from the return value — wrap the call in try/catch. The response out-parameter also stays empty even on success; use HTTP.Post, whose Response[0] carries the body.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
var CT = "application/json";
var PAYLOAD = "{\"a\":1}";
// Arity: only 3 and 6 arguments are accepted.
try {
var three = Platform.Function.HTTPPost("https://postman-echo.com/post", CT, PAYLOAD);
Platform.Response.Write("arity 3 => " + three + "\n"); // OBSERVED: 200
} catch (e) { Platform.Response.Write("arity 3 ERROR: " + Platform.Function.Stringify(e) + "\n"); }
try {
Platform.Function.HTTPPost("https://postman-echo.com/post", CT, PAYLOAD, ["x-test"]);
Platform.Response.Write("arity 4: unexpectedly OK\n");
} catch (e) {
// OBSERVED: "Unable to retrieve security descriptor for this frame." - the engine's
// generic unaccepted-signature (arity) signal, NOT a security/auth error.
Platform.Response.Write("arity 4 ERROR (expected): " + Platform.Function.Stringify(e) + "\n");
}
// Error statuses throw. CONTROL: the same host returns a success status normally.
try {
Platform.Response.Write("status 201 => " + Platform.Function.HTTPPost("https://httpbin.org/status/201", CT, PAYLOAD) + "\n");
} catch (e) { Platform.Response.Write("201 ERROR: " + Platform.Function.Stringify(e) + "\n"); }
try {
Platform.Function.HTTPPost("https://httpbin.org/status/404", CT, PAYLOAD);
Platform.Response.Write("status 404: unexpectedly returned\n");
} catch (e) {
// OBSERVED: "An error occurred when attempting to evaluate a HTTPPost function call."
Platform.Response.Write("status 404 ERROR (expected): " + Platform.Function.Stringify(e) + "\n");
}
// The response out-parameter stays empty even on a successful call.
try {
var response = [];
Platform.Function.HTTPPost("https://postman-echo.com/post", CT, PAYLOAD, null, null, response);
Platform.Response.Write("response.length => " + response.length + "\n"); // OBSERVED: 0
} catch (e) { Platform.Response.Write("out-param ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Data Extension functions — resolve by Name, not external key
Reference: Lookup · LookupRows · LookupOrderedRows · InsertData · UpdateData · UpsertData · DeleteData · InsertDE · UpdateDE · UpsertDE · DeleteDE
All eleven Platform.Function Data Extension functions resolve the DE by its Name only. Passing the external key / CustomerKey throws “A Data Extension of this name does not exist.” — verified per-function against a fixture whose CustomerKey deliberately differs from its Name. None of them accept the external key, and none accept both.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// DE functions resolve by NAME only. Proven against a fixture whose external key deliberately
// DIFFERS from its Name: Name = "SSJSGUIDE_VERIFY2_NAME", CustomerKey = "SSJSGUIDE_VERIFY2_KEY".
// Probe A: pass the NAME -> resolves (no "does not exist" error).
try {
var rows = Platform.Function.LookupRows("SSJSGUIDE_VERIFY2_NAME", "Pk", "x");
// OBSERVED: resolves; typeof rows = object. The Name is accepted.
Platform.Response.Write("A NAME resolves: OK (typeof rows=" + (typeof rows) + ")\n");
} catch (e) {
Platform.Response.Write("A NAME ERROR: " + Platform.Function.Stringify(e).substring(0,180) + "\n");
}
// Probe B: pass the external KEY where a Name is expected -> throws "does not exist".
try {
Platform.Function.LookupRows("SSJSGUIDE_VERIFY2_KEY", "Pk", "x");
Platform.Response.Write("B KEY: unexpectedly resolved (key accepted!)\n");
} catch (e) {
// OBSERVED: {"message":"The Data Extension name for a LookupRows function call is invalid.
// A Data Extension of this name does not exist. Data Extension Name: SSJSGUIDE_VERIFY2_KEY ..."}
Platform.Response.Write("B KEY ERROR (expected): " + Platform.Function.Stringify(e).substring(0,180) + "\n");
}
// CONFIRMED: with key != name, the NAME resolves and the external KEY throws the documented
// "A Data Extension of this name does not exist." error. DE-name-taking Platform functions accept
// the Name ONLY — never the external key, and never both.
</script>
Platform.Function.InvokeCreate — returns the OverallStatus string, not an object
The official docs type the return value as an object, but at runtime the call returns the OverallStatus message as a string ("OK" / "Error") — runtime-confirmed. The valid signature is 3 arguments: InvokeCreate(apiObject, status, options). The status argument is a single array: status[0] holds the OverallStatus message and status[1] holds the numeric request-id / error code (there are no separate statusMsgVar / errorCodeVar out-parameters). Calling with any other argument count throws "Unable to retrieve security descriptor for this frame" — the engine’s generic arity / no-matching-overload signal, not a security, auth, or CloudPage-context error.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeCreate returns the OverallStatus MESSAGE as a STRING ("OK" / "Error") — not an object. CONFIRMED at runtime.
// Valid signature is 3 args: InvokeCreate(apiObject, status[], options). status[0]=message, status[1]=numeric code.
// Wrong arg counts throw "Unable to retrieve security descriptor for this frame" (Jint arity signal, NOT auth).
try {
var status = [];
var apiObject = Platform.Function.CreateObject("DataExtension");
// 3-arg form (options may be null) returns the string OverallStatus; "Error" here because the payload is intentionally invalid.
var result = Platform.Function.InvokeCreate(apiObject, status, null);
Platform.Response.Write("InvokeCreate return typeof: " + (typeof result) + "\n");
Platform.Response.Write("result: " + result + "\n");
Platform.Response.Write("status[0] (message): " + status[0] + "\n");
Platform.Response.Write("status[1] (code): " + status[1] + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.InvokeUpdate — returns the OverallStatus string, not an object
The official docs type the return value as an object, but at runtime the call returns the OverallStatus message as a string ("OK" / "Error") — runtime-confirmed. The valid signature is 3 arguments: InvokeUpdate(apiObject, status, options). The status argument is a single array: status[0] holds the OverallStatus message and status[1] holds the numeric request-id / error code. The documented separate statusMsgVar / errorCodeVar out-parameters are refuted at runtime — supplying that 4-argument form throws. Any wrong argument count throws "Unable to retrieve security descriptor for this frame" — the engine’s generic arity / no-matching-overload signal, not a security, auth, or CloudPage-context error.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeUpdate returns the OverallStatus MESSAGE as a STRING ("OK" / "Error") — not an object. CONFIRMED at runtime.
// Valid signature is 3 args: InvokeUpdate(apiObject, status[], options). status[0]=message, status[1]=numeric code.
// The separate statusMsgVar/errorCodeVar 4-arg form is REFUTED (throws). Wrong arg counts throw the Jint arity signal, NOT auth.
try {
var status = [];
var apiObject = Platform.Function.CreateObject("DataExtension");
var result = Platform.Function.InvokeUpdate(apiObject, status, null);
Platform.Response.Write("InvokeUpdate return typeof: " + (typeof result) + "\n");
Platform.Response.Write("result: " + result + "\n");
Platform.Response.Write("status[0] (message): " + status[0] + "\n");
Platform.Response.Write("status[1] (code): " + status[1] + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.InvokeDelete — returns the OverallStatus string, not an object
The official docs type the return value as an object, but at runtime the call returns the OverallStatus message as a string ("OK" / "Error") — runtime-confirmed. The valid signature is 3 arguments: InvokeDelete(apiObject, status, options). The status argument is a single array: status[0] holds the OverallStatus message and status[1] holds the numeric request-id / error code. The documented separate statusMsgVar / errorCodeVar out-parameters are refuted at runtime — supplying that 4-argument form throws. Any wrong argument count throws "Unable to retrieve security descriptor for this frame" — the engine’s generic arity / no-matching-overload signal, not a security, auth, or CloudPage-context error.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeDelete returns the OverallStatus MESSAGE as a STRING ("OK" / "Error") — not an object. CONFIRMED at runtime.
// Valid signature is 3 args: InvokeDelete(apiObject, status[], options). status[0]=message, status[1]=numeric code.
// The separate statusMsgVar/errorCodeVar 4-arg form is REFUTED (throws). Wrong arg counts throw the Jint arity signal, NOT auth.
try {
var status = [];
var apiObject = Platform.Function.CreateObject("DataExtension");
var result = Platform.Function.InvokeDelete(apiObject, status, null);
Platform.Response.Write("InvokeDelete return typeof: " + (typeof result) + "\n");
Platform.Response.Write("result: " + result + "\n");
Platform.Response.Write("status[0] (message): " + status[0] + "\n");
Platform.Response.Write("status[1] (code): " + status[1] + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.InvokeRetrieve — returns object[] or null
The official docs type the return value as an object, but at runtime the call returns an array of result objects — or null when the retrieve errors or matches no rows. Both branches are runtime-confirmed: the null-on-error branch was observed with an unauthenticated Subscriber retrieve, and the array-of-objects SUCCESS branch was observed by retrieving a DataExtensionObject from a seeded Data Extension with three rows — the call returned an array-like value of .length 3 whose elements are DataExtensionObject records (each carrying a Properties array of {Name, Value} pairs). The two-argument signature matches the docs; unlike its InvokeExecute/InvokeExtract siblings, the docs do not list an options argument here.
One correction to the earlier claim: the status array is never populated. Even on the SUCCESS path, status.length stayed 0 and both status[0] and status[1] came back undefined — there is no OverallStatus message in status[0] and no request-id GUID in status[1]. The status out-parameter is inert for InvokeRetrieve; read the returned array itself for results. (Note: SSJS has no Array.isArray, so Array.isArray reports “n/a” — the return is nonetheless array-like: it has a numeric .length and is index-accessible.)
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeRetrieve returns an ARRAY of result objects — or null on error / no rows. Two-arg signature.
// SUCCESS path proven by retrieving a DataExtensionObject from a seeded DE (3 rows).
try {
var status = [];
var req = Platform.Function.CreateObject("RetrieveRequest");
Platform.Function.SetObjectProperty(req, "ObjectType", "DataExtensionObject[SSJSGUIDE_VERIFY]");
Platform.Function.AddObjectArrayItem(req, "Properties", "Pk");
Platform.Function.AddObjectArrayItem(req, "Properties", "Txt");
var results = Platform.Function.InvokeRetrieve(req, status);
// OBSERVED (CloudPage GET), SUCCESS branch:
// typeof results = "object", results.length = 3 (array-like; SSJS has no Array.isArray, so
// Array.isArray reports "n/a"). Each element is a DataExtensionObject, e.g.
// {"Type":"DataExtensionObject","Properties":[{"Name":"Pk","Value":"r1"},{"Name":"Txt","Value":"alpha"}], ...}.
// status.length = 0, status[0] = undefined, status[1] = undefined — the status array is NEVER
// populated (no message, no request-id GUID), even on success. Read the returned array for results.
// The null-on-error branch was separately observed with an unauthenticated Subscriber retrieve
// (results == null, typeof "object").
Platform.Response.Write("typeof results: " + (typeof results) + " length: " + (results != null ? results.length : "n/a") + "\n");
if (results != null && results.length > 0) {
Platform.Response.Write("element[0]: " + Platform.Function.Stringify(results[0]).substring(0,220) + "\n");
}
Platform.Response.Write("status.length: " + (status != null ? status.length : "n/a") + "\n");
Platform.Response.Write("status[0]: " + status[0] + " status[1]: " + status[1] + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.InvokePerform — returns the OverallStatus string, not an object
The official docs type the return value as an object, but at runtime the call returns the OverallStatus message as a string ("OK" / "Error") — runtime-confirmed. The valid signature takes 3 or 4 arguments: InvokePerform(apiObject, method, status[, options]), where method is the perform-action string. The status argument is a single array: status[0] holds the OverallStatus message, status[1] the numeric error code, and status[2] the serialized perform-response object. Any other argument count throws "Unable to retrieve security descriptor for this frame" — the engine’s generic arity / no-matching-overload signal, not a security, auth, or CloudPage-context error.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokePerform returns the OverallStatus MESSAGE as a STRING ("OK" / "Error") — not an object. CONFIRMED at runtime.
// Valid signature is 3 OR 4 args: InvokePerform(apiObject, method, status[, options]); method is the action string.
// status[0]=message, status[1]=numeric code, status[2]=serialized perform-response. Wrong arg counts throw the Jint arity signal, NOT auth.
try {
var status = [];
var apiObject = Platform.Function.CreateObject("Automation");
var result = Platform.Function.InvokePerform(apiObject, "start", status);
Platform.Response.Write("InvokePerform return typeof: " + (typeof result) + "\n");
Platform.Response.Write("result: " + result + "\n");
Platform.Response.Write("status[0] (message): " + status[0] + "\n");
Platform.Response.Write("status[1] (code): " + status[1] + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.InvokeConfigure — returns the OverallStatus string, not an object
The official docs type the return value as an object, but at runtime the call returns the OverallStatus message as a string ("OK" / "Error") — runtime-confirmed. The valid signature is 4 arguments: InvokeConfigure(apiObject, action, status, options), where action is the configure-action string. The status argument is a single array: status[0] holds the OverallStatus message and status[1] the numeric error code. Any other argument count throws "Unable to retrieve security descriptor for this frame" — the engine’s generic arity / no-matching-overload signal, not a security, auth, or CloudPage-context error.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeConfigure returns the OverallStatus MESSAGE as a STRING ("OK" / "Error") — not an object. CONFIRMED at runtime.
// Valid signature is 4 args: InvokeConfigure(apiObject, action, status[], options); action is the configure-action string.
// status[0]=message, status[1]=numeric code. Wrong arg counts throw the Jint arity signal, NOT auth.
try {
var status = [];
var apiObject = Platform.Function.CreateObject("List");
var result = Platform.Function.InvokeConfigure(apiObject, "configure", status, null);
Platform.Response.Write("InvokeConfigure return typeof: " + (typeof result) + "\n");
Platform.Response.Write("result: " + result + "\n");
Platform.Response.Write("status[0] (message): " + status[0] + "\n");
Platform.Response.Write("status[1] (code): " + status[1] + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.InvokeExecute — undocumented arity, returns object[]
The official docs are wrong on two counts. Signature: they list a third options argument, but at runtime the function accepts only two arguments (apiObject, status) — passing a third throws an “Unable to retrieve security descriptor for this frame” error. Return: they type the return value as an object, but at runtime the call returns an array of result objects.
The array-SUCCESS path is runtime-confirmed: a LogUnsubEvent execute request returned an array-like value of .length 1 whose single element is a result object of shape {StatusCode, StatusMessage, OrdinalID, Results, ErrorCode, ...} (observed: StatusCode:"Error", StatusMessage:"The Subscriber was not found", ErrorCode:12001 for a dummy subscriber). The per-item StatusCode:"Error" is a data-level result inside the array — not a null return — so the call reached and returned the array-of-objects shape. The status out-parameter is never populated: status.length stayed 0 and both status[0] and status[1] were undefined. (SSJS has no Array.isArray, so Array.isArray reports “n/a”; the return is still array-like — numeric .length, index-accessible.)
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeExecute accepts ONLY two args (apiObject, status); a third throws. Returns an ARRAY of result objects.
// SUCCESS path proven with a LogUnsubEvent execute request (dummy params).
try {
var status = [];
var ex = Platform.Function.CreateObject("ExecuteRequest");
Platform.Function.SetObjectProperty(ex, "Name", "LogUnsubEvent");
var addProp = function(name, val) {
var p = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(p, "Name", name);
Platform.Function.SetObjectProperty(p, "Value", val);
Platform.Function.AddObjectArrayItem(ex, "Parameters", p);
};
addProp("SubscriberKey", "verify_test@example.com");
addProp("JobID", "0"); addProp("ListID", "0"); addProp("BatchID", "0");
var results = Platform.Function.InvokeExecute(ex, status);
// OBSERVED (CloudPage GET), SUCCESS branch:
// typeof results = "object", results.length = 1 (array-like; SSJS has no Array.isArray).
// results[0] = {"StatusCode":"Error","StatusMessage":"The Subscriber was not found",
// "OrdinalID":0,"Results":null,"ErrorCode":12001, ...}. The per-item Error is a data-level
// result INSIDE the array (not a null return) — the array-of-objects shape is CONFIRMED.
// status.length = 0, status[0] = undefined, status[1] = undefined — status is NEVER populated.
Platform.Response.Write("typeof: " + (typeof results) + " length: " + (results != null ? results.length : "n/a") + "\n");
if (results != null && results.length > 0) {
Platform.Response.Write("element[0]: " + Platform.Function.Stringify(results[0]).substring(0,260) + "\n");
}
Platform.Response.Write("status.length: " + (status != null ? status.length : "n/a") + " status[0]: " + status[0] + " status[1]: " + status[1] + "\n");
} catch (e) { Platform.Response.Write("two-arg ERROR: " + Platform.Function.Stringify(e).substring(0,180) + "\n"); }
try {
var status2 = [];
var apiObject2 = Platform.Function.CreateObject("ExecuteRequest");
// Third argument — EXPECTED to throw "Unable to retrieve security descriptor for this frame".
Platform.Function.InvokeExecute(apiObject2, status2, {});
Platform.Response.Write("three-arg: unexpectedly OK\n");
} catch (e) {
// OBSERVED: the THREE-arg call threw exactly "Unable to retrieve security descriptor for this
// frame." — CONFIRMING the arity claim (only two args are accepted; a third is rejected).
Platform.Response.Write("three-arg ERROR (expected): " + Platform.Function.Stringify(e).substring(0,180) + "\n");
}
</script>
Platform.Function.InvokeExtract — undocumented arity, inert status array, return string unproven at runtime
The official docs are wrong on two counts. Signature: they list a third options argument, but at runtime the function accepts only two arguments (apiObject, statusArray) — passing a third throws an “Unable to retrieve security descriptor for this frame” error. Return: they type the return value as an object; the docs (and the lookup_ssjs_function catalog) describe the return as the OverallStatus message as a string — but that string was never observed at runtime from a CloudPage GET, so the string claim remains unproven.
An ExtractRequest CLR object exposes only two writable top-level properties — Parameters (an ExtractParameter[], each item having Name and Value) and Options (an ExtractOptions object). Intuitive names such as Name, CustomerKey, RequestID, Fields, and ExtractType are not valid on ExtractRequest and throw “Invalid property name” on SetObjectProperty. The definition-reference has to be carried inside a Parameters entry, so the call was exercised against the BU’s real, pre-configured Data Extract definitions (test-ssjs, copado_dataExtract, copado-extract-gGustavo, copado-extract-golem, copado-extract), referenced by every plausible parameter-name convention (CustomerKey, Name, DataExtractDefinitionId, DataExtractDefinitionCustomerKey, ExtractDefinitionCustomerKey, DataExtractDefinition, DefinitionKey, ExtractType), plus an empty request and an Options-carrying request.
The reproducible outcome for every one of these shapes is a catchable System.NullReferenceException thrown from ExactTarget.Integration.Framework — not the earlier-reported HTTP 422 page abort. The page renders fully, output before and after the call survives, and the surrounding try/catch runs. Because the SOAP Extract verb resolves a saved definition by its internal ExtractType GUID (not by a name/CustomerKey string), passing the definition’s key as a Parameters value never binds to a real definition, and the framework NREs internally. The statusArray is inert: it stays at its initial values (status[0]/status[1] unchanged, no status message or RequestID written) exactly like the InvokeRetrieve/InvokeExecute siblings. A single early run once returned typeof "object" / value null (non-reproducible), which — if anything — contradicts the documented string return rather than confirming it. Confirmed: two-argument arity, third-arg rejection, inert status array, catchable failure (no 422). Unproven: the OverallStatus string return — never reached from a CloudPage because a genuine extract requires the Automation/activity execution context, not an inline SSJS invoke.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeExtract accepts ONLY two args (apiObject, statusArray); a third throws
// "Unable to retrieve security descriptor for this frame" (confirms the arity claim).
//
// ExtractRequest exposes only Parameters (ExtractParameter[] of {Name,Value}) and Options
// (ExtractOptions). Name / CustomerKey / RequestID / Fields / ExtractType are INVALID directly
// on ExtractRequest (SetObjectProperty throws "Invalid property name").
//
// Referencing a REAL saved Data Extract definition by CustomerKey via a Parameters entry does
// NOT 422 the page (contrary to an earlier report) — it throws a CATCHABLE
// System.NullReferenceException, and the statusArray stays INERT (never populated). The
// documented OverallStatus STRING return was never observed here: a genuine extract binds a
// saved definition by its internal ExtractType GUID inside the Automation/activity runtime,
// which cannot be reproduced from an inline CloudPage SSJS invoke.
function line(label, value) {
Platform.Response.Write(label + ": " + value + "\n");
}
// (1) Arity: a third argument is rejected (catchable).
try {
var s3 = [];
var r3 = Platform.Function.CreateObject("ExtractRequest");
Platform.Function.InvokeExtract(r3, s3, {});
line("three-arg", "unexpectedly OK");
} catch (e3) {
line("three-arg ERROR (expected)", ("" + e3.message).substring(0, 120));
}
// (2) Reference a real definition ("test-ssjs") by CustomerKey via a Parameters entry.
var status = [0, 0];
try {
var req = Platform.Function.CreateObject("ExtractRequest");
var p = Platform.Function.CreateObject("ExtractParameter");
Platform.Function.SetObjectProperty(p, "Name", "CustomerKey");
Platform.Function.SetObjectProperty(p, "Value", "test-ssjs");
Platform.Function.AddObjectArrayItem(req, "Parameters", p);
var ret = Platform.Function.InvokeExtract(req, status);
// If reached: OBSERVED once as typeof "object" / null (non-reproducible), status still [0,0].
line("typeof/return", typeof ret + " / " + ("" + ret).substring(0, 120));
line("status.len/0/1", status.length + " / " + status[0] + " / " + status[1]);
} catch (e) {
// REPRODUCIBLE: catchable System.NullReferenceException from ExactTarget.Integration.Framework.
line("invoke THREW (catchable, NOT a 422)", ("" + e.message).substring(0, 120));
line("status stayed inert", status.length + " / " + status[0] + " / " + status[1]);
}
</script>
Platform.Function.InvokeSchedule — options argument optional (docs mark it required), returns the OverallStatus string
The official docs are wrong on two counts. Signature: they mark the trailing options argument as required (five required arguments), but at runtime it is optional — a four-argument call (apiObject, action, schedule, statusArray) reaches the function body and returns normally instead of raising an arity error. Return: they type the return value as an object, but at runtime the call returns the OverallStatus message as a string (typeof "string"); the OverallStatus message is written into statusArray[0] and a status code into statusArray[1]. Passing a sixth argument (one more than documented) throws “Unable to retrieve security descriptor for this frame.”, so the accepted arity range is four-to-five arguments.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// InvokeSchedule: trailing options arg is OPTIONAL (4-arg call succeeds). Returns OverallStatus STRING.
// API OPERATION: requires a valid schedule definition + permissions. Empty objects here still let the
// function evaluate and RETURN (a string) rather than throw an arity error, which is what proves the claim.
// OBSERVED (CloudPage GET): 4-arg call returned typeof "string" (value "Error") with the
// OverallStatus message in status[0] ("Exception occurred during [ScheduleProgram] ErrorID: ...") and
// status[1] = "2" — i.e. NO unknown-member/wrong-arity throw, so the trailing options arg IS optional and
// the return is a STRING. The 5-arg call (options = null) behaved identically (typeof "string").
// A 6-arg over-arity call threw "Unable to retrieve security descriptor for this frame." — CONFIRMED.
try {
var status = [0,0,0];
var apiObject = Platform.Function.CreateObject("Automation");
var schedule = Platform.Function.CreateObject("ScheduleDefinition");
// Four-argument call: apiObject, action, schedule, statusArray (no options).
var result = Platform.Function.InvokeSchedule(apiObject, "start", schedule, status);
Platform.Response.Write("4-arg return typeof: " + (typeof result) + "\n");
Platform.Response.Write("result: " + result + "\n");
Platform.Response.Write("status[0] (OverallStatus): " + status[0] + "\n");
Platform.Response.Write("status[1] (status code): " + status[1] + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.LocalDateToSystemDate — returns a Date object, not a string
The official docs type the return value as a string, but at runtime the call returns a genuine Date object (typeof "object", Object.prototype.toString reports [object Date], and getFullYear() / getHours() / getTime() all work). It only serializes to an ISO-like string (e.g. 2025-08-05T04:00:00.000) when written or passed through Stringify(). The conversion strips daylight saving, so the same wall-clock local input yields a system hour one hour earlier in summer than in winter.
Core-library equivalent: DateTime.LocalDateToSystemDate() is the short-form Core alias (available after Platform.Load("core", ...)) and returns the same genuine Date object.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage): returns a genuine Date object, NOT a string. Now()-based call ->
// OBSERVED: typeof "object"; toString tag "[object Date]"; getFullYear()=2026, getHours()=13,
// OBSERVED: getTime()=1784487862408 all work; only serializes via Stringify -> "2026-07-19T13:04:22.408".
// OBSERVED: DST stripped - summer local noon (Jul 15 12:00) -> system "2025-07-15T04:00:00.000" (hour 4),
// OBSERVED: winter local noon (Jan 15 12:00) -> system "2025-01-15T05:00:00.000" (hour 5); system hour is
// OBSERVED: one hour earlier in summer than winter for the same wall-clock input. CLAIM CONFIRMED.
try {
var d = Platform.Function.LocalDateToSystemDate(Platform.Function.Now());
// typeof "object"; toString reports [object Date]; getFullYear/getHours/getTime all work.
Platform.Response.Write("typeof: " + (typeof d) + "\n");
Platform.Response.Write("toString tag: " + Object.prototype.toString.call(d) + "\n");
Platform.Response.Write("getFullYear(): " + d.getFullYear() + "\n");
Platform.Response.Write("getHours(): " + d.getHours() + "\n");
// Only serializes to an ISO-like string when passed through Stringify().
Platform.Response.Write("Stringify: " + Platform.Function.Stringify(d) + "\n");
// DST stripping: convert a summer and a winter local wall-clock noon.
var summerLocal = new Date(2025, 6, 15, 12, 0, 0);
var winterLocal = new Date(2025, 0, 15, 12, 0, 0);
Platform.Response.Write("summer -> " + Platform.Function.Stringify(Platform.Function.LocalDateToSystemDate(summerLocal)) + "\n");
Platform.Response.Write("winter -> " + Platform.Function.Stringify(Platform.Function.LocalDateToSystemDate(winterLocal)) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.SystemDateToLocalDate — returns a Date object, not a string
The official docs type the return value as a string, but at runtime the call returns a genuine Date object (typeof "object", Object.prototype.toString reports [object Date], and getFullYear() / getHours() / getTime() all work) — symmetric with its sibling LocalDateToSystemDate. It only serializes to an ISO-like string when written or passed through Stringify(). The conversion shifts system (Central) time to the account/user local offset — the opposite direction to LocalDateToSystemDate — and is daylight-saving aware (a fixed summer system date shifts by a larger offset than a winter one). A July system noon converts to local 20:00 (+8h) while a January system noon converts to local 19:00 (+7h), and the value round-trips back via LocalDateToSystemDate.
Core-library equivalent: DateTime.SystemDateToLocalDate() is the short-form Core alias (available after Platform.Load("core", ...)) and returns the same genuine Date object.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage): CONFIRMED - returns a genuine Date object (typeof "object", [object Date],
// getFullYear/getHours/getTime all work); serializes to ISO string only via Stringify; shifts system->local
// offset (Now() system hour 21 -> local hour 5, round-trips back via LocalDateToSystemDate) and is DST-aware
// (summer +8h, winter +7h). Docs typing it as a string are wrong.
try {
var d = Platform.Function.SystemDateToLocalDate(Platform.Function.Now());
Platform.Response.Write("B1 typeof: " + (typeof d) + "\n");
Platform.Response.Write("B1 toString tag: " + Object.prototype.toString.call(d) + "\n");
Platform.Response.Write("B1 getFullYear(): " + d.getFullYear() + "\n");
Platform.Response.Write("B1 getHours(): " + d.getHours() + "\n");
Platform.Response.Write("B1 getTime(): " + d.getTime() + "\n");
Platform.Response.Write("B1 Stringify: " + Platform.Function.Stringify(d) + "\n");
} catch (e) { Platform.Response.Write("B1 ERROR: " + Platform.Function.Stringify(e) + "\n"); }
try {
// Direction: SystemDateToLocalDate is the inverse of LocalDateToSystemDate.
var nowSystem = Platform.Function.Now();
var local = Platform.Function.SystemDateToLocalDate(nowSystem);
var backToSystem = Platform.Function.LocalDateToSystemDate(local);
Platform.Response.Write("B2 nowSystem getHours(): " + nowSystem.getHours() + "\n");
Platform.Response.Write("B2 local getHours(): " + local.getHours() + "\n");
Platform.Response.Write("B2 roundtrip system getHours(): " + backToSystem.getHours() + "\n");
} catch (e) { Platform.Response.Write("B2 ERROR: " + Platform.Function.Stringify(e) + "\n"); }
try {
// Seasonal (DST-aware) system->local offset on fixed dates.
var summerSys = new Date(2025, 6, 15, 12, 0, 0);
var winterSys = new Date(2025, 0, 15, 12, 0, 0);
Platform.Response.Write("B3 summer local getHours(): " + Platform.Function.SystemDateToLocalDate(summerSys).getHours() + "\n");
Platform.Response.Write("B3 winter local getHours(): " + Platform.Function.SystemDateToLocalDate(winterSys).getHours() + "\n");
} catch (e) { Platform.Response.Write("B3 ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Now — returns a Date object; String, Write, and Stringify use different renderings
The official reference types the result as an RFC 2822 date-time string. The runtime instead returns a genuine Date object with working getters and epoch access. A captured value is a stable snapshot, while separate calls can advance. Serialize deliberately: String() or concatenation gives an RFC-like value, Platform.Response.Write() uses the account’s locale-style representation, and Stringify() gives a quoted ISO-like value. Raw string methods and toISOString() are unavailable.
The optional useContextTime argument is accepted. On a CloudPage, which has no triggering send or activity timestamp, Now(true) remains close to the current request clock; that control does not replace testing inside a triggered context.
Core-library equivalent: Now() is the bare-name Core form (available after Platform.Load("core", ...)) and returns the same genuine Date object.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs claim: the official docs type Now() as an RFC 2822
* string, but the runtime returns a Date object that only becomes text when
* explicitly coerced or written.
*
* Salesforce docs: a date-time string is returned.
* SFMC Jint: a Date object is returned; String() is RFC-like,
* Platform.Response.Write() is locale-style, and
* Stringify() is quoted ISO-like.
*
* Proves both halves of the claim:
* 1. DEV — the raw value has Date identity and Date methods, while direct
* string methods throw.
* 2. The recommended explicit serialization workarounds produce strings.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
var result = Platform.Function.Now();
var stringForm = String(result);
var stringified = String(Stringify(result));
/* 1. The raw result is a Date, not a string. */
assert("DEV typeof Now() is object (official docs: string)", String(typeof result), "object");
assert("DEV the raw object tag is [object Date] (official docs: string)", String(Object.prototype.toString.call(result)), "[object Date]");
assert("DEV .constructor === Date (official docs: string)", result.constructor === Date ? "true" : "false", "true");
assert("DEV getTime() works on the raw return (official docs: string)", String(typeof result.getTime()), "number");
assertThrows("DEV .indexOf() throws on the raw return (official docs: string)", function () {
return result.indexOf(String(result.getFullYear()));
});
/* 2. Explicit serialization produces strings in distinct forms. */
assert("workaround String(result) returns a string", String(typeof stringForm), "string");
assert("workaround String(result) contains the result year", stringForm.indexOf(String(result.getFullYear())) >= 0 ? "true" : "false", "true");
assert("workaround Stringify(result) returns a string", String(typeof stringified), "string");
assert("workaround Stringify(result) has an ISO-like T separator", stringified.substring(11, 12), "T");
assert("String() and Stringify() are distinct serialization forms", stringForm === stringified ? "true" : "false", "false");
</script>
Platform.Function.ParseJSON — scalar-only argument; permissive JSON-like grammar and broader returns
The official signature and return type are incomplete:
- The docs type the argument as
string|string[], but an array — or any non-string object — throwsSystem.InvalidOperationException(“Unable to retrieve security descriptor for this frame.”). The function accepts exactly one scalar argument: a number matches the equivalent numeric string; a boolean is accepted but returns the CLR strings"True"/"False"(not JSON boolean primitives and not equal toParseJSON("true")/"false");nullandundefinedreturn null. - The documented
object|object[]return omits two runtime outcomes. A top-level scalar JSON value ('"hello"','42','true','null') is returned unchanged as a string. Empty, whitespace-only, null-argument, undefined-argument, and malformed structural input such as"{not json"return genuine JSnullwithout throwing. - The parser is more permissive than strict JSON. It accepts trailing content after an object, trailing commas, single-quoted keys, and unquoted keys. A leading Unicode BOM is not skipped and leaves the input as a string. Do not use successful
ParseJSONoutput as proof that source text is standards-compliant JSON.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Platform.Function.ParseJSON differs from the official signature and
* return type, and accepts several non-standard JSON extensions.
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
assert("object JSON is deserialised", String(Platform.Function.ParseJSON('{"a":1}').a), "1");
assertThrows("DEV string[] throws (official docs: accepted)", function () {
return Platform.Function.ParseJSON(["a", "b"]);
});
assert("DEV number argument is coerced (official docs: string|string[])", String(Platform.Function.ParseJSON(42)), "42");
assert("DEV boolean true returns CLR True (official docs: string|string[])", String(Platform.Function.ParseJSON(true)), "True");
assert("DEV boolean false returns CLR False (official docs: string|string[])", String(Platform.Function.ParseJSON(false)), "False");
assert("DEV scalar JSON returns string (official docs: object|object[])", String(typeof Platform.Function.ParseJSON("42")), "string");
assert("DEV malformed structural input returns null", Platform.Function.ParseJSON("{not json") === null ? "true" : "false", "true");
assert("DEV trailing content is accepted (strict JSON: syntax error)", String(Platform.Function.ParseJSON('{"a":1} trailing').a), "1");
assert("DEV trailing comma is accepted (strict JSON: syntax error)", String(Platform.Function.ParseJSON('[1,2,]').length), "2");
assert("DEV single quotes are accepted (strict JSON: syntax error)", String(Platform.Function.ParseJSON("{'a':1}").a), "1");
assert("DEV unquoted keys are accepted (strict JSON: syntax error)", String(Platform.Function.ParseJSON('{a:1}').a), "1");
</script>
Platform.Function.RedirectTo — returns the URL string, does not redirect from SSJS
The official docs present RedirectTo as an email link-target helper that returns no value (@returns {void}). At runtime from SSJS it returns the passed-in URL as a string (not void); execution continues after the call and no HTTP redirect is issued in a CloudPage context. The string is returned verbatim with no validation — an empty string returns "" and a non-URL string is returned unchanged, neither throwing nor redirecting. For CloudPage HTTP redirects use Platform.Response.Redirect instead.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// RedirectTo returns the passed-in URL as a STRING on a CloudPage; no HTTP redirect is issued.
Platform.Response.Write("before RedirectTo\n");
try {
var url = "https://example.com/next";
var ret = Platform.Function.RedirectTo(url);
// typeof "string", ret === the passed URL; execution continues (no redirect on CloudPage).
Platform.Response.Write("return typeof: " + (typeof ret) + "\n");
Platform.Response.Write("ret === url: " + (ret === url) + "\n");
Platform.Response.Write("after RedirectTo: execution continued\n");
// NOTE: for a real CloudPage HTTP redirect use Platform.Response.Redirect instead.
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// Edge cases: empty string and non-URL string are returned verbatim (no throw, no redirect).
try { var e1 = Platform.Function.RedirectTo(""); Platform.Response.Write("empty: typeof=" + (typeof e1) + " isEmpty=" + (e1 === "") + "\n"); } catch (ex1) { Platform.Response.Write("empty THREW -> " + ex1.message + "\n"); }
try { var e2 = Platform.Function.RedirectTo("not a url at all"); Platform.Response.Write("nonurl: typeof=" + (typeof e2) + " val=[" + e2 + "]\n"); } catch (ex2) { Platform.Response.Write("nonurl THREW -> " + ex2.message + "\n"); }
// OBSERVED (live 2026-07-20): RedirectTo("https://example.com/next") returned typeof "string" with ret === url (verbatim); execution continued with NO HTTP redirect on the CloudPage; empty string returned "" and non-URL string returned unchanged, neither throwing nor redirecting. CLAIM CONFIRMED (docs say @returns {void}; runtime returns the URL string and does not redirect from SSJS).
</script>
Platform.Function.Stringify — NaN and both infinities serialize wrongly, with the infinity signs inverted
The official page promises a “JSON string value” and says the function “works only with known JSON objects and types”, without naming a single failure mode. Runtime verification shows the .NET serializer behind it produces output that is silently wrong, and in some cases not valid JSON at all.
NaNand both infinities are corrupted.NaNserializes to the single characterU+221E(the infinity sign). Worse, the infinity signs are inverted: positiveInfinityserializes to-∞and negativeInfinityto∞.JSON.stringifyemitsnullfor all three. Confirmed via two independent constructions (1/0andNumber.POSITIVE_INFINITY), and the corruption survives inside an object, so a single strayNaNsilently poisons a whole payload. Guard the value before serializing.- Two cases emit invalid JSON. A control character such as
U+0001is written raw where the spec requires\u0001, and a double quote inside a key is not escaped —{ 'a"b': 1 }serializes to{"a"b":1}. - Array elements are separated by a comma plus CRLF, so array output spans multiple lines and is never byte-identical to a compact serializer. Object output is compact. This matters whenever a payload is compared, hashed, or signed.
- Numbers use the .NET round-trip exponent form, not the JavaScript form —
9.00719925474099E+15,6.02E+23,1E-07. That text is not valid JSON number syntax, soParseJSONhands it back as a string rather than a number. - Non-serializable members are not omitted. An
undefinedproperty becomesnull(JSON.stringifydrops it), and a function-valued member becomes the string"function"in both objects and arrays. - Argument handling differs between the two forms. The qualified
Platform.Function.Stringifythrows on zero arguments and on a second argument, while the bare-name CoreStringifyreturns"null"for zero arguments and silently ignores an extra one. Neither form has areplacerorspaceparameter.
A circular reference does not throw — the repeated node is emitted as null.
Show test script
<script runat="server">
// Platform.Function.Stringify emits corrupted NaN/Infinity and, in two cases, invalid JSON.
// EXPECTED OUTPUT: every line starts with PASS.
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function codes(s) {
var out = [];
for (var i = 0; i < s.length; i++) { out.push(s.charCodeAt(i)); }
return out.join(" ");
}
assert("DEV NaN serializes to U+221E (JSON.stringify: null)", codes(Platform.Function.Stringify(0 / 0)), "8734");
assert("DEV positive Infinity gets a MINUS sign (JSON.stringify: null)", codes(Platform.Function.Stringify(1 / 0)), "45 8734");
assert("DEV negative Infinity gets NO sign (JSON.stringify: null)", codes(Platform.Function.Stringify(0 - 1 / 0)), "8734");
assert("DEV Number.POSITIVE_INFINITY reproduces the inversion", codes(Platform.Function.Stringify(Number.POSITIVE_INFINITY)), "45 8734");
assert("DEV a control character is emitted raw (JSON spec: \\u0001)", codes(Platform.Function.Stringify(String.fromCharCode(1))), "34 1 34");
assert("DEV a quote inside a KEY is not escaped", codes(Platform.Function.Stringify({ 'a\"b': 1 })), "123 34 97 34 98 34 58 49 125");
assert("DEV array elements are separated by comma+CRLF (compact JSON: comma)", codes(Platform.Function.Stringify([1, 2])), "91 49 44 13 10 50 93");
assert("object output is compact by contrast", Platform.Function.Stringify({ a: 1, b: 2 }), '{\"a\":1,\"b\":2}');
assert("DEV a large integer uses the .NET exponent form (JS: 9007199254740991)", Platform.Function.Stringify(9007199254740991), "9.00719925474099E+15");
assert("DEV that exponent text does not round trip as a number", String(typeof Platform.Function.ParseJSON(Platform.Function.Stringify(6.02e23))), "string");
var undef;
assert("DEV an undefined property becomes null (JSON.stringify: omitted)", Platform.Function.Stringify({ a: 1, b: undef }), '{\"a\":1,\"b\":null}');
assert("DEV a function property becomes the string function", Platform.Function.Stringify({ a: 1, b: function () { return 1; } }), '{\"a\":1,\"b\":\"function\"}');
var circA = { name: "a" };
circA.self = circA;
assert("a circular reference does not throw; the repeat becomes null", Platform.Function.Stringify(circA), '{\"name\":\"a\",\"self\":null}');
// OBSERVED (live 2026-07-31): every line PASS on the QA CloudPage.
</script>
Platform.Request.GetUserLanguages — documented but not defined at runtime
Although listed in the official docs, this method is not defined at runtime. Calling it throws “Unable to retrieve security descriptor for this frame.” (System.InvalidOperationException from mscorlib) — the generic error the SSJS engine raises for a function it does not recognize at runtime. Setting an Accept-Language request header does not help: it throws regardless of the header. There is no argument count or calling form that makes it return a value; wrap any use in try/catch or avoid it entirely.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage GET, both with and without an Accept-Language header):
// Platform.Request.GetUserLanguages() THREW
// "Unable to retrieve security descriptor for this frame."
// (System.InvalidOperationException - from mscorlib). It is not defined at runtime.
// GetUserLanguages is not defined at runtime — calling it always throws. Wrap in try/catch or avoid.
try {
var langs = Platform.Request.GetUserLanguages();
// NOT reached — the method is not defined at runtime and throws before returning.
Platform.Response.Write("languages typeof: " + (typeof langs) + "\n");
Platform.Response.Write("languages: " + Platform.Function.Stringify(langs) + "\n");
} catch (e) {
// OBSERVED: "Unable to retrieve security descriptor for this frame."
Platform.Response.Write("ERROR (expected): " + Platform.Function.Stringify(e) + "\n");
}
</script>
HTTP.Post — header arrays optional; StatusCode is a number, Response is an array
The official docs are wrong on two counts:
- They list
headerNamesandheaderValuesas required, but at runtime a three-argument call (url,contentType,payload) works — the two header arrays are optional and only need to be paired when supplied. - They type
StatusCodeas a string andResponseas a single string, but at runtime the call returns an object whoseStatusCodeis a number and whoseResponseis an array — the body isResponse[0]. (Note the field names differ fromHTTP.Get, which returns{ Status, Content }.)
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage POST to postman-echo.com): B1 3-arg call (headers omitted) succeeded StatusCode 200; return keys = [StatusCode,Response]; typeof StatusCode = "number"; Response is an ARRAY ([object Array], length 1) whose Response[0] is the string body; B2 5-arg call echoed the supplied custom header back (present=true). Claim CONFIRMED: header arrays optional; StatusCode number; Response array.
var url = "https://postman-echo.com/post";
// B1: three-argument HTTP.Post(url, contentType, payload) - header arrays OMITTED.
try {
var r = HTTP.Post(url, "application/json", Platform.Function.Stringify({ hello: "world" }));
var keys = "";
for (var k in r) { keys += k + ","; }
Platform.Response.Write("B1 keys = [" + keys + "]\n");
Platform.Response.Write("B1 typeof StatusCode = " + (typeof r.StatusCode) + " StatusCode = " + r.StatusCode + "\n");
Platform.Response.Write("B1 Response toString = [" + Object.prototype.toString.call(r.Response) + "] length = " + r.Response.length + "\n");
Platform.Response.Write("B1 typeof Response[0] = " + (typeof r.Response[0]) + "\n");
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: five-argument HTTP.Post(url, contentType, payload, headerNames[], headerValues[]) - headers consumed when supplied.
try {
var r2 = HTTP.Post(url, "application/json", Platform.Function.Stringify({ ping: "pong" }), ["X-Custom-Probe"], ["idx40-value"]);
Platform.Response.Write("B2 StatusCode = " + r2.StatusCode + " echoed custom header present = " + (("" + r2.Response[0]).indexOf("idx40-value") >= 0) + "\n");
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.postData — write-only — every read throws
The official docs list postData among the configuration properties of the request handler, alongside readable properties such as method and contentType. At runtime the property has no getter: assignment works normally and the body does reach the server, but every read — including one immediately after assigning it — throws “Property Get method was not found.” Because that throw is not catchable at the top level of a CloudPage, an unguarded read aborts the whole page. Keep the request body in your own variable if you need to read it back.
Show test script
<script runat="server">
/*
* Chapter: HttpRequestInstance Properties
*
* Proves, for each documented property, its DEFAULT value and whether it is
* writable:
* 1. method default "GET", writable, accepts POST/PUT/PATCH/DELETE
* 2. contentType default "" (empty), writable
* 3. encoding writable; an assigned value reads back LOWER-CASED
* 4. timeout default 30, writable
* 5. emptyContentHandling default 0, accepts 1 and 2
* 6. retries default 1, writable
* 7. continueOnError default false, writable
*
* DEVIATIONS from the official Salesforce docs, each marked "DEV":
* - encoding defaults to "Windows-1252", not the documented "UTF-8".
* - postData is WRITE-ONLY: every read throws "Property Get method was
* not found.", although the docs list it as a readable property.
* - timeout is not documented at all; it exists and defaults to 30,
* matching the documented 30-second send() timeout, so the unit is
* SECONDS — not milliseconds.
* - emptyContentHandling is documented as a boolean but is numeric at
* runtime: true/false are rejected outright.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
var got;
try { got = "" + actual; } catch (ex) { got = "THREW: " + ("" + ex.message); }
Platform.Response.Write((got === "" + expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
function assertThrowsFragment(id, fn, fragment) {
var msg = "";
try { fn(); msg = "did NOT throw"; } catch (ex) { msg = "" + ex.message; }
Platform.Response.Write((msg.indexOf(fragment) > -1 ? "PASS " : "FAIL ") + id + " -> " + msg + "\n");
}
var req = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
/* 1. method — default and writability. */
assert("method default is GET", req.method, "GET");
req.method = "POST";
assert("method accepts POST", req.method, "POST");
req.method = "PUT";
assert("method accepts PUT", req.method, "PUT");
req.method = "PATCH";
assert("method accepts PATCH", req.method, "PATCH");
req.method = "DELETE";
assert("method accepts DELETE", req.method, "DELETE");
req.method = "GET";
/* 2. contentType — default empty, writable. */
var fresh = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("contentType default is empty", fresh.contentType, "");
fresh.contentType = "application/json";
assert("contentType is writable", fresh.contentType, "application/json");
/* 3. DEVIATION — encoding default is Windows-1252, not the documented UTF-8. */
var encReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("DEV encoding default is Windows-1252 (docs: UTF-8)", encReq.encoding, "Windows-1252");
encReq.encoding = "UTF-8";
assert("DEV assigned encoding reads back lower-cased (docs: UTF-8)", encReq.encoding, "utf-8");
/* 4. DEVIATION — timeout is undocumented; default 30 proves the unit is seconds. */
var toReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("DEV timeout default is 30 seconds (docs: property not listed)", toReq.timeout, "30");
toReq.timeout = 45;
assert("timeout is writable", toReq.timeout, "45");
/* 5. DEVIATION — postData is write-only: assignment works, every read throws. */
var pdReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assertThrowsFragment("DEV reading postData before assignment throws (docs: readable property)", function () {
return pdReq.postData;
}, "Property Get method was not found.");
pdReq.postData = "hello-body";
assertThrowsFragment("DEV reading postData after assignment still throws (docs: readable property)", function () {
return pdReq.postData;
}, "Property Get method was not found.");
/* 6. DEVIATION — emptyContentHandling is numeric, not the documented boolean. */
var ecReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("emptyContentHandling default is 0", ecReq.emptyContentHandling, "0");
ecReq.emptyContentHandling = 1;
assert("emptyContentHandling accepts 1 (stop)", ecReq.emptyContentHandling, "1");
ecReq.emptyContentHandling = 2;
assert("emptyContentHandling accepts 2 (next subscriber)", ecReq.emptyContentHandling, "2");
assertThrowsFragment("DEV emptyContentHandling rejects true (docs: boolean)", function () {
ecReq.emptyContentHandling = true;
}, "cannot be converted");
assertThrowsFragment("DEV emptyContentHandling rejects false (docs: boolean)", function () {
ecReq.emptyContentHandling = false;
}, "cannot be converted");
/* 7. retries and continueOnError — defaults and writability. */
var rReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("retries default is 1", rReq.retries, "1");
rReq.retries = 2;
assert("retries is writable", rReq.retries, "2");
assert("continueOnError default is false", rReq.continueOnError, "False");
rReq.continueOnError = true;
assert("continueOnError is writable", rReq.continueOnError, "True");
</script>
Send.Definition.AddWithFilterDefinition — always throws "Error adding EmailSendDefinition." even when the send definition was created
The docs describe this static as returning "OK" on success and throwing on failure. At runtime no invocation shape returns normally — the call always throws the raw string "Error adding EmailSendDefinition." (typeof ex === "string", so ex.message is undefined) — yet with a valid filter definition key and a real list ID the send definition is created anyway and is immediately retrievable via Send.Definition.Retrieve on the same page. Because the throw happens whether or not the create succeeded, a try/catch result tells you nothing: the only reliable success check is to Retrieve the new CustomerKey after catching. Shapes tried, all of which threw: the list ID passed as a number, the list ID passed as a single-element array, a publication list name in place of the list ID, a Data Extension key in place of the list ID, and the fifth argument omitted entirely. The sibling statics Add and AddWithDE do return (a CLR object) on the same page and with the same classification/email keys, so the throw is specific to this method.
Show test script
<script runat="server">
// OBSERVED: every AddWithFilterDefinition shape THREW the raw STRING "Error adding EmailSendDefinition.",
// but the shapes using a valid filter definition key + real list ID still CREATED the send definition
// (a follow-up Send.Definition.Retrieve returned it). Throwaway records were removed afterwards. CONFIRMED.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
// Adapt these to real, existing keys/IDs in your account.
var SC_KEY = "your_send_classification_key";
var EMAIL_KEY = "your_email_key";
var FD_KEY = "your_filter_definition_key";
var LIST_ID = 12345;
var key = "__qa_esd_fd__";
// THE CLAIM — returns "OK" on success, throws on failure. Runtime ALWAYS throws.
try {
var r = Send.Definition.AddWithFilterDefinition({ CustomerKey: key, Name: key, EmailSubject: "QA probe" }, SC_KEY, EMAIL_KEY, FD_KEY, LIST_ID);
pre("RETURNED typeof=" + (typeof r) + " String()=" + String(r)); // never reached
} catch (e1) {
pre("THREW typeof=" + (typeof e1) + " String=" + String(e1)); // string / "Error adding EmailSendDefinition."
pre("has .message? " + (typeof e1.message)); // undefined
}
// THE ONLY RELIABLE SUCCESS CHECK — retrieve the key after catching.
var rows = Send.Definition.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: key });
pre("Retrieve-after-throw length=" + (rows ? rows.length : "null")); // OBSERVED: 1 - created despite the throw
// Other shapes tried - all threw the same string.
var shapes = [[LIST_ID], "your_publication_list_name", "your_sendable_de_key", null];
for (var i = 0; i < shapes.length; i++) {
var k = "__qa_esd_fd_" + i + "__";
try {
if (shapes[i] === null) {
Send.Definition.AddWithFilterDefinition({ CustomerKey: k, Name: k, EmailSubject: "QA probe" }, SC_KEY, EMAIL_KEY, FD_KEY);
} else {
Send.Definition.AddWithFilterDefinition({ CustomerKey: k, Name: k, EmailSubject: "QA probe" }, SC_KEY, EMAIL_KEY, FD_KEY, shapes[i]);
}
pre("shape " + i + " RETURNED normally");
} catch (e2) { pre("shape " + i + " THREW String=" + String(e2)); }
var chk = Send.Definition.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: k });
pre("shape " + i + " created? " + (chk && chk.length > 0));
if (chk && chk.length > 0) { try { Send.Definition.Init(k).Remove(); } catch (e3) {} }
}
// CLEANUP
try { pre("Remove -> " + String(Send.Definition.Init(key).Remove())); } catch (e4) { pre("Remove THREW " + String(e4)); }
pre("=== END ===");
</script>
<SendDefinitionInstance>.Send / TestSend — return error text as a normal return value instead of throwing; TestSend is undocumented and had no working invocation
<SendDefinitionInstance>.Send() is documented as returning "OK", with failures surfacing as exceptions. At runtime a rejected send is returned as a multi-line error string and nothing is thrown, so code that only wraps the call in try/catch treats a rejected send as a success. Always compare the returned value to "OK". Observed returns include "An EmailSendDefinition must have an audience to be sent." when no audience is attached and "The following email validation errors need addressed before the email can be sent." followed by the offending tokens once an audience is present. A WSProxy performItem("EmailSendDefinition", …, "start") control returned the identical validation text, so the Core method dispatches the same operation. The instance also exposes an undocumented TestSend([emailAddress]) method (typeof === "function") for which no working invocation was found: with no arguments it returns "An EmailSendDefinition cannot be used in a test send to a list or group without a test email address." even after a test address was stored on the record — set both through this object’s own Update({ TestEmailAddr: … }) (returned "OK") and through a WSProxy updateItem control (returned Status: "OK", StatusMessage "EmailSendDefinition updated") — and passing an address as an argument replaces that message with the same email validation error string as Send(). Like Send(), it returns error text rather than throwing.
Show test script
<script runat="server">
// OBSERVED: Send() returned an ERROR STRING (no throw) - e.g. "An EmailSendDefinition must have an audience to be sent."
// and, once an audience was attached, "The following email validation errors need addressed before the email can be sent."
// A WSProxy performItem(..., "start") control returned the identical text. The undocumented TestSend() likewise returned
// error strings for every shape tried (no argument, and with an explicit address, before and after setting TestEmailAddr).
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
var ESD_KEY = "ssjs-send"; // adapt to a real send definition external key in your account
// THE CLAIM — Send() returns "OK" and throws on failure. Runtime RETURNS the error text.
try {
var s = Send.Definition.Init(ESD_KEY).Send();
pre("Send typeof=" + (typeof s)); // string
pre("Send String()=" + String(s)); // error text, NOT a throw
pre("Send is exactly 'OK'? " + (String(s) === "OK"));
} catch (e1) { pre("Send THREW typeof=" + (typeof e1) + " String=" + String(e1)); }
// WSProxy CONTROL — same operation, same text.
try {
var api = new Script.Util.WSProxy();
var w = api.performItem("EmailSendDefinition", { CustomerKey: ESD_KEY }, "start");
pre("WSProxy performItem Status=" + w.Status + " Message=" + (w.Results && w.Results[0] ? w.Results[0].StatusMessage : ""));
} catch (e2) { pre("WSProxy performItem THREW " + String(e2)); }
// UNDOCUMENTED TestSend — exists, but no working invocation found.
var esd = Send.Definition.Init(ESD_KEY);
pre("typeof TestSend=" + (typeof esd.TestSend)); // function
try { pre("TestSend() -> " + String(esd.TestSend())); } catch (e3) { pre("TestSend() THREW " + String(e3)); }
// OBSERVED: "An EmailSendDefinition cannot be used in a test send to a list or group without a test email address."
try { pre("Update TestEmailAddr -> " + String(Send.Definition.Init(ESD_KEY).Update({ TestEmailAddr: "test@example.com" }))); } catch (e4) { pre("Update THREW " + String(e4)); }
try { pre("TestSend() after Update -> " + String(Send.Definition.Init(ESD_KEY).TestSend())); } catch (e5) { pre("TestSend() THREW " + String(e5)); }
// OBSERVED: same "without a test email address" string
try { pre("TestSend(addr) -> " + String(Send.Definition.Init(ESD_KEY).TestSend("test@example.com"))); } catch (e6) { pre("TestSend(addr) THREW " + String(e6)); }
// OBSERVED: the same email validation error string returned by Send()
pre("=== END ===");
</script>
<DataExtensionInstance>.Rows.Retrieve / Lookup — string vs typed values; Lookup Date is ISO-8601; empty-array vs null; Retrieve works on CloudPages
<DataExtensionInstance>.Rows.Retrieve()without a filter DOES work on CloudPages and returns a host array — the widely-repeated “returns empty on CloudPages” bug does not occur. (When the DE is empty it returns a length-0 host array, not a throw.)Retrievereturns every field value as a string (even Number/Boolean/Date columns);Lookupreturns typed values — except Date columns, whichLookupreturns as an ISO-8601 string (e.g."2024-01-15T00:00:00.000"), not aDateobject.- On no match,
Retrieve(called with aSimpleFilterobject) returns an empty array (.length === 0) whileLookupreturnsnull(=== nulltrue,[object Object], no.length). Passing a positional(field, op, value)filter toRetrieveinstead throws “There was an error retrieving the rows.” — use theSimpleFilterobject form. - Both
RetrieveandLookup(match) results are host arrays:Object.prototype.toStringreports[object Array]and.length/ index access work, butinstanceof Arrayisfalse.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage, DE SSJSGUIDE_VERIFY empty at test time):
// Retrieve() no-filter -> [object Array], length 0, instanceof Array=false, did NOT throw (claim 1 & 4 CONFIRMED)
// Retrieve(SimpleFilter no-match) -> [object Array], length 0 (claim 3a CONFIRMED)
// Retrieve('Pk','=',val) positional -> THREW "There was an error retrieving the rows." (use SimpleFilter form)
// Lookup(['Pk'],[no-match]) -> === null true, [object Object], length undefined (claim 3b CONFIRMED)
// Claim 2 (Retrieve=string / Lookup=typed) NOT re-exercised: DE empty, read-only run (no rows written)
function pre(t) { Platform.Response.Write(t + "\n"); }
function dump(desc, v) {
pre(desc + " typeof=" + (typeof v));
try { pre(desc + " toStringTag=" + Object.prototype.toString.call(v)); } catch (x1) {}
try { pre(desc + " length=" + v.length); } catch (x2) {}
try { pre(desc + " === null? " + (v === null)); } catch (x3) {}
try { pre(desc + " instanceof Array? " + (v instanceof Array)); } catch (x4) {}
}
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
// Claim 1 + 4: Retrieve() no filter works on CloudPages, host array.
try { dump("Retrieve()", de.Rows.Retrieve()); } catch (e1) { pre("Retrieve() THREW: " + e1); }
// Claim 3a: no-match Retrieve via SimpleFilter -> empty array (NOT a throw).
try { dump("Retrieve(simpleFilter no-match)", de.Rows.Retrieve({ Property: "Pk", SimpleOperator: "equals", Value: "__no_such_pk_zzz__" })); } catch (e2) { pre("Retrieve(filter) THREW: " + e2); }
// Claim 3b: no-match Lookup -> null.
try { dump("Lookup no-match", de.Rows.Lookup(["Pk"], ["__no_such_pk_zzz__"])); } catch (e3) { pre("Lookup THREW: " + e3); }
// Claim 2 (needs a populated DE): Retrieve strings vs Lookup typed values.
try {
var all = de.Rows.Retrieve();
if (all && all.length > 0) {
var f = ["Pk", "Txt", "Num", "Dec", "Flag", "Dt", "Email"], i;
for (i = 0; i < f.length; i++) { pre("Retrieve row0." + f[i] + " typeof=" + (typeof all[0][f[i]])); }
var lk = de.Rows.Lookup(["Pk"], [all[0].Pk]);
if (lk && lk.length > 0) { for (i = 0; i < f.length; i++) { pre("Lookup row0." + f[i] + " typeof=" + (typeof lk[0][f[i]])); } }
} else { pre("Claim 2 SKIP: DE empty (read-only run, no rows written)"); }
} catch (e4) { pre("Claim2 THREW: " + e4); }
</script>
<WSProxyInstance>.describe — Results elements are the ObjectDefinition itself, not a nested wrapper
The official docs show describe returning { RequestID, Results: [{ ObjectDefinition: { Properties: [...] } }] } — one ObjectDefinition wrapper per requested type. At runtime each element of Results is the object definition directly: result.Results[0] already carries ObjectType, Name, Properties, IsCreatable, etc., and there is no Results[0].ObjectDefinition key (typeof it is undefined). Read fields as result.Results[0].Properties, not result.Results[0].ObjectDefinition.Properties (for Subscriber, Properties is a 29-element array whose first entry’s Name is ID). objectType accepts a single string or an array of strings; the returned Results array element order matches the input order (describe(["List", "Subscriber"]) returns List then Subscriber). Describing an unknown object type does not throw — it returns { RequestID, Results: [null] }, so guard against a null element. The return object exposes RequestID but no Status field (typeof result.Status is undefined).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Verify <WSProxyInstance>.describe() result shape (READ-ONLY metadata retrieval).
// OBSERVED (live CloudPage): CONFIRMED. typeof api.describe = "clrmethodinfo". Single-string
// AND array forms both accepted. Results[i] IS the object definition directly:
// typeof Results[0].ObjectDefinition = "undefined"; Results[0].ObjectType = "Subscriber",
// typeof Properties = "object" (array, length 29, first prop Name = "ID"), typeof IsCreatable
// = "boolean" (Results[0].Name came back null for Subscriber, but the key exists). Unknown type
// "NoSuchObjectType_XYZ" -> Results[1] === null (no throw). Return has RequestID (true) and no
// Status field (typeof result.Status = "undefined"). Input order preserved: describe(["List",
// "Subscriber"]) -> Results[0].ObjectType "List", Results[1].ObjectType "Subscriber".
Platform.Response.Write("=== describe() verification ===\n");
// Probe A: typeof describe and single-string form
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("typeof api.describe: " + (typeof api.describe) + "\n");
var single = api.describe("Subscriber");
Platform.Response.Write("A single-string call ok; Results length: " + single.Results.length + "\n");
Platform.Response.Write("A typeof single.Results[0].ObjectDefinition: " + (typeof single.Results[0].ObjectDefinition) + "\n");
Platform.Response.Write("A single.Results[0].ObjectType: " + single.Results[0].ObjectType + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: array form with a valid and an unknown type
try {
var api2 = new Script.Util.WSProxy();
var result = api2.describe(["Subscriber", "NoSuchObjectType_XYZ"]);
Platform.Response.Write("B Results length: " + result.Results.length + "\n");
Platform.Response.Write("B typeof Results[0].ObjectDefinition: " + (typeof result.Results[0].ObjectDefinition) + "\n");
Platform.Response.Write("B Results[0].ObjectType: " + result.Results[0].ObjectType + "\n");
Platform.Response.Write("B typeof Results[0].Properties: " + (typeof result.Results[0].Properties) + "\n");
Platform.Response.Write("B typeof Results[0].IsCreatable: " + (typeof result.Results[0].IsCreatable) + "\n");
Platform.Response.Write("B Results[1] (unknown) === null? " + (result.Results[1] === null) + "\n");
Platform.Response.Write("B has RequestID: " + (typeof result.RequestID != "undefined") + "\n");
Platform.Response.Write("B typeof result.Status: " + (typeof result.Status) + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: order matches input order (reverse pair)
try {
var api3 = new Script.Util.WSProxy();
var ordered = api3.describe(["List", "Subscriber"]);
Platform.Response.Write("C Results[0].ObjectType: " + ordered.Results[0].ObjectType + "\n");
Platform.Response.Write("C Results[1].ObjectType: " + ordered.Results[1].ObjectType + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Date.parse() — returns 0 (never NaN) for invalid strings, and parses date-only strings as local
MDN specifies that Date.parse() returns NaN when the string cannot be parsed, and that date-only ISO forms ("2026-06-18") are interpreted as UTC. In the SFMC engine both differ: an unparseable or invalid string ("garbage", "", "2021-13-45") returns 0 — the Unix epoch — so isNaN() cannot detect a bad date and invalid input silently becomes 1970-01-01; and a date-only string is parsed as local midnight (getUTCHours() = 6 at GMT-06:00), not UTC. Validate input explicitly; do not rely on NaN for error detection.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof Date.parse = function. Valid strings return a Number epoch (ms):
// "2020-01-01"=1577858400000, "January 1, 2020"=1577858400000, "2020-01-01T12:30:45Z"=1577881845000.
// INVALID strings ("garbage", "", "2021-13-45") all return 0 (typeof number, isNaN=false) — NOT NaN,
// so isNaN() cannot detect bad input. Date-only "2026-06-18": getUTCHours=6 (BU GMT-06:00) / getHours=0,
// i.e. parsed as LOCAL midnight, not UTC. CONFIRMED — matches MDN divergence documented above.
Platform.Response.Write("typeof Date.parse = " + (typeof Date.parse) + "\n");
// Valid standard date strings — Number epoch ms (same as standard JS)
var valid = ["2020-01-01", "January 1, 2020", "2020-01-01T12:30:45Z"];
for (var i = 0; i < valid.length; i++) {
try {
var v = Date.parse(valid[i]);
Platform.Response.Write("parse('" + valid[i] + "') = " + v + " | typeof=" + (typeof v) + " | isNaN=" + isNaN(v) + "\n");
} catch (e) { Platform.Response.Write("THREW: " + Platform.Function.Stringify(e) + "\n"); }
}
// Invalid strings — SFMC returns 0 (epoch), NOT NaN; isNaN() cannot detect it
var bad = ["garbage", "", "2021-13-45"];
for (var j = 0; j < bad.length; j++) {
try {
var b = Date.parse(bad[j]);
Platform.Response.Write("parse('" + bad[j] + "') = " + b + " | typeof=" + (typeof b) + " | isNaN=" + isNaN(b) + "\n");
} catch (e) { Platform.Response.Write("THREW: " + Platform.Function.Stringify(e) + "\n"); }
}
// Date-only string parsed as LOCAL midnight, not UTC (getUTCHours != 0 off-UTC)
try {
var d = new Date(Date.parse("2026-06-18"));
Platform.Response.Write("date-only '2026-06-18' getUTCHours=" + d.getUTCHours() + " | getHours=" + d.getHours() + "\n");
} catch (e) { Platform.Response.Write("THREW: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Object.prototype.isPrototypeOf — exists but hangs the engine
MDN specifies isPrototypeOf as a normal Object.prototype method that returns a boolean. In the SFMC Jint engine the method is present (typeof is "function") but calling it hangs the engine — the request times out (HTTP 408) with no output. An isolated single call to Object.prototype.isPrototypeOf({}) — with an incremental “before” marker written first — produced a 408 Request Timeout and never emitted the “after” marker, while the same page with only presence checks rendered fully in ~20s. Never call it; compare obj.constructor === Ctor instead. See also Known Bugs.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Presence-only probe. Do NOT call isPrototypeOf: invoking it hangs the engine.
// OBSERVED (live CloudPage): typeof Object.prototype.isPrototypeOf = "function"; typeof ({}).isPrototypeOf = "function".
// OBSERVED: an isolated call Object.prototype.isPrototypeOf({}) wrote its "before" marker
// then never returned -> HTTP 408 Request Timeout, empty body (engine hang). Presence-only
// page (no call) rendered fully in ~20s, so the hang is caused specifically by the call.
// Probe 1: the method exists on Object.prototype
Platform.Response.Write("typeof Object.prototype.isPrototypeOf = " + (typeof Object.prototype.isPrototypeOf) + "\n");
// Probe 2: it is also inherited by plain object instances
var obj = { a: 1 };
Platform.Response.Write("typeof obj.isPrototypeOf = " + (typeof obj.isPrototypeOf) + "\n");
// Probe 3: the actual defect - calling it never returns (HTTP 408, empty body).
// DO NOT UNCOMMENT: this line is what hangs the engine.
// Platform.Response.Write("before call\n");
// Platform.Response.Write("isPrototypeOf => " + Object.prototype.isPrototypeOf(obj) + "\n");
// Platform.Response.Write("after call\n"); // never reached
Platform.Response.Write("call skipped: invoking isPrototypeOf hangs the engine\n");
</script>
RegExp.exec — capture groups broken (null), result.length always 5, index/input work
MDN specifies that exec() returns an array whose element [0] is the full match and [1]…[n] are the captured groups, with length reflecting the group count and index/input properties set. In the SFMC Jint engine exec exists (typeof "function") and result[0], result.index, and result.input are correct, and a no-match correctly returns null. However capture groups are broken — indices result[1], result[2] read back as null (typeof "object"), not the captured substrings (and Stringify(result) shows ["<match>", null, null]); indices beyond that are undefined. result.length is always 5 regardless of the number of capture groups (both a one-group and a three-group pattern report length: 5), so it cannot be used to count groups. Read capture groups by matching a non-global pattern with String.match instead.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live 2026-07-20, CloudPage): typeof exec = "function".
// Single group /(\d+)/.exec("abc123"): m[0]="123" (correct), m[1]=null (typeof object, NOT the captured "123"),
// m.index=3, m.input="abc123" (correct), m.length=5, Stringify=["123",null,null].
// Three groups /(\d{4})-(\d{2})-(\d{2})/.exec("date 2026-06-18 end"): m[0]="2026-06-18" (correct),
// m[1]=null, m[2]=null (typeof object), m[3]=undefined; m.index=5, m.input correct, m.length=5 (SAME as single group).
// No-match /(z+)/.exec("abc"): typeof "object", ===null true, Stringify=null (correct null).
// VERDICT: CONFIRMED (with corrected specifics) — capture groups are broken (read back as null, not the docs' substrings)
// and result.length is a fixed constant (5), NOT the group count (3). index/input/[0]/no-match-null are correct.
// Probe A: typeof exec on a literal regex
try {
Platform.Response.Write("A typeof (/x/).exec = " + (typeof /x/.exec) + "\n");
} catch (e) { Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// Probe B: single capture group /(\d+)/ against "abc123"
try {
var m1 = /(\d+)/.exec("abc123");
Platform.Response.Write("B m1 stringified = " + Platform.Function.Stringify(m1) + "\n");
Platform.Response.Write("B m1[0]=" + (m1 ? m1[0] : "N/A") + " m1[1]=" + (m1 ? m1[1] : "N/A") + " (typeof " + (m1 ? typeof m1[1] : "N/A") + ") index=" + (m1 ? m1.index : "N/A") + " input=" + (m1 ? m1.input : "N/A") + " length=" + (m1 ? m1.length : "N/A") + "\n");
} catch (e) { Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// Probe C: three capture groups against "date 2026-06-18 end"
try {
var m3 = /(\d{4})-(\d{2})-(\d{2})/.exec("date 2026-06-18 end");
Platform.Response.Write("C m3 stringified = " + Platform.Function.Stringify(m3) + "\n");
Platform.Response.Write("C m3[0]=" + (m3 ? m3[0] : "N/A") + " m3[1]=" + (m3 ? m3[1] : "N/A") + " m3[2]=" + (m3 ? m3[2] : "N/A") + " m3[3]=" + (m3 ? m3[3] : "N/A") + " index=" + (m3 ? m3.index : "N/A") + " length=" + (m3 ? m3.length : "N/A") + "\n");
} catch (e) { Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// Probe D: no-match returns null
try {
var mn = /(z+)/.exec("abc");
Platform.Response.Write("D no-match typeof=" + (typeof mn) + " ===null=" + (mn === null) + "\n");
} catch (e) { Platform.Response.Write("D THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
RegExp lastIndex — never advances and manual assignment is ignored
MDN specifies that with the g (or y) flag, exec() and test() update lastIndex so successive calls advance through the string, and that assigning lastIndex repositions the next search. In the SFMC Jint engine lastIndex stays 0 after exec() with the g flag — it never advances, so successive exec() calls all re-match from the start. Manually assigning lastIndex does store the value (a write of 4 reads back as 4), but exec() ignores it for positioning — the next exec() still matches from index 0 and leaves the assigned value untouched. The classic while ((m = re.exec(str)) !== null) loop therefore never terminates. Use String.match(/…/g) to collect all matches in one call.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof lastIndex=number, initial 0. Five bounded exec() calls on
// /\d/g against "a1b2c3" ALL returned match="1" with lastIndex=0 every iteration (never advances).
// Assigning re.lastIndex=4 read back as 4, but the next exec() still matched "1" at index 1
// (assignment ignored for positioning). String.match(/\d/g) reliably returned ["1","2","3"]. CONFIRMED.
try {
var re = /\d/g;
var s = "a1b2c3";
Platform.Response.Write("typeof lastIndex=" + (typeof re.lastIndex) + " initial=" + re.lastIndex + "\n");
// BOUNDED loop (max 5) — never unbounded; a stuck lastIndex would hang a while-exec loop
for (var i = 0; i < 5; i++) {
var m = re.exec(s);
Platform.Response.Write("iter" + i + ": lastIndex=" + re.lastIndex + " match=" + (m ? m[0] : "null") + "\n");
if (!m) { break; }
}
// Manual assignment: stored but ignored by exec() for positioning
var re2 = /\d/g;
re2.exec(s);
re2.lastIndex = 4;
Platform.Response.Write("after set 4, read back lastIndex=" + re2.lastIndex + "\n");
var second = re2.exec(s);
Platform.Response.Write("next match=" + (second ? second[0] : "null") + " index=" + (second ? second.index : "N/A") + " | lastIndex=" + re2.lastIndex + "\n");
// Reliable alternative: String.match(/.../g) collects all matches at once
Platform.Response.Write("match(/\\d/g): " + Platform.Function.Stringify(s.match(/\d/g)) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
String.search — unreliable index, no-match returns 0 not -1
MDN specifies String.prototype.search(regexp) returns the index of the first match, or -1 when there is no match. In the SFMC Jint engine it is unreliable: a no-match returns 0 instead of -1, and some real matches return the wrong index (a match at position 9 returns 10; a no-match returns 0 which is indistinguishable from a match at the start). Do not use search to detect or locate a match. Use RegExp.test to detect a match, or String.match (whose [0] is correct) to read it, or apply the search polyfill.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Search for a present and an absent pattern
try {
// EXPECTED (SFMC): no-match returns 0 (not -1), so it is indistinguishable from a start match
Platform.Response.Write("search(no match): " + "Hello".search(/zzz/) + "\n");
// EXPECTED (SFMC): real matches can return the wrong index (e.g. off by one)
Platform.Response.Write("search(/world/) in 'Hello, world': " + "Hello, world".search(/world/) + "\n");
// Reliable alternatives: RegExp.test to detect, String.match[0] to read
Platform.Response.Write("test detect: " + /world/.test("Hello, world") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
String.charAt — out-of-range index returns the last char, not ""
MDN specifies String.prototype.charAt(index) returns the empty string "" when index is negative or >= str.length. In the Jint engine a positive out-of-range index instead returns the last character of the string — "hello".charAt(5) and "hello".charAt(99) both return "o" (in-range indexes are correct: charAt(0)="h"). A negative index still returns "" per MDN (charAt(-1)===""). Two further deviations: charAt() with no argument throws "Index was outside the bounds of the array" (System.IndexOutOfRangeException) instead of MDN’s "h" (index 0), and bracket access for an out-of-range index also throws the same error ("hello"[99]) rather than returning undefined. Guard the index against .length before reading a character.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): typeof charAt=function; charAt(0)='h', charAt(1)='e', charAt(4)='o' (in-range OK); charAt(5)='o' and charAt(99)='o' (positive out-of-range returns LAST char, NOT ""); charAt(-1)='' (negative returns "" per MDN); charAt() THREW System.IndexOutOfRangeException "Index was outside the bounds of the array"; bracket[0]='h'; bracket[99] THREW the same IndexOutOfRangeException; charCodeAt(0)=104. VERDICT: CONFIRMED.
Platform.Response.Write("=== String.charAt probe ===\n");
try { Platform.Response.Write("typeof charAt: " + (typeof "hello".charAt) + "\n"); } catch (e) { Platform.Response.Write("typeof THREW: " + Platform.Function.Stringify(e) + "\n"); }
try { Platform.Response.Write("charAt(0): '" + "hello".charAt(0) + "'\n"); } catch (e) { Platform.Response.Write("charAt(0) THREW: " + Platform.Function.Stringify(e) + "\n"); }
try { Platform.Response.Write("charAt(4): '" + "hello".charAt(4) + "'\n"); } catch (e) { Platform.Response.Write("charAt(4) THREW: " + Platform.Function.Stringify(e) + "\n"); }
// positive out-of-range: SFMC returns LAST char, MDN returns ""
try { Platform.Response.Write("charAt(5): '" + "hello".charAt(5) + "'\n"); } catch (e) { Platform.Response.Write("charAt(5) THREW: " + Platform.Function.Stringify(e) + "\n"); }
try { Platform.Response.Write("charAt(99): '" + "hello".charAt(99) + "'\n"); } catch (e) { Platform.Response.Write("charAt(99) THREW: " + Platform.Function.Stringify(e) + "\n"); }
// no-arg: SFMC throws, MDN returns "h"
try { Platform.Response.Write("charAt(): '" + "hello".charAt() + "'\n"); } catch (e) { Platform.Response.Write("charAt() THREW: " + Platform.Function.Stringify(e) + "\n"); }
// negative: SFMC returns "" per MDN
try { Platform.Response.Write("charAt(-1): '" + "hello".charAt(-1) + "'\n"); } catch (e) { Platform.Response.Write("charAt(-1) THREW: " + Platform.Function.Stringify(e) + "\n"); }
// bracket out-of-range throws
try { Platform.Response.Write("bracket[0]: '" + "hello"[0] + "'\n"); } catch (e) { Platform.Response.Write("bracket[0] THREW: " + Platform.Function.Stringify(e) + "\n"); }
try { Platform.Response.Write("bracket[99]: '" + "hello"[99] + "'\n"); } catch (e) { Platform.Response.Write("bracket[99] THREW: " + Platform.Function.Stringify(e) + "\n"); }
Platform.Response.Write("=== DONE ===\n");
</script>
String() (plain-object coercion) — String({}) throws; ("" + {}) is "", not "[object Object]"
MDN specifies that coercing a plain object to a string yields "[object Object]", via either String(value) or concatenation. In the SFMC Jint engine neither form does. String({}) throws Object reference not set to an instance of an object. — the throw is catchable and does not abort the page — while ("" + {}) returns the empty string. String([]) and ("" + []) are also both "". An explicit obj.toString() call is unaffected and still returns "[object Object]", so the divergence is specific to String() and implicit coercion. To render an object, read its fields individually or serialize it with Stringify().
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage, isolated probe): String({}) THREW "Object reference not set to an instance of an object."
// — catchable, both markers printed, the page did not abort. ("" + {}) => [] (empty string), NOT "[object Object]".
// String([]) => [] and ("" + []) => []. CONFIRMED.
Platform.Response.Write("start\n");
// Probe 1: String() of a plain object throws, and the throw is catchable
try { Platform.Response.Write("String({}) = [" + String({}) + "]\n"); }
catch (e) { Platform.Response.Write("String({}) THREW -> " + e.message + "\n"); }
// Probe 2: concat coercion of a plain object yields the empty string
try { Platform.Response.Write("(\"\" + {}) = [" + ("" + {}) + "]\n"); }
catch (e) { Platform.Response.Write("(\"\" + {}) THREW -> " + e.message + "\n"); }
// Probe 3: an array coerces to the empty string in both forms
try { Platform.Response.Write("String([]) = [" + String([]) + "] | (\"\" + []) = [" + ("" + []) + "]\n"); }
catch (e) { Platform.Response.Write("array coercion THREW -> " + e.message + "\n"); }
// Probe 4: an explicit toString() call still returns the spec object tag
try { Platform.Response.Write("({}).toString() = [" + ({}).toString() + "]\n"); }
catch (e) { Platform.Response.Write("({}).toString() THREW -> " + e.message + "\n"); }
Platform.Response.Write("done\n");
</script>
Number.MIN_VALUE — MIN_VALUE is a large negative number; *_INFINITY signs swapped
MDN specifies Number.MIN_VALUE is the smallest positive representable value (5e-324) and that Number.POSITIVE_INFINITY / Number.NEGATIVE_INFINITY hold +Infinity / -Infinity. In the SFMC Jint engine the constants are defined but several are wrong: Number.MIN_VALUE reads back as a large negative number (-1.79769313486232e+308), and both *_INFINITY constants have their signs swapped (Number.POSITIVE_INFINITY < 0 is true, Number.NEGATIVE_INFINITY > 0 is true). Number.MAX_VALUE (1.79769313486232e+308) and Number.NaN are correct. Do not trust Number.MIN_VALUE or the Number.*_INFINITY constants — use literals.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): Number.MAX_VALUE=1.79769313486232e+308 (correct); Number.MIN_VALUE=-1.79769313486232e+308 (WRONG); Number.POSITIVE_INFINITY<0 => true (sign swapped); Number.NEGATIVE_INFINITY>0 => true (sign swapped). VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("Number.MIN_VALUE", function(){ return Number.MIN_VALUE; });
P("Number.POSITIVE_INFINITY <0?", function(){ return (Number.POSITIVE_INFINITY < 0); });
P("Number.NEGATIVE_INFINITY >0?", function(){ return (Number.NEGATIVE_INFINITY > 0); });
</script>
Math.max — throws with 3+ args; no-arg returns 0
MDN specifies Math.max(...values) is variadic and returns -Infinity when called with no arguments. In the SFMC Jint engine it works only for two arguments: Math.max(1, 2) returns 2, but Math.max(1, 2, 3) (three or more) throws "Index was outside the bounds of the array.", and the no-argument Math.max() returns 0 instead of -Infinity. NaN still propagates. Compare two values at a time (Math.max(Math.max(a, b), c)) or fold with a loop / use the polyfill.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): Math.max()=0; Math.max(1,2)=2; Math.max(1,2,3)=ERR "Index was outside the bounds of the array."; Math.max(1,NaN)=nan. VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("Math.max()", function(){ return Math.max(); });
P("Math.max(1,2)", function(){ return Math.max(1,2); });
P("Math.max(1,2,3)", function(){ return Math.max(1,2,3); });
</script>
Math.min — throws with 3+ args; no-arg returns 0
MDN specifies Math.min(...values) is variadic and returns +Infinity when called with no arguments. In the SFMC Jint engine it works only for two arguments: Math.min(1, 2) returns 1, but Math.min(1, 2, 3) (three or more) throws "Index was outside the bounds of the array.", and the no-argument Math.min() returns 0 instead of +Infinity. NaN still propagates. Compare two values at a time (Math.min(Math.min(a, b), c)) or fold with a loop / use the polyfill.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): Math.min()=0; Math.min(1,2)=1; Math.min(1,2,3)=ERR "Index was outside the bounds of the array."; Math.min(1,NaN)=nan. VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("Math.min()", function(){ return Math.min(); });
P("Math.min(1,2)", function(){ return Math.min(1,2); });
P("Math.min(1,2,3)", function(){ return Math.min(1,2,3); });
</script>
TriggeredSend.Add — no working invocation - always throws "Error adding TSD."
The official docs present TriggeredSend.Add(properties) as a normal creation call returning an initialized instance. The name resolves at runtime (typeof TriggeredSend.Add === "function"), but no working invocation was found. Every call throws the plain string Error adding TSD., and TriggeredSend.LastMessage afterwards is either An error occurred when attempting to evaluate a SetObjectProperty function call. See inner exception for details. (any payload containing a nested object such as Email, List, or SendClassification) or the same Error adding TSD. with LastErrorCode 17014 / 2 (flat-only payloads). A string argument or a two-argument call fails earlier with Invalid cast from 'Char' to 'Double'.. Payload shapes swept without a single success: the nested SOAP shape, the documented flat shape (EmailID, ListID, SendClassificationID), dotted keys ("Email.ID"), scalar-only payloads, typed Core Library objects from Email.Init() / List.Init() / SendClassification.Init(), and the CLR object returned by TriggeredSend.Retrieve with a mutated CustomerKey. Decisive control in the same request: Script.Util.WSProxy().createItem("TriggeredSendDefinition", payload) with the identical payload returns Status: "OK", ErrorCode: 0, StatusMessage: "TriggeredSendDefinition created", and that definition then publishes, starts, sends, pauses, and updates normally. Create triggered sends with WSProxy createItem instead.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: TriggeredSend.Add throws the STRING "Error adding TSD." for every payload shape tried,
// while WSProxy createItem with the identical payload returns Status "OK" / "TriggeredSendDefinition created".
// Adapt the IDs below to real values in your account (numeric legacy email ID, list ID, send classification key).
var EMAIL_ID = 769268, LIST_ID = 72164, SC_KEY = "Default Transactional";
var payload = {
CustomerKey: "__qa_tsd_probe__", Name: "__qa_tsd_probe__",
Email: { ID: EMAIL_ID }, List: { ID: LIST_ID },
SendClassification: { CustomerKey: SC_KEY },
TriggeredSendType: "Continuous", FromName: "QA", FromAddress: "qa@example.com",
EmailSubject: "QA probe", IsWrapped: true
};
try {
var t = TriggeredSend.Add(payload);
Platform.Response.Write("Add returned typeof=" + (typeof t) + "\n");
} catch (e) {
Platform.Response.Write("Add THREW typeof=" + (typeof e) + " -> " + String(e) + "\n");
Platform.Response.Write("LastMessage: " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("LastErrorCode: " + TriggeredSend.LastErrorCode + "\n");
}
// Control: the same payload via WSProxy succeeds.
var res = new Script.Util.WSProxy().createItem("TriggeredSendDefinition", payload);
Platform.Response.Write("WSProxy Status: " + res.Status + "\n");
</script>
Boolean(value) — negative numbers and [] coerce to false; the primitive is not auto-boxed
MDN specifies Boolean(value) is falsy only for false, 0, -0, "", null, undefined and NaN, that every object (including []) is truthy, and that a primitive boolean is auto-boxed when a method is called on it. The SFMC Jint engine deviates three ways. (1) A number is truthy only when it is greater than zero, so Boolean(-1), Boolean(-0.5) and if (-1) are all falsy where the spec says truthy. (2) Boolean([]) is false — an empty array is coerced through ToPrimitive to "" first; Boolean([0]) is still true. (3) There is no auto-boxing: Boolean(1).toString() and Boolean(1).valueOf() throw "Object expected" instead of returning "true" / true. The return type itself is correct (typeof Boolean(1) === "boolean"). Compare numbers explicitly (n !== 0), test arrays with .length, and stringify with String(value) or Boolean.prototype.toString.call(value).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: Boolean(value) — coercion
*
* Proves:
* 1. Boolean(value) returns a PRIMITIVE boolean (typeof "boolean").
* 2. The truthy / falsy classification for each documented input.
* 3. Boolean(v), !!v and `if (v)` agree with each other.
* 4. DEVIATIONS from the ECMAScript spec, each marked "DEV":
* - negative numbers are FALSY (engine rule is `n > 0`, not `n !== 0`)
* - an empty array [] is FALSY (spec: every object is truthy)
* - the primitive result carries no methods: .toString() / .valueOf()
* throw "Object expected" instead of auto-boxing
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, actual, expected) {
var got;
try { got = String(actual); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === String(expected) ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function branch(v) { if (v) { return "truthy"; } return "falsy"; }
/* 1. The result is a primitive boolean, never an object. */
var a = Boolean(1);
assert("typeof Boolean(1) is boolean", typeof a, "boolean");
assert("Boolean(1) === true", Boolean(1) === true, "true");
assert("Boolean(0) === false", Boolean(0) === false, "true");
/* 2. Truthy inputs. */
assert("Boolean(true)", Boolean(true), "true");
assert("Boolean(2.5)", Boolean(2.5), "true");
assert("Boolean('x')", Boolean("x"), "true");
assert("Boolean('0')", Boolean("0"), "true");
assert("Boolean('false')", Boolean("false"), "true");
var obj = {};
assert("Boolean({})", Boolean(obj), "true");
/* 3. Falsy inputs that match the spec. */
assert("Boolean(false)", Boolean(false), "false");
assert("Boolean(0)", Boolean(0), "false");
assert("Boolean('')", Boolean(""), "false");
assert("Boolean(null)", Boolean(null), "false");
var undef;
assert("Boolean(undefined)", Boolean(undef), "false");
assert("Boolean(NaN)", Boolean(NaN), "false");
/* 4. DEVIATION — negative numbers are falsy. The engine rule is (n > 0). */
var neg1 = -1;
assert("DEV Boolean(-1) is false (spec: true)", Boolean(neg1), "false");
var neg2 = -0.5;
assert("DEV Boolean(-0.5) is false (spec: true)", Boolean(neg2), "false");
var neg3 = 0 - 3;
assert("DEV Boolean(0-3) is false (spec: true)", Boolean(neg3), "false");
assert("DEV if(-1) is falsy (spec: truthy)", branch(neg1), "falsy");
assert("rule check: -1 > 0", neg1 > 0, "false");
assert("rule check: 1 > 0", 1 > 0, "true");
/* 5. DEVIATION — an empty array is falsy; objects are coerced via ToPrimitive. */
var arr0 = [];
assert("DEV Boolean([]) is false (spec: true)", Boolean(arr0), "false");
assert("DEV if([]) is falsy (spec: truthy)", branch(arr0), "falsy");
var arr1 = [0];
assert("Boolean([0]) is true", Boolean(arr1), "true");
assert("String([]) is the empty string", String(arr0), "");
/* 6. Boolean(v), !!v and if(v) agree. */
assert("!!1 matches Boolean(1)", !!1, String(Boolean(1)));
assert("!!0 matches Boolean(0)", !!0, String(Boolean(0)));
assert("!!'' matches Boolean('')", !!"", String(Boolean("")));
assert("if(1) truthy", branch(1), "truthy");
assert("if(0) falsy", branch(0), "falsy");
assert("if('') falsy", branch(""), "falsy");
assert("if('x') truthy", branch("x"), "truthy");
/* 7. DEVIATION — no auto-boxing: the primitive result has no methods. */
assertThrows("DEV Boolean(1).toString() throws (spec: 'true')", function () { var t = Boolean(1); return t.toString(); });
assertThrows("DEV Boolean(1).valueOf() throws (spec: true)", function () { var t = Boolean(1); return t.valueOf(); });
var lit = true;
assert("literal true.toString() works and is lowercase", lit.toString(), "true");
assert("String(Boolean(true)) is 'true'", String(Boolean(true)), "true");
assert("String(Boolean(false)) is 'false'", String(Boolean(false)), "false");
</script>
new Boolean(value) — capitalized True/False; boxed false is falsy; valueOf does not unwrap; instanceof false
MDN specifies a boxed Boolean object stringifies to lowercase "true" / "false", is always truthy (it is an object), unwraps to its primitive via .valueOf(), and satisfies instanceof Boolean. The SFMC Jint engine breaks all four. (1) String(new Boolean(true)) is "True" — the first letter is capitalized, in String(), in "" + x concatenation, and in an explicit .toString(). (2) new Boolean(false) is falsy in a condition, so if (new Boolean(false)) is not entered and !!new Boolean(false) is false. (3) .valueOf() returns the boxed object itself (typeof is "object", box.valueOf() === box), so there is no reliable way to unwrap it. (4) new Boolean(true) instanceof Boolean is false, although .constructor === Boolean is true. Do not create boxed Booleans — use Boolean(value) or !!value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: new Boolean(value) — boxed object
*
* Proves:
* 1. new Boolean(value) produces an object (typeof "object").
* 2. DEVIATIONS from the ECMAScript spec, each marked "DEV":
* - stringification is CAPITALIZED: "True" / "False" (spec: lowercase)
* - a boxed false is FALSY (spec: every object is truthy)
* - .valueOf() does NOT unwrap — it returns the boxed object itself
* - `instanceof Boolean` is false even though .constructor matches
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
var got;
try { got = String(actual); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === String(expected) ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
function branch(v) { if (v) { return "truthy"; } return "falsy"; }
/* 1. It really is an object. */
var t = new Boolean(true);
assert("typeof new Boolean(true) is object", typeof t, "object");
var f = new Boolean(false);
assert("typeof new Boolean(false) is object", typeof f, "object");
/* 2. DEVIATION — capitalized string form. */
assert("DEV String(new Boolean(true)) is 'True' (spec: 'true')", String(t), "True");
assert("DEV String(new Boolean(false)) is 'False' (spec: 'false')", String(f), "False");
assert("DEV new Boolean(true).toString() is 'True' (spec: 'true')", t.toString(), "True");
assert("DEV new Boolean(false).toString() is 'False' (spec: 'false')", f.toString(), "False");
/* 3. DEVIATION — a boxed false is falsy here; in the spec every object is truthy. */
assert("DEV if(new Boolean(false)) is falsy (spec: truthy)", branch(f), "falsy");
assert("if(new Boolean(true)) is truthy", branch(t), "truthy");
assert("DEV !!new Boolean(false) is false (spec: true)", !!f, "false");
assert("DEV Boolean(new Boolean(false)) is false (spec: true)", Boolean(f), "false");
/* 4. DEVIATION — valueOf() does not unwrap to a primitive. */
var v = f.valueOf();
assert("DEV typeof boxed.valueOf() is object (spec: boolean)", typeof v, "object");
assert("DEV boxed.valueOf() === boxed (spec: false)", v === f, "true");
assert("DEV boxed.valueOf() === false is false (spec: true)", v === false, "false");
assert("boxed.valueOf() == false is true (loose compare)", v == false, "true");
/* 5. DEVIATION — instanceof fails although the constructor matches. */
assert("DEV new Boolean(true) instanceof Boolean is false (spec: true)", t instanceof Boolean, "false");
assert("new Boolean(true).constructor === Boolean", t.constructor === Boolean, "true");
/* 6. No-argument form. */
var n = new Boolean();
assert("typeof new Boolean() is object", typeof n, "object");
assert("String(new Boolean()) is 'False'", String(n), "False");
</script>
Platform.Function.UpdateDE — requires arrays, returns null, and commits on CloudPages
The official reference permits scalar strings for a single filter and reports an affected-row count in email contexts. The tested runtime requires nonempty, aligned arrays for all four filter/update name/value arguments and returns genuine JavaScript null for zero, one, or multiple matches. The qualified function also executes and commits on CloudPages. Use one-element arrays for a single column, and use UpdateData when the numeric affected-row count is required.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
var field = Platform.Function.CreateObject("DataExtensionField");
Platform.Function.SetObjectProperty(field, "Name", name); Platform.Function.SetObjectProperty(field, "FieldType", "Text");
Platform.Function.SetObjectProperty(field, "MaxLength", len); Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey);
Platform.Function.SetObjectProperty(field, "IsRequired", isKey); Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); Platform.Function.SetObjectProperty(de, "Name", name);
addField(de, "Id", "50", "true"); addField(de, "Txt", "50", "false"); var status = [0, 0]; return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); var status = [0, 0]; return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_ude_diff_2202_name", deKey = "ssjsg_ude_diff_2202_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("setup: row inserted", Platform.Function.InsertData(deName, ["Id", "Txt"], ["a", "seed"]), 1);
assertThrows("DEV scalar filter strings throw (docs: accepted)", function () { return Platform.Function.UpdateDE(deName, "Id", "a", ["Txt"], ["wrong"]); });
var result = Platform.Function.UpdateDE(deName, ["Id"], ["a"], ["Txt"], ["right"]);
assert("DEV array-form return is genuine null (docs: count)", result === null ? "null" : "not null", "null");
assert("DEV CloudPage update commits (docs: email only)", String(Platform.Function.Lookup(deName, "Txt", "Id", "a")), "right");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>
Platform.Function.UpsertDE — requires arrays, returns null, and commits on CloudPages
The official reference types whereFieldNames as string or string[] and whereFieldValues as string or array, documents a numeric count of upserted rows as the return value, and presents UpsertDE as a sendable-context function. All three are wrong at runtime. Every scalar argument position throws “Unable to retrieve security descriptor for this frame.”, so all four filter and field name/value arguments must be nonempty, positionally aligned arrays. The insert, single-match update and multiple-match update branches each return genuine JavaScript null. The call also executes and commits on a CloudPage. Wrap single columns and values in one-element arrays, and use UpsertData when the numeric affected-row count is required.
Show test script
<script runat="server">
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
var field = Platform.Function.CreateObject("DataExtensionField");
Platform.Function.SetObjectProperty(field, "Name", name); Platform.Function.SetObjectProperty(field, "FieldType", "Text");
Platform.Function.SetObjectProperty(field, "MaxLength", len); Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey);
Platform.Function.SetObjectProperty(field, "IsRequired", isKey); Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); Platform.Function.SetObjectProperty(de, "Name", name);
addField(de, "Id", "50", "true"); addField(de, "Txt", "50", "false"); var status = [0, 0]; return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); var status = [0, 0]; return String(Platform.Function.InvokeDelete(de, status, null));
}
function isNull(value) { return value === null ? "null" : "not null"; }
var deName = "ssjsg_upde_diff_2358_name", deKey = "ssjsg_upde_diff_2358_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
// Every scalar position is rejected with the engine's generic unaccepted-signature signal.
assertThrows("DEV scalar whereFieldNames throws (docs: string or string[])", function () { return Platform.Function.UpsertDE(deName, "Id", ["a"], ["Txt"], ["wrong"]); });
assertThrows("DEV scalar whereFieldValues throws (docs: string or array)", function () { return Platform.Function.UpsertDE(deName, ["Id"], "a", ["Txt"], ["wrong"]); });
assertThrows("scalar fieldNames throws", function () { return Platform.Function.UpsertDE(deName, ["Id"], ["a"], "Txt", ["wrong"]); });
assertThrows("scalar fieldValues throws", function () { return Platform.Function.UpsertDE(deName, ["Id"], ["a"], ["Txt"], "wrong"); });
// The workaround: one-element arrays in all four positions. Both branches return null.
assert("DEV the insert branch returns genuine null (docs: count of upserted rows)", isNull(Platform.Function.UpsertDE(deName, ["Id"], ["a"], ["Txt"], ["inserted"])), "null");
assert("DEV the update branch returns genuine null (docs: count of upserted rows)", isNull(Platform.Function.UpsertDE(deName, ["Id"], ["a"], ["Txt"], ["updated"])), "null");
assert("DEV the CloudPage upsert commits (docs: sendable contexts)", String(Platform.Function.Lookup(deName, "Id", "Txt", "updated")), "a");
assert("WORKAROUND UpsertData returns the numeric affected-row count", Platform.Function.UpsertData(deName, ["Id"], ["b"], ["Txt"], ["counted"]), 1);
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>
Platform.Variable.GetValue / SetValue — typed scalar bridge, null absence/return, optional @ prefix
The official member pages describe string values, require an @ prefix, and document SetValue as void. On a published CloudPage GET, the prefix is optional, names are case-insensitive, numbers and booleans retain their SSJS type between server-side blocks, a never-set variable returns JavaScript null, and SetValue also returns JavaScript null. The variable state is limited to the current request.
Show test script
<script runat="server">
/*
* Differs-from-docs: Platform.Variable.GetValue / SetValue
*
* Proves:
* 1. DEV: @ prefix is optional (official docs: require @).
* 2. DEV: names are case-insensitive.
* 3. DEV: numbers and booleans retain SSJS type (official docs: string).
* 4. DEV: never-set GetValue returns strict JS null (official docs: string).
* 5. DEV: SetValue returns strict JS null (official docs: void).
* 6. Values set in this request are readable in the same request.
*
* NON-ASSERTABLE in one GET: cross-request absence (request-local) -
* requires a second request after a prior SetValue.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var setAt = Platform.Variable.SetValue("@dfdPlatVarStr", "hello");
assert("DEV SetValue with @ returns null (official docs: void)", setAt === null ? "null" : "other", "null");
var setBare = Platform.Variable.SetValue("dfdPlatVarBare", "bare-value");
assert("DEV SetValue without @ returns null (official docs: void)", setBare === null ? "null" : "other", "null");
assert("DEV @ optional: bare SetValue readable with @", Platform.Variable.GetValue("@dfdPlatVarBare"), "bare-value");
assert("DEV @ optional: @ SetValue readable without @", Platform.Variable.GetValue("dfdPlatVarStr"), "hello");
Platform.Variable.SetValue("@dfdPlatVarCase", "case-a");
Platform.Variable.SetValue("@DFDPLATVARCASE", "case-b");
assert("DEV names are case-insensitive (last write wins)", Platform.Variable.GetValue("@dfdPlatVarCase"), "case-b");
Platform.Variable.SetValue("@dfdPlatVarNum", 42.5);
var numVal = Platform.Variable.GetValue("@dfdPlatVarNum");
assert("DEV number retains type (official docs: string)", numVal, 42.5);
assert("DEV number typeof is number (official docs: string)", "" + (typeof numVal), "number");
Platform.Variable.SetValue("@dfdPlatVarBool", true);
var boolVal = Platform.Variable.GetValue("@dfdPlatVarBool");
assert("DEV boolean retains type (official docs: string)", boolVal, true);
assert("DEV boolean typeof is boolean (official docs: string)", "" + (typeof boolVal), "boolean");
var missing = Platform.Variable.GetValue("dfdPlatVarMissing20260815");
assert("DEV never-set is strict null (official docs: string)", missing === null ? "null" : "other", "null");
assert("DEV never-set typeof is object", "" + (typeof missing), "object");
assert("same-request read-back after SetValue", Platform.Variable.GetValue("@dfdPlatVarStr"), "hello");
</script>
Variable (bare-name Core alias) — scalar GetValue + undefined SetValue return after Core load
The bare-name Variable alias (after Platform.Load("core", "1.1.5")) shares request-local state with Platform.Variable. GetValue preserves number/boolean scalars and returns JavaScript null for a never-set name (official docs: string). SetValue returns undefined (void-like), which matches the official void wording more closely than Platform.Variable.SetValue, which returns JavaScript null.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: Variable (bare-name Core alias)
*
* Proves:
* 1. After Core load, bare Variable shares state with Platform.Variable.
* 2. DEV: GetValue preserves number/boolean (official docs: string).
* 3. DEV: never-set GetValue returns strict JS null (official docs: string).
* 4. DEV: bare SetValue returns undefined; Platform.Variable.SetValue
* returns null (official docs: void for both).
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
Variable.SetValue("@dfdCoreVarAlias", "from-bare");
assert("bare Variable shares Platform.Variable state", Platform.Variable.GetValue("@dfdCoreVarAlias"), "from-bare");
Platform.Variable.SetValue("@dfdCoreVarFromPlat", "from-platform");
assert("Platform.Variable writes visible via bare Variable", Variable.GetValue("@dfdCoreVarFromPlat"), "from-platform");
Variable.SetValue("@dfdCoreVarNum", 99);
var numVal = Variable.GetValue("@dfdCoreVarNum");
assert("DEV GetValue preserves number (official docs: string)", numVal, 99);
assert("DEV GetValue number typeof is number (official docs: string)", "" + (typeof numVal), "number");
Variable.SetValue("@dfdCoreVarBool", false);
assert("DEV GetValue preserves boolean (official docs: string)", Variable.GetValue("@dfdCoreVarBool"), false);
var missing = Variable.GetValue("@dfdCoreVarMissing20260815");
assert("DEV never-set is strict null (official docs: string)", missing === null ? "null" : "other", "null");
var bareRet = Variable.SetValue("@dfdCoreVarRet", "r");
var platRet = Platform.Variable.SetValue("@dfdCoreVarRetPlat", "r");
assert("DEV bare SetValue returns undefined (official docs: void)", bareRet === undefined ? "undefined" : "other", "undefined");
assert("DEV Platform.Variable.SetValue returns null (bare returns undefined)", platRet === null ? "null" : "other", "null");
</script>
HTTPHeader (bare-name global) — separate inbound/outbound collections
Available after Platform.Load("core", ...). GetValue reads inbound request headers and returns null for a header you set via SetValue (separate inbound vs outbound collections). Remove returns undefined, not the "OK" string implied by some docs. Official docs claim host cannot be changed; runtime SetValue("Host", …) does emit an outbound Host header. content-length remains protected.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: separate inbound/outbound collections + Remove undefined
* + Host not protected (content-length is).
*
* Proves:
* 1. DEV: GetValue after SetValue is null (official docs: one header bag).
* 2. GetValue("Host") still returns the inbound CloudPage host string.
* 3. DEV: Remove returns undefined (official docs: "OK").
* 4. DEV: SetValue("Host", ...) succeeds (official docs: host cannot be changed).
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
HTTPHeader.SetValue("X-Custom-Test", "hello");
var readBack = HTTPHeader.GetValue("X-Custom-Test");
assert("DEV GetValue after SetValue is null (official docs: shared collection)", readBack === null ? "null" : "other", "null");
var host = HTTPHeader.GetValue("Host");
assert("GetValue Host is a string", "" + (typeof host), "string");
assert("GetValue Host is non-empty", host && host.length > 0 ? "true" : "false", "true");
var removed = HTTPHeader.Remove("X-Custom-Test");
assert("DEV Remove returns undefined (official docs: OK)", removed === undefined ? "undefined" : "other", "undefined");
assert("DEV Remove typeof is undefined", "" + (typeof removed), "undefined");
var hostSet = HTTPHeader.SetValue("Host", "differs-host-probe");
assert("DEV SetValue Host succeeds (official docs: cannot change host)", hostSet === undefined ? "undefined" : "other", "undefined");
</script>
ErrorUtil (bare-name global) — only in Core "1"
ErrorUtil is provided only by Platform.Load("Core", "1"). Under newer Core versions ("1.1.1", "1.1.5", …) it is undefined. Effectively deprecated in Core > 1. Prefer checking result.Status and throwing new Error(...) instead of ErrorUtil.ThrowWSProxyError.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Probe: under Core 1.1.5 the ErrorUtil global is undefined (only Core \"1\" provides it)
try {
Platform.Response.Write("typeof ErrorUtil (Core 1.1.5) = " + (typeof ErrorUtil) + "\n");
// OBSERVED: \"undefined\" under Core 1.1.5
} catch (e) {
Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n");
}
// Counter-probe (separate deploy): swapping the load to Platform.Load("Core", "1")
// and re-running yields typeof ErrorUtil = \"object\" (OBSERVED). This confirms the
// version split: ErrorUtil exists only under Core \"1\", not Core > 1.
// Probe: preferred alternative - check result.Status and throw a plain Error
try {
var fakeResult = { Status: "Error" };
if (fakeResult.Status != "OK") {
throw new Error("WSProxy call failed with Status = " + fakeResult.Status);
}
Platform.Response.Write("OK\n");
} catch (e) {
Platform.Response.Write("Caught expected error: " + Platform.Function.Stringify(e) + "\n");
// Prefer new Error(...) over ErrorUtil.ThrowWSProxyError in Core > 1
}
</script>
Attribute.GetValue — works in CloudPages, returns "" when no recipient
After Platform.Load("Core", ...) the Attribute object exists and Attribute.GetValue(name) executes and returns a string — it is not unavailable in CloudPages, contrary to what the docs imply. When no subscriber/attribute is in context (e.g. a plain CloudPage GET) it returns an empty string rather than throwing. In email/triggered-send/personalized contexts it returns the actual attribute value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Probe 1: the Attribute object exists after Platform.Load in a CloudPage
try {
Platform.Response.Write("typeof Attribute = " + (typeof Attribute) + "\n");
// OBSERVED: \"object\" - Attribute is available in CloudPages, contrary to docs
} catch (e) {
Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n");
}
// Probe 2: GetValue on a plain CloudPage GET returns an empty string (no throw)
try {
var val = Attribute.GetValue("FirstName");
Platform.Response.Write("Attribute.GetValue('FirstName') = '" + val + "' (length " + String(val).length + ")\n");
// OBSERVED: '' (length 0) on a plain CloudPage GET - no throw (actual value in email/send context)
} catch (e) {
Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n");
}
</script>
Platform.Function.ContentArea — only the single-argument form works from SSJS
Only arity 1 returns a value. The docs present ContentArea(id, regionName, stopOnError, fallbackContent) as an ordinary callable, with stopOnError: false letting a failed call proceed and fallbackContent displayed when nothing comes back. At runtime:
- Arity 1 works. Given the id of a Content Area that exists, it returns that area’s content, and the same id passed as a numeric string works too. An id that does not resolve throws
An error occurred when attempting to evaluate an ContentArea function call.— that throw means the id did not resolve, nothing more. - Arity 2–4 throw a resolved-value error (
… must be a literal (constant) values.,Parameter Name: ImpressionRegionName,Parameter Ordinal: 2,Parameter Type: ResolvedValueParameter) for everyregionNameshape — string literal, concatenation, variable, empty string andnullalike. Because parameter 2 is rejected outright,stopOnErrorandfallbackContentare unreachable and the fallback string is never emitted. - Arity 0 and 5+ throw the overloaded
Unable to retrieve security descriptor for this frame.
Core-library equivalent: ContentArea() is a genuine function after Platform.Load("core", …) (typeof is "function", not the phantom "clrmethodinfo") and behaves identically — arity 1 returns the content, arity 2 hits the same parameter-2 rejection, and its arity 3–5 forms throw the security-descriptor error, so its errorMsg parameter is unreachable too.
Salesforce documents classic Content Areas as deprecated; use Platform.Function.ContentBlockByID() for new content.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// NOTE: these CLR exception messages cannot be inspected with .length /
// .indexOf / .substring - those abort the CloudPage with HTTP 422.
function attempt(id, fn) {
try { Platform.Response.Write(id + ": returned: " + fn() + "\n"); }
catch (e) { Platform.Response.Write(id + ": threw: " + e.message + "\n"); }
}
// Resolve the id of a Content Area that genuinely exists in this business
// unit - an invented id would only prove that a missing area fails.
var realId = 0;
var prox = new Script.Util.WSProxy();
var res = prox.retrieve("ContentArea", ["ID","Name"], null);
if (res.Results && res.Results.length > 0) { realId = res.Results[0].ID; }
Platform.Response.Write("a real Content Area id exists: " + (realId > 0) + "\n");
attempt("arity 1, real existing id", function () { return Platform.Function.ContentArea(realId); });
attempt("arity 1, same id as a numeric string", function () { return Platform.Function.ContentArea("" + realId); });
// OBSERVED: both return the content area's markup.
attempt("arity 2, regionName literal", function () { return Platform.Function.ContentArea(realId, "impressionRegion"); });
attempt("arity 2, regionName null", function () { return Platform.Function.ContentArea(realId, null); });
attempt("arity 3, stopOnError false", function () { return Platform.Function.ContentArea(realId, "impressionRegion", false); });
attempt("arity 4, with fallbackContent", function () { return Platform.Function.ContentArea(realId, "impressionRegion", false, "FALLBACK"); });
// OBSERVED: all four throw the ImpressionRegionName / ResolvedValueParameter error,
// so stopOnError and fallbackContent never take effect.
attempt("arity 0", function () { return Platform.Function.ContentArea(); });
attempt("arity 5", function () { return Platform.Function.ContentArea(realId, "reg", false, "fb", "extra"); });
// OBSERVED: both throw "Unable to retrieve security descriptor for this frame."
attempt("bare-name arity 1", function () { return ContentArea(realId); });
// OBSERVED: returns the same content - the Core form behaves identically.
</script>
Platform.Function.ContentAreaByName — only the single-argument form works from SSJS
Only arity 1 returns a value. The docs present ContentAreaByName(name, regionName, stopOnError, fallbackContent) as an ordinary callable, with stopOnError: false letting a failed call proceed and fallbackContent displayed when nothing comes back. At runtime:
- Arity 1 works. Given the name of a Content Area that exists, it returns that area’s content. Matching is case-insensitive and the
folder\nameform resolves as well; the area’s CustomerKey, a space-padded name and an unknown name are rejected withAn error occurred when attempting to evaluate a ContentAreaByName function call.— a throw here means the name did not resolve, nothing more. - Arity 2–4 throw a resolved-value error (
… must be a literal (constant) values.,Parameter Name: ImpressionRegionName,Parameter Ordinal: 2,Parameter Type: ResolvedValueParameter) for everyregionNameshape — string literal, concatenation, variable, empty string andnullalike. Because parameter 2 is rejected outright,stopOnErrorandfallbackContentare unreachable and the fallback string is never emitted. - Arity 0 and 5+ throw the overloaded
Unable to retrieve security descriptor for this frame.
Core-library equivalent: ContentAreaByName() is a genuine function after Platform.Load("core", …) (typeof is "function", not the phantom "clrmethodinfo") and behaves identically — arity 1 returns the content, arity 2 hits the same parameter-2 rejection, and its arity 3–5 forms throw the security-descriptor error, so its errorMsg parameter is unreachable too.
Salesforce documents classic Content Areas as deprecated; use Platform.Function.ContentBlockByName() for new content.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// NOTE: these CLR exception messages cannot be inspected with .length /
// .indexOf / .substring - those abort the CloudPage with HTTP 422.
function attempt(id, fn) {
try { Platform.Response.Write(id + ": returned: " + fn() + "\n"); }
catch (e) { Platform.Response.Write(id + ": threw: " + e.message + "\n"); }
}
// Resolve a Content Area name that is verified to exist in this business
// unit - reading the Name back is what makes the test meaningful.
var prox = new Script.Util.WSProxy();
var res = prox.retrieve("ContentArea", ["ID","Name","CustomerKey"], null);
var realName = "", realKey = "";
for (var i = 0; i < res.Results.length; i++) {
if (res.Results[i].Name) { realName = res.Results[i].Name; realKey = res.Results[i].CustomerKey; break; }
}
Platform.Response.Write("a Content Area with a readable name exists: " + (realName !== "") + "\n");
attempt("arity 1, verified name", function () { return Platform.Function.ContentAreaByName(realName); });
attempt("arity 1, same name uppercased", function () { return Platform.Function.ContentAreaByName(realName.toUpperCase()); });
// OBSERVED: both return the content area's markup - matching is case-insensitive.
attempt("arity 1, CustomerKey instead of Name", function () { return Platform.Function.ContentAreaByName(realKey); });
attempt("arity 1, space-padded name", function () { return Platform.Function.ContentAreaByName(" " + realName + " "); });
attempt("arity 1, unknown name", function () { return Platform.Function.ContentAreaByName("zzz-no-such-area"); });
// OBSERVED: all three throw "An error occurred when attempting to evaluate a ContentAreaByName function call."
attempt("arity 2, regionName literal", function () { return Platform.Function.ContentAreaByName(realName, "impressionRegion"); });
attempt("arity 2, regionName null", function () { return Platform.Function.ContentAreaByName(realName, null); });
attempt("arity 3, stopOnError false", function () { return Platform.Function.ContentAreaByName(realName, "impressionRegion", false); });
attempt("arity 4, with fallbackContent", function () { return Platform.Function.ContentAreaByName(realName, "impressionRegion", false, "FALLBACK"); });
// OBSERVED: all four throw the ImpressionRegionName / ResolvedValueParameter error,
// so stopOnError and fallbackContent never take effect.
attempt("arity 0", function () { return Platform.Function.ContentAreaByName(); });
attempt("arity 5", function () { return Platform.Function.ContentAreaByName("a", "reg", false, "fb", "extra"); });
// OBSERVED: both throw "Unable to retrieve security descriptor for this frame."
attempt("bare-name arity 1", function () { return ContentAreaByName(realName); });
// OBSERVED: returns the same content - the Core form behaves identically.
</script>
Platform.Function.ContentBlockByID — only the single-argument form works from SSJS
Only the 1-argument form works from SSJS. The docs show Platform.Function.ContentBlockByID(12345,"impressionRegion",false,"defaultContentHere") as ordinary working SSJS. At runtime:
- Arity 1 works and returns the block’s rendered body. The
idmay be a number, a numeric string or a variable — there is no literal restriction on the single-argument form. - Arity 2–4 throw
A ContentBlockByID function call includes an invalid parameter value. … must be a literal (constant) values.namingParameter Name: ImpressionRegionName,Parameter Ordinal: 2,Parameter Type: ResolvedValueParameter— for a string literal, a number, a boolean, the empty string,nulland a variable alike. This is therefore not a literal-vs-variable rule: the second parameter cannot be supplied from SSJS at all, which makesstopOnErrorandfallbackContentunreachable and the fallback string is never emitted. - Arity 0 and 5+ throw the overloaded
Unable to retrieve security descriptor for this frame., as does passing the asset’s external key instead of its numeric id. - No bare-name Core form exists:
typeof ContentBlockByIDis"undefined"even afterPlatform.Load("core", …), and invoking it throwsObject expected: ContentBlockByID.
The platform is not at fault — the SSJS binding is. The AMPscript function of the same name honours all four parameters. Invoked through Platform.Function.TreatAsContent() in the very same request, a named impression region returns the body, fallbackContent is returned for a missing block, stopOnError: true propagates the error and stopOnError: false without a fallback yields the empty string. Use that as the workaround whenever an optional parameter is needed.
The sibling Platform.Function.ContentBlockByKey() carries the identical restriction, so this is a family-wide SSJS binding limitation rather than a quirk of the ID form.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// NOTE: these CLR exception messages cannot be inspected with .length /
// .indexOf / .substring - those abort the CloudPage with HTTP 422.
// FIXTURE: id 1469165 is a Content Builder HTML block whose whole body is
// the token SSJSGUIDE-TEST-BLOCK-OK; id 12345 does not exist.
function attempt(id, fn) {
try { Platform.Response.Write(id + ": returned [" + fn() + "]\n"); }
catch (e) { Platform.Response.Write(id + ": threw: " + e.message + "\n"); }
}
attempt("arity 1, real id", function () { return Platform.Function.ContentBlockByID(1469165); });
attempt("arity 1, numeric string id", function () { return Platform.Function.ContentBlockByID("1469165"); });
// OBSERVED: both return SSJSGUIDE-TEST-BLOCK-OK - the 1-argument form works.
attempt("arity 2, regionName STRING LITERAL", function () { return Platform.Function.ContentBlockByID(1469165, "heroRegion"); });
attempt("arity 2, regionName variable", function () { var r = "heroRegion"; return Platform.Function.ContentBlockByID(1469165, r); });
attempt("arity 2, regionName null", function () { return Platform.Function.ContentBlockByID(1469165, null); });
attempt("arity 3, stopOnError false", function () { return Platform.Function.ContentBlockByID(12345, "heroRegion", false); });
attempt("arity 4, with fallbackContent", function () { return Platform.Function.ContentBlockByID(12345, "heroRegion", false, "FALLBACK"); });
// OBSERVED: all five throw the ImpressionRegionName / Ordinal 2 /
// ResolvedValueParameter error - a literal fails exactly like a variable.
attempt("arity 0", function () { return Platform.Function.ContentBlockByID(); });
attempt("arity 5", function () { return Platform.Function.ContentBlockByID(1469165, "r", false, "fb", "extra"); });
attempt("external key instead of id", function () { return Platform.Function.ContentBlockByID("ssjs-guide-test-block"); });
// OBSERVED: all three throw "Unable to retrieve security descriptor for this frame."
Platform.Response.Write("typeof bare-name ContentBlockByID: " + (typeof ContentBlockByID) + "\n");
// OBSERVED: undefined - there is no Core global.
// CONTROL: the AMPscript form honours every optional parameter.
attempt("AMPscript arity 2 with a region", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByID(1469165,"heroRegion")=%%'); });
attempt("AMPscript arity 4 fallback, missing block", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByID(12345,"heroRegion",false,"FALLBACK")=%%'); });
attempt("AMPscript arity 3 stopOnError false", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByID(12345,"heroRegion",false)=%%'); });
// OBSERVED: SSJSGUIDE-TEST-BLOCK-OK / FALLBACK / empty string - the
// documented semantics are implemented, just not reachable from SSJS.
</script>
Platform.Function.ContentBlockByKey — only the single-argument form works from SSJS
Only the 1-argument form works from SSJS. The docs show Platform.Function.ContentBlockByKey("myExternalKey","impressionRegion",false,"defaultContentHere") as ordinary working SSJS. At runtime:
- Arity 1 works and returns the block’s rendered body. The key may be a string literal or a variable — there is no literal restriction on the single-argument form.
- Arity 2–4 throw
A ContentBlockByKey function call includes an invalid parameter value. … must be a literal (constant) values.namingParameter Name: ImpressionRegionName,Parameter Ordinal: 2,Parameter Type: ResolvedValueParameter— for a string literal, a number, a boolean, the empty string,nulland a variable alike. This is therefore not a literal-vs-variable rule: the second parameter cannot be supplied from SSJS at all, which makesstopOnErrorandfallbackContentunreachable and the fallback string is never emitted. - A key that does not exist throws
An error occurred when attempting to evaluate a ContentBlockByKey function call.— it does not return the empty string, so a missing block must be guarded withtry/catch. Passing the asset’s numeric ID instead of its key throws the same evaluation error. - Arity 0 and 5+ throw the overloaded
Unable to retrieve security descriptor for this frame. - No bare-name Core form exists:
typeof ContentBlockByKeyis"undefined"even afterPlatform.Load("core", …), and invoking it throwsObject expected: ContentBlockByKey.
The platform is not at fault — the SSJS binding is. The AMPscript function of the same name honours all four parameters. Invoked through Platform.Function.TreatAsContent() in the very same request, a named impression region returns the body, fallbackContent is returned for a missing block, stopOnError: true propagates the error and stopOnError: false without a fallback yields the empty string. Use that as the workaround whenever an optional parameter is needed.
The siblings Platform.Function.ContentBlockByID() and Platform.Function.ContentBlockByName() carry the identical restriction, so this is a family-wide SSJS binding limitation.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// NOTE: these CLR exception messages cannot be inspected with .length /
// .indexOf / .substring - those abort the CloudPage with HTTP 422.
// FIXTURE: key ssjs-guide-test-block is a Content Builder HTML block whose
// whole body is the token SSJSGUIDE-TEST-BLOCK-OK; the xyz key does not exist.
function attempt(id, fn) {
try { Platform.Response.Write(id + ": returned [" + fn() + "]\n"); }
catch (e) { Platform.Response.Write(id + ": threw: " + e.message + "\n"); }
}
attempt("arity 1, real key", function () { return Platform.Function.ContentBlockByKey("ssjs-guide-test-block"); });
attempt("arity 1, variable key", function () { var k = "ssjs-guide-test-block"; return Platform.Function.ContentBlockByKey(k); });
// OBSERVED: both return SSJSGUIDE-TEST-BLOCK-OK - the 1-argument form works.
attempt("arity 2, regionName STRING LITERAL", function () { return Platform.Function.ContentBlockByKey("ssjs-guide-test-block", "heroRegion"); });
attempt("arity 2, regionName variable", function () { var r = "heroRegion"; return Platform.Function.ContentBlockByKey("ssjs-guide-test-block", r); });
attempt("arity 2, regionName null", function () { return Platform.Function.ContentBlockByKey("ssjs-guide-test-block", null); });
attempt("arity 3, stopOnError false", function () { return Platform.Function.ContentBlockByKey("ssjs-guide-no-such-block-xyz", "heroRegion", false); });
attempt("arity 4, with fallbackContent", function () { return Platform.Function.ContentBlockByKey("ssjs-guide-no-such-block-xyz", "heroRegion", false, "FALLBACK"); });
// OBSERVED: all five throw the ImpressionRegionName / Ordinal 2 /
// ResolvedValueParameter error - a literal fails exactly like a variable.
attempt("arity 1, missing key", function () { return Platform.Function.ContentBlockByKey("ssjs-guide-no-such-block-xyz"); });
attempt("numeric id instead of key", function () { return Platform.Function.ContentBlockByKey(1469165); });
// OBSERVED: both throw "An error occurred when attempting to evaluate a
// ContentBlockByKey function call." - a missing block is NOT an empty string.
attempt("arity 0", function () { return Platform.Function.ContentBlockByKey(); });
attempt("arity 5", function () { return Platform.Function.ContentBlockByKey("ssjs-guide-test-block", "r", false, "fb", "extra"); });
// OBSERVED: both throw "Unable to retrieve security descriptor for this frame."
Platform.Response.Write("typeof bare-name ContentBlockByKey: " + (typeof ContentBlockByKey) + "\n");
// OBSERVED: undefined - there is no Core global.
// CONTROL: the AMPscript form honours every optional parameter.
attempt("AMPscript arity 2 with a region", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByKey("ssjs-guide-test-block","heroRegion")=%%'); });
attempt("AMPscript arity 4 fallback, missing block", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByKey("ssjs-guide-no-such-block-xyz","heroRegion",false,"FALLBACK")=%%'); });
attempt("AMPscript arity 3 stopOnError false", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByKey("ssjs-guide-no-such-block-xyz","heroRegion",false)=%%'); });
// OBSERVED: SSJSGUIDE-TEST-BLOCK-OK / FALLBACK / empty string - the
// documented semantics are implemented, just not reachable from SSJS.
</script>
Platform.Function.ContentBlockByName — only the single-argument form works, and folder paths need a backslash
Only the 1-argument form works from SSJS. The docs show Platform.Function.ContentBlockByName(pathAndName, regionName, stopOnError, fallbackContent, statusVariable) as ordinary working SSJS. At runtime:
- Arity 1 works and returns the block’s rendered body. The name may be a string literal or a variable — there is no literal restriction on the single-argument form.
- Arity 2–4 throw
A ContentBlockByName function call includes an invalid parameter value. … must be a literal (constant) values.namingParameter Name: ImpressionRegionName,Parameter Ordinal: 2,Parameter Type: ResolvedValueParameter— for a string literal, a number, a boolean, the empty string,nulland a variable alike. This is therefore not a literal-vs-variable rule: the second parameter cannot be supplied from SSJS at all, which makesstopOnErrorandfallbackContentunreachable. - Arity 0 and 5+ throw the overloaded
Unable to retrieve security descriptor for this frame.— so the documented fifth parameterstatusVariableis unreachable for a different reason: the call never even reaches the parameter check. - A name that does not exist throws
An error occurred when attempting to evaluate a ContentBlockByName function call.— it does not return the empty string. Passing the asset’s numeric ID throws the same evaluation error. - No bare-name Core form exists:
typeof ContentBlockByNameis"undefined"even afterPlatform.Load("core", …), and invoking it throwsObject expected: ContentBlockByName.
The folder-path separator is a backslash, not a forward slash. A bare name resolves the asset at any folder depth — the path is only needed to disambiguate a name reused across folders. When a path is supplied it must use \: "Content Builder\My Folder\My Block" resolves, while "Content Builder/My Folder/My Block" throws. Both the fully-qualified path from the Content Builder root and a partial path (immediate folder plus name) resolve, and a path naming the wrong folder throws — so the path really is matched rather than ignored.
SSJS authoring caveat: a string literal whose last character is a backslash ("Content Builder\\" + folder) aborts the whole CloudPage with HTTP 422 before any line runs — it is not a catchable throw. Keep the separator in the middle of a literal, or build it with String.fromCharCode(92).
The platform is not at fault — the SSJS binding is. The AMPscript function of the same name honours all five parameters. Invoked through Platform.Function.TreatAsContent() in the very same request, a named impression region returns the body, fallbackContent is returned for a missing block, stopOnError: true propagates the error, stopOnError: false without a fallback yields the empty string, and the 5-argument statusVariable form returns the body. Use that as the workaround whenever an optional parameter is needed.
The siblings Platform.Function.ContentBlockByID() and Platform.Function.ContentBlockByKey() carry the identical single-argument restriction, so this is a family-wide SSJS binding limitation.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// NOTE: these CLR exception messages cannot be inspected with .length /
// .indexOf / .substring - those abort the CloudPage with HTTP 422.
// NOTE: a string literal ENDING in a backslash also aborts the page, so the
// separator is built with String.fromCharCode(92).
// FIXTURES: "ssjs-guide-test-block" sits in the Content Builder root,
// "ssjs-guide-folder-block" one folder deep under "ssjs-guide-tests"; the
// xyz name does not exist.
var SEP = String.fromCharCode(92);
function attempt(id, fn) {
try { Platform.Response.Write(id + ": returned [" + fn() + "]\n"); }
catch (e) { Platform.Response.Write(id + ": threw: " + e.message + "\n"); }
}
attempt("arity 1, bare name", function () { return Platform.Function.ContentBlockByName("ssjs-guide-test-block"); });
attempt("arity 1, bare name of a block one folder deep", function () { return Platform.Function.ContentBlockByName("ssjs-guide-folder-block"); });
// OBSERVED: SSJSGUIDE-TEST-BLOCK-OK / SSJSGUIDE-FOLDER-BLOCK-OK - a bare
// name resolves the asset regardless of folder depth.
attempt("BACKSLASH path, fully qualified", function () { return Platform.Function.ContentBlockByName("Content Builder" + SEP + "ssjs-guide-tests" + SEP + "ssjs-guide-folder-block"); });
attempt("BACKSLASH path, partial", function () { return Platform.Function.ContentBlockByName("ssjs-guide-tests" + SEP + "ssjs-guide-folder-block"); });
// OBSERVED: both return SSJSGUIDE-FOLDER-BLOCK-OK.
attempt("FORWARD-SLASH path", function () { return Platform.Function.ContentBlockByName("ssjs-guide-tests/ssjs-guide-folder-block"); });
attempt("BACKSLASH path naming the WRONG folder", function () { return Platform.Function.ContentBlockByName("Content Builder" + SEP + "ssjs-guide-tests" + SEP + "ssjs-guide-test-block"); });
attempt("arity 1, missing name", function () { return Platform.Function.ContentBlockByName("ssjs-guide-no-such-block-xyz"); });
attempt("numeric id instead of name", function () { return Platform.Function.ContentBlockByName(1469165); });
// OBSERVED: all four throw "An error occurred when attempting to evaluate a
// ContentBlockByName function call." - a forward slash does NOT work, and a
// missing block is NOT an empty string.
attempt("arity 2, regionName STRING LITERAL", function () { return Platform.Function.ContentBlockByName("ssjs-guide-test-block", "heroRegion"); });
attempt("arity 2, regionName variable", function () { var r = "heroRegion"; return Platform.Function.ContentBlockByName("ssjs-guide-test-block", r); });
attempt("arity 2, regionName null", function () { return Platform.Function.ContentBlockByName("ssjs-guide-test-block", null); });
attempt("arity 3, stopOnError false", function () { return Platform.Function.ContentBlockByName("ssjs-guide-no-such-block-xyz", "heroRegion", false); });
attempt("arity 4, with fallbackContent", function () { return Platform.Function.ContentBlockByName("ssjs-guide-no-such-block-xyz", "heroRegion", false, "FALLBACK"); });
// OBSERVED: all five throw the ImpressionRegionName / Ordinal 2 /
// ResolvedValueParameter error - a literal fails exactly like a variable.
attempt("arity 0", function () { return Platform.Function.ContentBlockByName(); });
attempt("arity 5, with statusVariable", function () { return Platform.Function.ContentBlockByName("ssjs-guide-test-block", "r", false, "fb", "statusVar"); });
// OBSERVED: both throw "Unable to retrieve security descriptor for this frame."
Platform.Response.Write("typeof bare-name ContentBlockByName: " + (typeof ContentBlockByName) + "\n");
// OBSERVED: undefined - there is no Core global.
// CONTROL: the AMPscript form honours every optional parameter.
attempt("AMPscript arity 2 with a region", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByName("ssjs-guide-test-block","heroRegion")=%%'); });
attempt("AMPscript arity 4 fallback, missing block", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByName("ssjs-guide-no-such-block-xyz","heroRegion",false,"FALLBACK")=%%'); });
attempt("AMPscript arity 3 stopOnError false", function () { return Platform.Function.TreatAsContent('%%=ContentBlockByName("ssjs-guide-no-such-block-xyz","heroRegion",false)=%%'); });
attempt("AMPscript arity 5 with a statusVariable", function () { return Platform.Function.TreatAsContent('%%[ var @s ]%%%%=ContentBlockByName("ssjs-guide-test-block","heroRegion",false,"FALLBACK",@s)=%%'); });
// OBSERVED: SSJSGUIDE-TEST-BLOCK-OK / FALLBACK / empty string /
// SSJSGUIDE-TEST-BLOCK-OK - the documented semantics are implemented, just
// not reachable from SSJS.
</script>
Platform.Function.BeginImpressionRegion — unusable from SSJS
No SSJS call shape works. Every invocation is rejected at runtime with a resolved-value (ResolvedValueParameter) error (OMM_FUNC_SYNTAX_ERR, ExactTarget.OMM.InvalidFunctionException) — a compile-time string literal, a number literal, a string concatenation, the empty string and a variable all fail identically. The official docs present it as an ordinary callable function taking a string region name and mention none of this.
Why: the AMPscript parser requires the region name to be a literal token. An SSJS argument always reaches the function as a computed value (Parameter Type: ResolvedValueParameter) and is rejected before its content is examined, which is why even a compile-time literal fails from SSJS. The rule is real and observable inside AMPscript too: BeginImpressionRegion(@r) throws with Parameter Type: ResolvedVariableParameter, and a nested function call (BeginImpressionRegion(Concat(...))) is rejected as well, while an AMPscript string literal is accepted.
An earlier reading attributed the CloudPage failures to a missing send/impression-tracking context. That explanation is disproven: the AMPscript literal form runs without error on a plain CloudPage GET in the same request in which every SSJS shape throws.
Scope of the evidence — CloudPage only. Every result above was obtained from plain CloudPage GET requests. No measurement was taken inside a real email send, although impression regions are chiefly a send-tracking feature — so the send-time behaviour remains untested and could differ.
The equivalent AMPscript literal call succeeds in the very same content, so the feature itself works and only the SSJS binding is blocked — impression regions are effectively an AMPscript-only feature. From an SSJS context the workaround is to emit the AMPscript form through Platform.Function.TreatAsContent(), which does not throw. A dynamic name still works if it is spliced into the AMPscript source string, because the parser then sees a literal.
Core-library equivalent: BeginImpressionRegion() is the bare-name Core form (available after Platform.Load("core", ...)) — it throws the same error, so it is no escape hatch.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// NOTE: the CLR exception message cannot be inspected with .length / .indexOf /
// .substring - those abort the CloudPage with HTTP 422. Print it verbatim only.
function attempt(id, fn) {
try { fn(); Platform.Response.Write(id + ": did NOT throw\n"); }
catch (e) { Platform.Response.Write(id + ": threw: " + e.message + "\n"); }
}
// Every SSJS argument shape is rejected - literal included.
attempt("string literal", function () { return Platform.Function.BeginImpressionRegion("MyRegion"); });
attempt("concatenation", function () { return Platform.Function.BeginImpressionRegion("My" + "Region"); });
var regionName = "MyRegion";
attempt("variable", function () { return Platform.Function.BeginImpressionRegion(regionName); });
attempt("bare-name Core form", function () { return BeginImpressionRegion("MyRegion"); });
// OBSERVED: all four throw the identical ResolvedValueParameter error.
// The AMPscript route succeeds in the same request.
attempt("TreatAsContent AMPscript form", function () {
return Platform.Function.TreatAsContent('%%[BeginImpressionRegion("MyRegion")]%%');
});
// OBSERVED: did NOT throw - the feature works, only the SSJS binding is blocked.
// The literal-only rule lives in the AMPscript parser: an AMPscript variable
// is rejected as ResolvedVariableParameter, a spliced-in name is accepted.
attempt("AMPscript variable argument", function () {
return Platform.Function.TreatAsContent('%%[ var @r set @r = "MyRegion" BeginImpressionRegion(@r) ]%%');
});
// OBSERVED: threw - inner exception reports Parameter Type: ResolvedVariableParameter.
var dyn = "built-at-runtime";
attempt("JS-built name spliced into the AMPscript source", function () {
return Platform.Function.TreatAsContent('%%[ BeginImpressionRegion("' + dyn + '") ]%%');
});
// OBSERVED: did NOT throw - the parser sees a literal, so a dynamic name is possible.
</script>
Platform.Function.CreateObject — returns a CLR host object, not a readable plain object
The docs type the return value as a plain object. At runtime it is a .NET CLR host object — typeof reports "clr" for DataExtensionObject, Subscriber and APIProperty alike, and the engine hard-blocks introspection of it:
String(obj)andPlatform.Function.Stringify(obj)yield only the .NET type name (e.g.ExactTarget.Integration.WSDL.Subscriber), never the field values — before and after the object was populated.- Dot access and bracket access both throw
Use of Common Language Runtime (CLR) is not allowed, andfor..inenumerates zero keys.
So properties assigned with SetObjectProperty() or AddObjectArrayItem() cannot be read back from SSJS. Unreadable is not unset: the only way to prove a value landed is to submit the object through an Invoke* call and read the result back from the API — a round trip through InvokeCreate plus InvokeRetrieve does confirm the values arrived. Each call also returns a new, independent instance. For most SOAP work WSProxy is the simpler option.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// The CreateObject result is a CLR host object whose fields cannot be read back from SSJS.
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
var sub = Platform.Function.CreateObject("Subscriber");
assert("DEV typeof CreateObject('Subscriber') is clr (docs imply a plain object)", typeof sub, "clr");
// OBSERVED: clr
Platform.Response.Write("DEV String(obj) -> [" + String(sub) + "]\n");
Platform.Response.Write("DEV Stringify(obj) -> [" + Platform.Function.Stringify(sub) + "]\n");
// OBSERVED: both print only ExactTarget.Integration.WSDL.Subscriber — never the values.
Platform.Function.SetObjectProperty(sub, "SubscriberKey", "ssjs-guide-probe");
assertThrows("DEV dot access on the populated object throws", function () { return sub.SubscriberKey; });
assertThrows("DEV bracket access on the populated object throws", function () { return sub["SubscriberKey"]; });
var keyCount = 0;
for (var k in sub) { keyCount += 1; }
assert("DEV for..in over the CLR object yields 0 keys", "" + keyCount, "0");
// OBSERVED: the set SUCCEEDED — unreadable is not unset. Prove values landed by
// round-tripping the object through InvokeCreate + InvokeRetrieve instead.
var a = Platform.Function.CreateObject("APIProperty");
var b = Platform.Function.CreateObject("APIProperty");
assert("each call returns a new, independent instance", a === b ? "true" : "false", "false");
</script>
Platform.Function.AddObjectArrayItem — returns null (not the documented object[])
The function only works on ExactTarget.Integration.WSDL API objects created via Platform.Function.CreateObject — it appends the item to that object’s array property in place. Passing a plain JS object (e.g. { Items: [] }) throws AddArrayItem only works for objects in the ExactTarget.Integration.WSDL namespace (OMM_FUNC_EXEC_ERROR). On a valid WSDL object the call returns a genuine JS null (typeof "object", strict === null is true) — not undefined and not a meaningful return value; capture the mutated object, not the return.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// AddObjectArrayItem only works on ExactTarget.Integration.WSDL objects (CreateObject), not plain JS objects.
// Probe A: plain JS object -> throws a namespace error.
try {
var obj = { Attributes: [] };
var retA = Platform.Function.AddObjectArrayItem(obj, "Attributes", "x");
Platform.Response.Write("A plain-object return typeof: " + (typeof retA) + "\n");
} catch (e) {
// OBSERVED: "AddArrayItem only works for objects in the ExactTarget.Integration.WSDL namespace".
Platform.Response.Write("A plain-object ERROR: " + Platform.Function.Stringify(e).substring(0,180) + "\n");
}
// Probe B: WSDL Subscriber object with an Attribute array item.
try {
var sub = Platform.Function.CreateObject("Subscriber");
var attr = Platform.Function.CreateObject("Attribute");
Platform.Function.SetObjectProperty(attr, "Name", "FirstName");
Platform.Function.SetObjectProperty(attr, "Value", "Jane");
var retB = Platform.Function.AddObjectArrayItem(sub, "Attributes", attr);
// OBSERVED: typeof "object", retB === null is true (returns genuine null, not undefined).
Platform.Response.Write("B WSDL return typeof: " + (typeof retB) + "\n");
Platform.Response.Write("B WSDL return === null: " + (retB === null) + "\n");
} catch (e) { Platform.Response.Write("B WSDL ERROR: " + Platform.Function.Stringify(e).substring(0,180) + "\n"); }
</script>
Platform.Function.SetObjectProperty — returns null (not void), validates the property
The official docs type the return as void, but at runtime the call returns a genuine JS null (typeof "object", strict === null is true) on success. The property name is validated against the object’s SOAP schema at set-time: setting an unknown property (or a value the property rejects) throws. The assigned property cannot be read back from SSJS — the object is a .NET CLR host object (typeof "clr") and the engine blocks all introspection of it. Runtime-proven: dot and bracket property access both throw Use of Common Language Runtime (CLR) is not allowed, for..in yields zero keys, and both Stringify() and String() return only the type name ("ExactTarget.Integration.WSDL.Subscriber") rather than the values. Pass the populated object straight into the consuming SOAP call instead.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// SetObjectProperty returns genuine JS null on success; target is a CLR host object.
try {
// A Subscriber CLR object created via the API; property set is schema-validated.
var sub = Platform.Function.CreateObject("Subscriber");
var ret = Platform.Function.SetObjectProperty(sub, "EmailAddress", "test@example.com");
// OBSERVED: return typeof "object", === null: true.
Platform.Response.Write("return typeof: " + (typeof ret) + "\n");
Platform.Response.Write("=== null: " + (ret === null) + "\n");
} catch (e) { Platform.Response.Write("P1 ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The assigned property CANNOT be read back — CLR introspection is blocked.
try {
var sub2 = Platform.Function.CreateObject("Subscriber");
Platform.Function.SetObjectProperty(sub2, "EmailAddress", "blocked@example.com");
var v = sub2.EmailAddress;
Platform.Response.Write("read-back (unexpected): " + v + "\n");
} catch (e) {
// OBSERVED: "Use of Common Language Runtime (CLR) is not allowed" (System.Security.SecurityException).
Platform.Response.Write("read-back ERROR (expected CLR block): " + Platform.Function.Stringify(e).substring(0,140) + "\n");
}
// Unknown property is rejected at set-time (schema validation).
try {
var sub3 = Platform.Function.CreateObject("Subscriber");
Platform.Function.SetObjectProperty(sub3, "NotARealProperty", "x");
Platform.Response.Write("unknown-prop (unexpected OK)\n");
} catch (e) {
// OBSERVED: SetObjectProperty function-execution error (property not on the SOAP schema).
Platform.Response.Write("unknown-prop ERROR (expected): " + Platform.Function.Stringify(e).substring(0,140) + "\n");
}
</script>
Platform.Function.InsertDE / UpdateDE / UpsertDE / DeleteDE — run on CloudPages, return null
Reference: InsertDE · UpdateDE · UpsertDE · DeleteDE
The official docs restrict the *DE variants to email contexts, but at runtime all four execute and commit their write on a CloudPage too (verified within the same request and across requests). They return null rather than the affected-row count that the *Data variants return, so the *Data functions remain preferable outside email when you need the count.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// RUNTIME-PROVEN: all four *DE and all four *Data functions execute AND COMMIT on a CloudPage
// (verified by reading the row back in a second request). *Data returns the affected-row count
// (typeof number); *DE returns null. Use the ARRAY signature — the flat/variadic (AMPscript-style)
// form throws "Unable to retrieve security descriptor for this frame", which is the engine's generic
// wrong-arity / no-matching-overload signal, NOT a security/auth/CloudPage-context error.
// Signatures: Insert(deName, [cols], [vals]); Delete(deName, [whereCols], [whereVals]);
// Update/Upsert(deName, [whereCols], [whereVals], [cols], [vals]).
var de = "SSJSGUIDE_VERIFY"; // fixture: PK "Pk" (Text), data col "Txt" (Text)
try {
// *Data variant — array form; returns the affected-row count (a number).
var cnt = Platform.Function.InsertData(de, ["Pk","Txt"], ["k-demo-1","insData"]);
Platform.Response.Write("InsertData return typeof: " + (typeof cnt) + " val=" + cnt + "\n");
} catch (e) { Platform.Response.Write("InsertData ERROR: " + Platform.Function.Stringify(e).substring(0,200) + "\n"); }
try {
// *DE variant — array form; executes and commits, returns null (no row count).
var ret = Platform.Function.InsertDE(de, ["Pk","Txt"], ["k-demo-2","insDE"]);
Platform.Response.Write("InsertDE return typeof: " + (typeof ret) + " === null: " + (ret === null) + "\n");
} catch (e) { Platform.Response.Write("InsertDE ERROR: " + Platform.Function.Stringify(e).substring(0,200) + "\n"); }
</script>
Platform.Function.IsPhoneNumber — NANP numbers only; punctuation ignored, + rejected
The SSJS reference page describes generic “valid phone number” validation and never mentions the North American Numbering Plan (NANP), but that is exactly what the function enforces. A value passes only when its digits form exactly 10 digits (an optional leading 1 US country code is allowed, making 11), the area code’s first digit is 2–9, and the exchange (central-office) code’s first digit is 2–9.
Punctuation is ignored, not rejected. Spaces, dots, hyphens and parentheses are stripped before validation, so "647 555 0123", "425.555.0185", "(829) 555-0142" and "1-212-555-1234" all return true, and leading/trailing whitespace is tolerated. Every other character makes the value false, including a + prefix ("+14255550142" is false even though "14255550142" is true), / and _ separators, letters, and a trailing extension such as "2125551234x99".
Non-NANP international numbers return false regardless of formatting — "0161 496 0009" (UK), "82 517 460 123" (South Korea) and "4917612345678" (Germany) all fail. So does any number whose area or exchange begins with 0 or 1: 2342345678 and 12342345678 return true, while 0342345678, 1342345678 (bad area code), 2340345678, 2341345678 (bad exchange), 22342345678 (bad leading digit) and 15551234567 (exchange 123 starts with 1) all return false. The return value is a boolean; calling with no argument throws, an array argument throws, null and undefined return false, and a numeric argument is coerced and validated. This is a shape check only — unassigned area codes such as 200 still return true — and it is not a general international phone-number validator.
The AMPscript reference for the same function documents the NANP behaviour correctly; only the SSJS reference page is misleading.
Core-library equivalent: IsPhoneNumber() is the bare-name Core form (available after Platform.Load("core", ...)) and applies the identical NANP validation.
Show test script
<script runat="server">
// OBSERVED: IsPhoneNumber enforces NANP: optional leading US "1" + 10 digits, area-code
// OBSERVED: and exchange first digit must be 2-9. 2342345678/12342345678 => true;
// OBSERVED: 0342345678, 1342345678, 2340345678, 2341345678, 22342345678, 15551234567 => false.
// OBSERVED: spaces/dots/dashes/parentheses are IGNORED; + and 00 prefixes and any other
// OBSERVED: character => false; returns boolean; no-arg throws; null => false.
Platform.Load("core","1.1.5");
/**
* Print a labelled IsPhoneNumber result.
* @param {string} label - human-readable description of the input
* @param {string} value - the phone value to test
* @returns {void}
*/
function checkPhone(label, value) {
Platform.Response.Write(label + " (" + value + "): " + Platform.Function.IsPhoneNumber(value) + "\n");
}
try {
// OBSERVED: valid 10-digit NANP -> true.
checkPhone("valid 10-digit", "2342345678");
// OBSERVED: valid with optional leading US 1 -> true.
checkPhone("1 + 10-digit", "12342345678");
// OBSERVED: area code first digit 0 or 1 -> false.
checkPhone("area 0xx", "0342345678");
checkPhone("area 1xx", "1342345678");
// OBSERVED: exchange first digit 0 or 1 -> false.
checkPhone("exch 0xx", "2340345678");
checkPhone("exch 1xx", "2341345678");
// OBSERVED: 11 digits without leading 1 -> false.
checkPhone("11 no lead-1", "22342345678");
// OBSERVED: old claim example (exchange 123 starts with 1) -> false.
checkPhone("old claim '15551234567'", "15551234567");
// OBSERVED: punctuation is IGNORED on a valid NANP number -> true.
checkPhone("with spaces", "212 555 1234");
checkPhone("with dots", "425.555.0185");
checkPhone("with parentheses", "(829) 555-0142");
// OBSERVED: + prefix -> false; 00 prefix -> false; non-NANP -> false.
checkPhone("plus prefix", "+12342345678");
checkPhone("00 prefix", "0012342345678");
checkPhone("UK landline", "0161 496 0009");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.RaiseError — only message is required; errorCode/errorNumber not exposed on the caught error
The official docs are wrong on two counts:
- They mark
currentRecipientOnly,errorCode, anderrorNumberas required, but at runtime a singlemessageargument raises correctly — those three are optional (minArgs: 1). - When the raised error is caught in a CloudPage
try/catch, the exception exposes only.message(the passed text) and.description(anExactTarget.OMM.AMPScriptRaiseErrorException); theerrorCodeanderrorNumbervalues are not surfaced on the error object — even when all four arguments are supplied,.errorCode,.number, and.errorNumberall read backundefined. The exception is a normal catchable object (.nameis"TypeError"), so on a CloudPageRaiseErrorcan be caught rather than fatally halting the page (execution continues after thecatch).
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// RaiseError: a single message arg is enough (others optional); it is CATCHABLE on a CloudPage.
Platform.Response.Write("before RaiseError\n");
try {
// Only the message argument supplied (minArgs: 1).
Platform.Function.RaiseError("custom failure text");
Platform.Response.Write("no throw: unexpected\n");
} catch (e) {
// caught: .message = passed text; .name = "TypeError"; .description = AMPScriptRaiseErrorException; codes undefined.
Platform.Response.Write(".message: " + e.message + "\n");
Platform.Response.Write(".name: " + e.name + "\n");
Platform.Response.Write(".description: " + e.description + "\n");
Platform.Response.Write(".errorCode: " + e.errorCode + "\n");
Platform.Response.Write(".number: " + e.number + "\n");
Platform.Response.Write(".errorNumber: " + e.errorNumber + "\n");
}
Platform.Response.Write("after catch: execution continued\n");
// Full 4-arg form still does NOT surface errorCode/errorNumber on the caught error.
try {
Platform.Function.RaiseError("full arg text", true, "MYCODE", 100);
} catch (e2) {
Platform.Response.Write("4arg .errorCode: " + e2.errorCode + " .number: " + e2.number + " .errorNumber: " + e2.errorNumber + "\n");
}
// OBSERVED (live 2026-07-20): single message arg raises fine (minArgs:1); caught e.name="TypeError", e.message="custom failure text", e.description="ExactTarget.OMM.AMPScriptRaiseErrorException: ... - from Jint", e.errorCode/e.number/e.errorNumber all undefined (even with 4 args); execution continued after catch (catchable, not fatal). CLAIM CONFIRMED.
</script>
Platform.Function.UrlEncode — the reserved set differs, and only the query string is encoded
The official docs name one reserved set — ! # $ & ' ( ) * + , / : ; = ? @ [ ] — and state that with encodeReservedKeywords set to true, “all reserved characters in the URL are converted to percent-encoded values”. Runtime testing shows three deviations.
The reserved set is wrong in both directions. !, (, ) and * are documented as reserved but are never encoded, while ", %, <, >, \, `, ^, {, |, } and ~ are encoded despite not being listed. The real rule is a passthrough set: alphanumerics plus - _ . ! * ( ) survive; everything else in printable ASCII is escaped.
Only the query string is processed. Neither mode touches anything before the first ? — a space in the path stays a literal space — and a value containing no ? at all is returned completely unchanged in both modes. That is what “the value must be a complete URL” means in practice, and it is why an arbitrary string cannot be encoded with this function at all.
Escapes use lowercase hex (%3d, not %3D), and non-ASCII input is only encoded in true mode, as lowercase UTF-8 byte escapes.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// The reserved set differs from the docs in BOTH directions, and only the
// query string is ever processed.
// Percent signs are built from a fragment and printed via a token so that no
// literal double-percent sequence reaches the CloudPage pre-processor.
function P(hex) { return "%" + hex; }
function san(s) {
var str = "" + s, out = "";
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i), code = str.charCodeAt(i);
if (ch === "%") { out = out + "{pct}"; }
else if (code < 32 || code > 126) { out = out + "{u" + code + "}"; }
else { out = out + ch; }
}
return out;
}
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + san(actual) + "]\n");
}
var HEAD = "http://x.com?q" + P("3d");
// Documented as reserved, but never encoded.
assert("DEV [!] is NOT encoded (docs: reserved)", Platform.Function.UrlEncode("http://x.com?q=!end", true), HEAD + "!end");
assert("DEV [(] is NOT encoded (docs: reserved)", Platform.Function.UrlEncode("http://x.com?q=(end", true), HEAD + "(end");
assert("DEV [)] is NOT encoded (docs: reserved)", Platform.Function.UrlEncode("http://x.com?q=)end", true), HEAD + ")end");
assert("DEV [*] is NOT encoded (docs: reserved)", Platform.Function.UrlEncode("http://x.com?q=*end", true), HEAD + "*end");
// Encoded although the docs do not list them as reserved.
assert("DEV [~] IS encoded (docs: not reserved)", Platform.Function.UrlEncode("http://x.com?q=~end", true), HEAD + P("7e") + "end");
assert("DEV [<] IS encoded (docs: not reserved)", Platform.Function.UrlEncode("http://x.com?q=<end", true), HEAD + P("3c") + "end");
assert("DEV [|] IS encoded (docs: not reserved)", Platform.Function.UrlEncode("http://x.com?q=|end", true), HEAD + P("7c") + "end");
// Only the query string is processed.
assert("DEV a space in the PATH is never encoded", Platform.Function.UrlEncode("http://x.com/a b?c=d e", true), "http://x.com/a b?c" + P("3d") + "d+e");
assert("DEV a value with no question mark is unchanged", Platform.Function.UrlEncode("hello world", true), "hello world");
// Lowercase hex escapes.
assert("DEV escapes use lowercase hex (spec style: uppercase)", Platform.Function.UrlEncode("http://x.com?q==end", true), HEAD + P("3d") + "end");
// OBSERVED (live 2026-08-01): every line PASS on the QA CloudPage.
</script>
Platform.Response.Redirect — second argument is optional; the call terminates the script
The official docs list movedPermanently as a required second argument and say nothing about what happens to the rest of the script. Runtime verification shows two differences. First, the second argument is optional: Platform.Response.Redirect(url) with a single argument emits a 302 with the Location header set, exactly as if false had been passed (true still yields a 301). Second, the redirect terminates script execution immediately — statements after the call never run, and wrapping the call in a try/catch does not regain control because no catchable exception is raised. Any response body written before the call is discarded in favour of the redirect payload, so cleanup or logging code placed after a redirect will silently never execute.
Show test script
<script runat="server">
// OBSERVED: a single-argument Redirect produced a 302 with the Location header set,
// and the "after redirect" line never appeared in the response (script terminated),
// not even with the call wrapped in try/catch.
Platform.Response.Write("before redirect\n");
try {
// Single argument -> 302 (same as passing false)
Platform.Response.Redirect("https://example.com/thank-you");
} catch (e) {
// NEVER reached - no catchable exception is raised
Platform.Response.Write("caught: " + String(e) + "\n");
}
// NEVER reached - the redirect ended the script
Platform.Response.Write("after redirect\n");
</script>
Platform.Response.ContentType / Platform.Response.CharacterSet — write-only - every read throws
Both properties are documented as ordinary readable/writable properties (a getter/setter pair). At runtime they are write-only. Assignment works and is reflected in the HTTP response — ContentType sets the Content-Type header and CharacterSet sets its charset — but every read throws a null-reference error, both as the very first statement of the script and after output has already been written. Calling either name as a function throws as well. Treat them as setters only and keep your own variable if you need the current value.
Show test script
<script runat="server">
// OBSERVED: assignment worked (the response carried the new Content-Type/charset),
// but every read threw "Object reference not set to an instance of an object",
// both before any output and after output had been written.
function pre(t) { Platform.Response.Write(t + "\n"); }
// Read BEFORE any assignment / any output.
try { pre("read ContentType: " + Platform.Response.ContentType); }
catch (e1) { pre("read ContentType THREW: " + String(e1)); }
try { pre("read CharacterSet: " + Platform.Response.CharacterSet); }
catch (e2) { pre("read CharacterSet THREW: " + String(e2)); }
// Assignment works.
Platform.Response.ContentType = "text/plain";
Platform.Response.CharacterSet = "UTF-8";
pre("assignment done");
// Read AFTER assignment and after output - still throws.
try { pre("read back ContentType: " + Platform.Response.ContentType); }
catch (e3) { pre("read back ContentType THREW: " + String(e3)); }
// Calling as a function throws too.
try { pre("call ContentType(): " + Platform.Response.ContentType()); }
catch (e4) { pre("call ContentType() THREW: " + String(e4)); }
</script>
Platform.Request.GetQueryStringParameter — returns null (not "") when absent
For an absent parameter this returns null (typeof "object", === null), not an empty string — the docs give no indication of the empty-vs-null behavior. A present parameter returns its string value (typeof "string"); a present-but-empty parameter (?p=) returns "" (not null), so null reliably means “absent”. Key lookup is case-insensitive (?Case=X is readable as both "case" and "Case"), and a repeated parameter (?p=1&p=2) is returned as a single comma-joined string ("1,2"). Guard reads with a truthiness / != null check.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live 2026-07-20, fetched with ?id=hello&multi=1&multi=2&Case=X&empty=):
// absent -> typeof "object", ===null true, ===""" false (returns null, NOT "");
// present 'id' -> "hello" (typeof string); multi 'multi' (?multi=1&multi=2) -> "1,2" (comma-joined string);
// case 'Case=X' readable via both "case" and "Case" -> "X" (case-INSENSITIVE keys);
// empty '?empty=' -> "" (typeof string, ===null false, ==='' true) so null only ever means absent.
// CLAIM CONFIRMED: absent returns null (typeof "object"), not "".
try {
var missing = Platform.Request.GetQueryStringParameter("definitelyNotPresent");
Platform.Response.Write("absent typeof: " + (typeof missing) + "\n");
Platform.Response.Write("absent === null: " + (missing === null) + "\n");
Platform.Response.Write("absent === '': " + (missing === "") + "\n");
var present = Platform.Request.GetQueryStringParameter("id");
Platform.Response.Write("present ('id'): [" + present + "] typeof=" + (typeof present) + "\n");
var multi = Platform.Request.GetQueryStringParameter("multi");
Platform.Response.Write("multi ('multi'): [" + multi + "] typeof=" + (typeof multi) + "\n");
Platform.Response.Write("case lower ('case'): [" + Platform.Request.GetQueryStringParameter("case") + "]\n");
Platform.Response.Write("case upper ('Case'): [" + Platform.Request.GetQueryStringParameter("Case") + "]\n");
var emptyVal = Platform.Request.GetQueryStringParameter("empty");
Platform.Response.Write("empty ('empty'): [" + emptyVal + "] ===null:" + (emptyVal === null) + " ==='':" + (emptyVal === "") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Request.GetFormField — returns null (not "") when absent
For an absent field this returns null (typeof "object"), not an empty string — on both GET and POST. A present POSTed field returns its string value; a field that is present but posted empty (field=) returns "" (typeof "string") — so null uniquely signals “field not sent”. Note the official docs already state “or null if the field was not sent”, so runtime matches the docs on the null-vs-absent point.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage): absent field -> typeof "object", === null true, === "" false (both GET and POST).
// OBSERVED: present POST field 'probeField=hello_world' -> "hello_world" (typeof "string").
// OBSERVED: present-but-empty POST field 'emptyField=' -> "" (typeof "string", === "" true) — distinct from absent (null).
Platform.Response.Write("Method = '" + Platform.Request.Method + "'\n");
// B1: ABSENT field -> null (typeof "object"), not ""
try {
var missing = Platform.Request.GetFormField("definitelyNotSubmitted");
Platform.Response.Write("absent typeof: " + (typeof missing) + " === null: " + (missing === null) + " === '': " + (missing === "") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// B2: PRESENT POST field (send a POST with body 'probeField=hello_world' to populate it)
try {
var present = Platform.Request.GetFormField("probeField");
Platform.Response.Write("present val=[" + present + "] typeof=" + (typeof present) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// B3: PRESENT-BUT-EMPTY POST field (body 'emptyField=') -> "" typeof "string", distinct from absent null
try {
var emptyVal = Platform.Request.GetFormField("emptyField");
Platform.Response.Write("empty val=[" + emptyVal + "] typeof=" + (typeof emptyVal) + " === '': " + (emptyVal === "") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Request.GetCookieValue — returns null (not "") when absent
For an absent cookie this returns null (typeof "object", === null true, === "" false), not an empty string. A cookie sent via the request Cookie header reads back as a typeof "string" value.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage GET, Cookie header probeCookie=helloWorld123):
// B1 absent -> typeof "object", === null true, === "" false, String()="null"
// B2 present (sent via Cookie header) -> typeof "string", val "helloWorld123", === null false
// Confirms: absent cookie returns null (not ""); present cookie returns the string value.
// B1: ABSENT cookie -> null (typeof "object"), NOT "".
try {
var missing = Platform.Request.GetCookieValue("definitelyNoSuchCookie_xyz");
Platform.Response.Write("B1 absent typeof: " + (typeof missing) + "\n");
Platform.Response.Write("B1 absent === null: " + (missing === null) + "\n");
Platform.Response.Write("B1 absent === '': " + (missing === "") + "\n");
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: PRESENT cookie (send request header "Cookie: probeCookie=helloWorld123") -> string value.
try {
var present = Platform.Request.GetCookieValue("probeCookie");
Platform.Response.Write("B2 present typeof: " + (typeof present) + "\n");
Platform.Response.Write("B2 present val: [" + present + "]\n");
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Variable.GetValue — returns null (not "") when never set
When the variable was never set this returns null (typeof "object"), not an empty string. A variable explicitly set to "" (via AMPscript %%[ VAR @v SET @v = "" ]%% or SSJS Platform.Variable.SetValue) returns "" (typeof "string"). The leading @ is optional — GetValue("v") and GetValue("@v") return the same value. (The bare-name Variable alias only exists after Platform.Load("core", ...).)
Show test script
<!-- Declare AMPscript variables BEFORE the script block so GetValue can read them. -->
%%[ VAR @ampVar SET @ampVar = "hello-from-amp" ]%%
%%[ VAR @ampEmpty SET @ampEmpty = "" ]%%
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage GET):
// never-set var -> typeof "object", === null true, === "" false (returns null, NOT "")
// @ampVar value -> GetValue("@ampVar") == GetValue("ampVar") == "hello-from-amp" (@ optional)
// @ampEmpty "" -> typeof "string", === "" true, === null false (explicit "" returns "")
// SetValue("@myVar","") -> read back with & without @ both "" and equal
// GetValue returns null (typeof "object") when a variable was NEVER set; "" if explicitly set to "".
// The leading @ is optional; GetValue("v") and GetValue("@v") are equivalent.
try {
var neverSet = Platform.Variable.GetValue("@neverDefinedVar12345");
// OBSERVED: typeof "object", neverSet === null is true (not an empty string).
Platform.Response.Write("never set typeof: " + (typeof neverSet) + " | === null: " + (neverSet === null) + " | === '': " + (neverSet === "") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
try {
// AMPscript-declared var with a value, read with and without the @ prefix.
var withAt = Platform.Variable.GetValue("@ampVar");
var noAt = Platform.Variable.GetValue("ampVar");
// OBSERVED: both return "hello-from-amp" and are equal; @ prefix is optional.
Platform.Response.Write("with @: '" + withAt + "' | no @: '" + noAt + "' | equal: " + (withAt === noAt) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
try {
// AMPscript var explicitly set to empty string returns "" (not null).
var emptyVal = Platform.Variable.GetValue("@ampEmpty");
// OBSERVED: typeof "string", === "" true, === null false.
Platform.Response.Write("amp empty typeof: " + (typeof emptyVal) + " | === '': " + (emptyVal === "") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Recipient.GetAttributeValue — returns "" outside a send, does not throw
Does not throw outside a send context — in a plain CloudPage it returns "" (empty string, typeof "string") for any attribute because no recipient is bound. The bare-name Recipient alias is not available even after Platform.Load; use Platform.Recipient.GetAttributeValue(...) (or Attribute.GetValue(...) after load).
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage GET): B1 typeof "string", === "" true, === null false, does NOT throw; B2 unknown attr -> "" string; B3 typeof Recipient = undefined (bare-name alias not available). Claim CONFIRMED.
// B1: Does Platform.Recipient.GetAttributeValue THROW outside a send context, or return "" ?
try {
var v = Platform.Recipient.GetAttributeValue("FirstName");
Platform.Response.Write("B1 typeof: " + (typeof v) + "\n");
Platform.Response.Write("B1 === '': " + (v === "") + "\n");
Platform.Response.Write("B1 === null: " + (v === null) + "\n");
Platform.Response.Write("B1 toString: [" + Object.prototype.toString.call(v) + "]\n");
Platform.Response.Write("B1 value: '" + v + "'\n");
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: a differently-named unknown attribute proves the "" is generic, not FirstName-specific.
try {
var v2 = Platform.Recipient.GetAttributeValue("SomeUnknownAttr_98765");
Platform.Response.Write("B2 typeof: " + (typeof v2) + " value: '" + v2 + "'\n");
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: bare-name Recipient alias is not available even after Platform.Load.
Platform.Response.Write("B3 typeof Recipient: " + (typeof Recipient) + "\n");
</script>
Script.Util.HttpRequest.emptyContentHandling — numeric, not boolean
The official docs type this as a boolean, but at runtime the property is a System.Byte (numeric): its default value is 0, numeric assignments (0/1) are accepted, and assigning true/false throws “Object of type ‘System.Boolean’ cannot be converted to type ‘System.Byte’.” — identical to Script.Util.HttpGet. The runtime empty-body semantics are also the inverse of the documented boolean: with an empty (204) response, emptyContentHandling = 0 does NOT throw while emptyContentHandling = 1 DOES throw “The HTTP request call completed but returned no content.” — so 0 (default) tolerates empty content and 1 raises on empty content.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
// Endpoint that returns an EMPTY body (204 No Content).
var emptyUrl = "https://postman-echo.com/status/204";
// Probe 1: default value + typeof of emptyContentHandling on a fresh request
try {
var r1 = new Script.Util.HttpRequest(emptyUrl);
pre("default emptyContentHandling = " + r1.emptyContentHandling + " (typeof " + (typeof r1.emptyContentHandling) + ")");
// OBSERVED: default emptyContentHandling = 0 (typeof clr) - a numeric System.Byte, not a boolean
} catch (e) {
pre("B1 THREW -> " + Platform.Function.Stringify(e));
}
// Probe 2: numeric 0 and 1 are accepted and read back unchanged
try {
var r2 = new Script.Util.HttpRequest(emptyUrl);
r2.emptyContentHandling = 0;
pre("after set 0 -> " + r2.emptyContentHandling);
var r3 = new Script.Util.HttpRequest(emptyUrl);
r3.emptyContentHandling = 1;
pre("after set 1 -> " + r3.emptyContentHandling);
// OBSERVED: set 0 -> 0, set 1 -> 1 (numeric values accepted)
} catch (e) {
pre("B2 THREW -> " + Platform.Function.Stringify(e));
}
// Probe 3: boolean true/false are REJECTED (docs wrongly type this as boolean)
try {
var r4 = new Script.Util.HttpRequest(emptyUrl);
r4.emptyContentHandling = true;
pre("boolean accepted?? " + r4.emptyContentHandling);
} catch (e) {
pre("set true THREW -> " + Platform.Function.Stringify(e));
// OBSERVED: throws "Object of type 'System.Boolean' cannot be converted to type 'System.Byte'." (same for false)
}
// Probe 4: empty-body semantics - 0 tolerates empty content, 1 throws on empty content
try {
var r5 = new Script.Util.HttpRequest(emptyUrl);
r5.method = "GET";
r5.emptyContentHandling = 0;
var resp5 = r5.send();
pre("numeric 0 + empty body: statusCode = " + resp5.statusCode + " (did NOT throw)");
// OBSERVED: statusCode 204, did NOT throw
} catch (e) {
pre("0 + empty THREW -> " + Platform.Function.Stringify(e));
}
try {
var r6 = new Script.Util.HttpRequest(emptyUrl);
r6.method = "GET";
r6.emptyContentHandling = 1;
var resp6 = r6.send();
pre("numeric 1 + empty body: statusCode = " + resp6.statusCode);
} catch (e) {
pre("1 + empty THREW -> " + Platform.Function.Stringify(e));
// OBSERVED: throws "The HTTP request call completed but returned no content." on empty body when set to 1
}
</script>
Script.Util.HttpRequest response headers — for..in only, never direct index
Response headers are readable, but only through the headers property and only by enumerating it — never by direct indexing. On the object returned by send(), resp.headers is a CLR object (typeof is clr). Direct access by name — resp.headers["Content-Type"] — throws “Use of Common Language Runtime (CLR) is not allowed.” The supported path is a for..in over resp.headers, which yields keys shaped "[Name, Value]"; you read each header by parsing that key string (strip the surrounding [ ], split on the first ", ") rather than dereferencing a CLR value. This contradicts the official example, which indexes a header directly.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
// Public echo endpoint that returns known response headers.
var url = "https://postman-echo.com/get";
var resp = null;
try {
var req = new Script.Util.HttpRequest(url);
req.method = "GET";
resp = req.send();
pre("send ok: statusCode = " + resp.statusCode);
} catch (e) {
pre("SEND THREW -> " + Platform.Function.Stringify(e));
}
// Probe 1: resp.headers is a CLR object (the documented, working accessor)
try {
pre("typeof resp.headers = " + (typeof resp.headers));
// OBSERVED: "clr" - a CLR object you enumerate
} catch (e) { pre("typeof THREW -> " + e.message); }
// Probe 2: direct indexing by name throws the CLR error - never do this
try {
var v1 = resp.headers["Content-Type"];
pre("resp.headers[\"Content-Type\"] = " + v1);
} catch (e) {
pre("resp.headers[\"Content-Type\"] THREW -> " + e.message);
// OBSERVED: throws "Use of Common Language Runtime (CLR) is not allowed"
}
// Probe 3: the supported path - for..in yields "[Name, Value]" keys; parse the KEY string
// Build a plain { name: value } map without ever reading a CLR value.
function getHeaderMap(r) {
var map = {};
for (var k in r.headers) {
var pair = String(k);
if (pair.charAt(0) === "[") { pair = pair.substring(1); }
if (pair.charAt(pair.length - 1) === "]") { pair = pair.substring(0, pair.length - 1); }
var idx = pair.indexOf(", ");
if (idx > -1) { map[pair.substring(0, idx).toLowerCase()] = pair.substring(idx + 2); }
}
return map;
}
try {
var headers = getHeaderMap(resp);
pre("parsed content-type = " + headers["content-type"]);
// OBSERVED: "application/json; charset=utf-8" - headers ARE readable via the key-parse pattern
} catch (e) { pre("getHeaderMap THREW -> " + Platform.Function.Stringify(e)); }
pre("=== END ===");
</script>
Script.Util.HttpRequest.encoding — defaults to Windows-1252, not UTF-8
The official docs give UTF-8 as the example value for the request encoding property, which reads as though that is what a request uses by default. On a fresh new Script.Util.HttpRequest(url) instance the property actually reads back Windows-1252, so a UTF-8 body is not encoded as UTF-8 unless encoding is set explicitly. The property is writable, but the runtime normalises the read-back to lower case: assigning "UTF-8" reads back as "utf-8".
Show test script
<script runat="server">
/*
* Chapter: HttpRequestInstance Properties
*
* Proves, for each documented property, its DEFAULT value and whether it is
* writable:
* 1. method default "GET", writable, accepts POST/PUT/PATCH/DELETE
* 2. contentType default "" (empty), writable
* 3. encoding writable; an assigned value reads back LOWER-CASED
* 4. timeout default 30, writable
* 5. emptyContentHandling default 0, accepts 1 and 2
* 6. retries default 1, writable
* 7. continueOnError default false, writable
*
* DEVIATIONS from the official Salesforce docs, each marked "DEV":
* - encoding defaults to "Windows-1252", not the documented "UTF-8".
* - postData is WRITE-ONLY: every read throws "Property Get method was
* not found.", although the docs list it as a readable property.
* - timeout is not documented at all; it exists and defaults to 30,
* matching the documented 30-second send() timeout, so the unit is
* SECONDS — not milliseconds.
* - emptyContentHandling is documented as a boolean but is numeric at
* runtime: true/false are rejected outright.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
var got;
try { got = "" + actual; } catch (ex) { got = "THREW: " + ("" + ex.message); }
Platform.Response.Write((got === "" + expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
function assertThrowsFragment(id, fn, fragment) {
var msg = "";
try { fn(); msg = "did NOT throw"; } catch (ex) { msg = "" + ex.message; }
Platform.Response.Write((msg.indexOf(fragment) > -1 ? "PASS " : "FAIL ") + id + " -> " + msg + "\n");
}
var req = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
/* 1. method — default and writability. */
assert("method default is GET", req.method, "GET");
req.method = "POST";
assert("method accepts POST", req.method, "POST");
req.method = "PUT";
assert("method accepts PUT", req.method, "PUT");
req.method = "PATCH";
assert("method accepts PATCH", req.method, "PATCH");
req.method = "DELETE";
assert("method accepts DELETE", req.method, "DELETE");
req.method = "GET";
/* 2. contentType — default empty, writable. */
var fresh = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("contentType default is empty", fresh.contentType, "");
fresh.contentType = "application/json";
assert("contentType is writable", fresh.contentType, "application/json");
/* 3. DEVIATION — encoding default is Windows-1252, not the documented UTF-8. */
var encReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("DEV encoding default is Windows-1252 (docs: UTF-8)", encReq.encoding, "Windows-1252");
encReq.encoding = "UTF-8";
assert("DEV assigned encoding reads back lower-cased (docs: UTF-8)", encReq.encoding, "utf-8");
/* 4. DEVIATION — timeout is undocumented; default 30 proves the unit is seconds. */
var toReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("DEV timeout default is 30 seconds (docs: property not listed)", toReq.timeout, "30");
toReq.timeout = 45;
assert("timeout is writable", toReq.timeout, "45");
/* 5. DEVIATION — postData is write-only: assignment works, every read throws. */
var pdReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assertThrowsFragment("DEV reading postData before assignment throws (docs: readable property)", function () {
return pdReq.postData;
}, "Property Get method was not found.");
pdReq.postData = "hello-body";
assertThrowsFragment("DEV reading postData after assignment still throws (docs: readable property)", function () {
return pdReq.postData;
}, "Property Get method was not found.");
/* 6. DEVIATION — emptyContentHandling is numeric, not the documented boolean. */
var ecReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("emptyContentHandling default is 0", ecReq.emptyContentHandling, "0");
ecReq.emptyContentHandling = 1;
assert("emptyContentHandling accepts 1 (stop)", ecReq.emptyContentHandling, "1");
ecReq.emptyContentHandling = 2;
assert("emptyContentHandling accepts 2 (next subscriber)", ecReq.emptyContentHandling, "2");
assertThrowsFragment("DEV emptyContentHandling rejects true (docs: boolean)", function () {
ecReq.emptyContentHandling = true;
}, "cannot be converted");
assertThrowsFragment("DEV emptyContentHandling rejects false (docs: boolean)", function () {
ecReq.emptyContentHandling = false;
}, "cannot be converted");
/* 7. retries and continueOnError — defaults and writability. */
var rReq = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
assert("retries default is 1", rReq.retries, "1");
rReq.retries = 2;
assert("retries is writable", rReq.retries, "2");
assert("continueOnError default is false", rReq.continueOnError, "False");
rReq.continueOnError = true;
assert("continueOnError is writable", rReq.continueOnError, "True");
</script>
Script.Util.HttpResponse.encoding — always empty
The official docs list encoding as a populated property of the response object — “the encoding type returned in the response”. At runtime it is an empty string on every request handler, including Script.Util.HttpRequest, and it stays empty even when the response Content-Type carries a charset (a response typed text/plain; charset=utf-8 still yields ""). The charset is only obtainable from the content-type header, read through the for..in enumeration of resp.headers.
Show test script
<script runat="server">
/*
* Chapter: HttpResponseInstance Properties
*
* Proves, for the object returned by send():
* 1. content is a CLR string that must be converted before use.
* 2. contentType IS populated on Script.Util.HttpRequest.
* 3. headers is a CLR object whose for..in enumeration yields real entries.
* 4. returnStatus is 0 (OK) for a successful call.
* 5. statusCode carries the HTTP status.
*
* DEVIATIONS from the official Salesforce docs, each marked "DEV":
* - encoding is documented as a populated response property but is
* ALWAYS an empty string — even when the response Content-Type
* carries a charset.
* - statusCode and returnStatus are CLR values, so strict equality
* against a JavaScript number literal is always false.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
var got;
try { got = "" + actual; } catch (ex) { got = "THREW: " + ("" + ex.message); }
Platform.Response.Write((got === "" + expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var req = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
req.method = "GET";
var resp = req.send();
/* 1. content is a CLR value, not a JS string. */
assert("content is a CLR value", typeof resp.content, "clr");
assert("content converts to a non-empty JS string", ("" + resp.content).length > 0, "true");
/* 2. contentType IS populated on HttpRequest. */
assert("contentType is populated", ("" + resp.contentType).length > 0, "true");
assert("contentType carries the charset", ("" + resp.contentType).indexOf("charset") > -1, "true");
/* 3. DEVIATION — encoding is empty although the content-type has a charset. */
assert("DEV encoding is always empty (docs: populated response property)", resp.encoding, "");
/* 4. headers is a CLR object with real enumerable entries. */
assert("headers is a CLR object", typeof resp.headers, "clr");
var headerCount = 0;
for (var k in resp.headers) { headerCount = headerCount + 1; }
assert("headers enumeration yields real entries", headerCount > 0, "true");
/* 5. returnStatus / statusCode values. */
assert("returnStatus is 0 (OK)", resp.returnStatus, "0");
assert("statusCode is 200", resp.statusCode, "200");
/* 6. DEVIATION — CLR values never satisfy strict equality with a number. */
assert("DEV statusCode === 200 is false (docs: number)", resp.statusCode === 200 ? "true" : "false", "false");
assert("DEV returnStatus === 0 is false (docs: number)", resp.returnStatus === 0 ? "true" : "false", "false");
</script>
Script.Util.HttpResponse.statusCode / returnStatus — CLR values — strict equality never matches
The official docs type statusCode and returnStatus as numbers. At runtime both are CLR values (typeof reports clr), so strict equality against a JavaScript number literal is always false: resp.statusCode === 200 does not match on a 200 response, and neither does resp.returnStatus === 0 on a successful call. switch (resp.statusCode) fails the same way and silently runs the default branch. Convert once with Number(resp.statusCode) — that produces a real JavaScript number which === and switch both match.
Loose comparison (resp.statusCode == 200) does return the right answer on a populated status code, and the test below asserts that, but it is not the idiom to reach for: against a CLR value backed by a .NET null, == throws Value cannot be null. Parameter name: value for every operand type, whereas === returns false safely. Relational operators (>= 400, < 300) are the one comparison family that is already correct on the raw value.
Show test script
<script runat="server">
/*
* Chapter: Checking the status code
*
* Proves:
* 1. resp.statusCode is a CLR value, NOT a JavaScript number.
* 2. Strict equality against a number literal is ALWAYS false — this is
* the deviation the chapter warns about (marked DEV).
* 3. Loose equality (==) does discriminate correctly on a POPULATED
* status code — recorded for completeness, not as a recommendation:
* == throws on a CLR value backed by a .NET null, so the chapter
* steers readers to Number() + === instead.
* 4. The Number() conversion the chapter recommends makes === work.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
var got;
try { got = "" + actual; } catch (ex) { got = "THREW: " + ("" + ex.message); }
Platform.Response.Write((got === "" + expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var req = new Script.Util.HttpRequest("https://ssjs.guide/robots.txt");
req.method = "GET";
var resp = req.send();
/* 1. statusCode is a CLR value. */
assert("typeof statusCode is clr, not number", typeof resp.statusCode, "clr");
assert("statusCode stringifies to 200", "" + resp.statusCode, "200");
/* 2. DEVIATION — strict equality never matches a JS number literal. */
assert("DEV statusCode === 200 is false (docs type it number)", resp.statusCode === 200 ? "true" : "false", "false");
assert("DEV statusCode === 404 is also false", resp.statusCode === 404 ? "true" : "false", "false");
/* 3. Loose equality discriminates on a populated status code — but it
* throws on a .NET-null-backed CLR value, so it is not the idiom. */
assert("statusCode == 200 is true", resp.statusCode == 200 ? "true" : "false", "true");
assert("statusCode == 404 is false", resp.statusCode == 404 ? "true" : "false", "false");
/* 4. recommended — convert once with Number(), then === works. */
var status = Number(resp.statusCode);
assert("workaround typeof Number(statusCode) is number", typeof status, "number");
assert("workaround Number(statusCode) === 200 is true", status === 200 ? "true" : "false", "true");
</script>
<AccountInstance>.Update — returns "Error", does not throw on failure
The official docs state the call throws on failure, but at runtime <AccountInstance>.Update(properties) returns the plain string "Error" instead of throwing (the success return is the string "OK"). typeof acct.Update is function, and calling .Update({...}) on a failing (non-existent-key) account returns the string "Error" (typeof string) without throwing.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: acct.Update is a function; calling .Update({...}) on a failing (bogus-key) account
// OBSERVED: returns the STRING "Error" (typeof "string") and does NOT throw — matches the claim.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: Init an Account instance with a bogus external key and inspect .Update existence/type (NON-DESTRUCTIVE)
try {
var acct = Account.Init("NON_EXISTENT_KEY_QA_PROBE");
pre("B1 typeof acct = " + (typeof acct)); // OBSERVED: object
pre("B1 typeof acct.Update = " + (typeof acct.Update)); // OBSERVED: function
} catch (e) {
pre("B1 THREW -> " + Platform.Function.Stringify(e));
}
// B2: NON-DESTRUCTIVE failure probe — Update on a bogus account with an invalid payload.
// The account key does not exist, so nothing real is mutated.
try {
var acct2 = Account.Init("NON_EXISTENT_KEY_QA_PROBE");
var res = acct2.Update({ "FromName": "" });
pre("B2 Update() returned: typeof=" + (typeof res) + " value=" + Platform.Function.Stringify(res));
// OBSERVED: typeof=string value="Error" (does NOT throw)
} catch (e) {
pre("B2 THREW -> " + Platform.Function.Stringify(e));
}
</script>
<EmailInstance>.Validate — ValidationStatus is a string; ValidationMessages is null or an array
The official docs type Task.ValidationStatus as a boolean and Task.ValidationMessages as a string, but at runtime <EmailInstance>.Validate() returns ValidationStatus as a string (e.g. "Pass" / "Fail") and ValidationMessages as null on Pass or an array of {Location, Message, Description} objects on Fail. Compare the status against string values, not true/false. Two extra runtime facts: (1) the email must be initialized by its CustomerKey string — Email.Init(numericID).Validate() throws the opaque string "Error Validating Email", whereas Email.Init(customerKey).Validate() returns the Task object; (2) do not treat ValidationMessages as a single string. The whole classic Email object (Init/Add/Retrieve/Update/Remove/Validate/CheckContent) is deprecated (legacy classic Email Studio type) but not broken.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): Email.Init(CustomerKey).Validate() returns
// {Task:{ValidationStatus:"Fail", ValidationMessages:[{Location,Message,Description}], ...}, StatusCode:"OK", ...}
// -> typeof Task.ValidationStatus === "string" (value "Fail"), NOT boolean. CLAIM CONFIRMED.
// Note: Email.Init(numericID).Validate() throws the string "Error Validating Email";
// initializing by CustomerKey is required to get the Task back.
try {
// Find a real classic email and use its CustomerKey (numeric ID throws on Validate)
var list = Email.Retrieve({ Property: "IsActive", SimpleOperator: "equals", Value: "true" });
var ckey = (list && list.length) ? list[0].CustomerKey : "ssjs_verify_tsd_email";
var email = Email.Init(ckey);
var task = email.Validate();
// task.Task.ValidationStatus is a STRING (e.g. "Fail"), NOT a boolean.
Platform.Response.Write("typeof Task.ValidationStatus: " + typeof task.Task.ValidationStatus + "\n");
Platform.Response.Write("ValidationStatus value: " + Platform.Function.Stringify(task.Task.ValidationStatus) + "\n");
Platform.Response.Write("ValidationMessages typeof: " + typeof task.Task.ValidationMessages + " hasLength=" + (task.Task.ValidationMessages && typeof task.Task.ValidationMessages.length != "undefined") + "\n");
// Correct guard: compare against string, not boolean
Platform.Response.Write("is Fail? " + (String(task.Task.ValidationStatus) == "Fail") + "\n");
} catch (e) {
Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n");
}
</script>
ContentAreaObj.Add — returns an initialized instance, not "OK"
The official Add reference annotates the return as @returns {Enum("OK")}, but at runtime ContentAreaObj.Add(properties) returns an initialized ContentAreaObjInstance — an object exposing Update/Remove, identical in shape to ContentAreaObj.Init. This matches the doc’s own H1 summary (“returns an initialized object”) rather than the @returns annotation. It is not the string "OK". typeof of the return is object, and Stringify of it yields {"Remove":"function","Update":"function"} — identical to ContentAreaObj.Init’s return.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: CONFIRMED — ContentAreaObj.Add({...}) returns typeof=object exposing
// Update/Remove (Stringify={"Remove":"function","Update":"function"}), identical to
// ContentAreaObj.Init's return; it is NOT the string "OK".
function pre(t) { Platform.Response.Write(t + "\n"); }
function shape(desc, obj) {
try { pre(desc + " typeof.Update=" + (typeof obj.Update)); } catch (x1) { pre(desc + " .Update read threw"); }
try { pre(desc + " typeof.Remove=" + (typeof obj.Remove)); } catch (x2) { pre(desc + " .Remove read threw"); }
}
pre("=== ContentAreaObj.Add — NON-DESTRUCTIVE ===");
// Baseline: what does Init(key) return (for shape comparison)
try {
var inst = ContentAreaObj.Init("__qa_probe_key_nonexistent__");
pre("Init RETURNED typeof=" + (typeof inst));
shape("Init-return", inst);
} catch (e1) { pre("Init THREW -> " + Platform.Function.Stringify(e1)); }
// THE CLAIM: Add(props) — docs say @returns Enum("OK"); expect an initialized instance
try {
var addRet = ContentAreaObj.Add({ CustomerKey: "__qa_probe_invalid__", Name: "", Content: "" });
pre("Add RETURNED typeof=" + (typeof addRet));
pre("Add Stringify=" + Platform.Function.Stringify(addRet));
shape("Add-return", addRet);
pre("Add is exactly string 'OK'? " + (String(addRet) === "OK"));
} catch (e2) { pre("Add THREW -> " + Platform.Function.Stringify(e2)); }
pre("=== END ===");
</script>
Portfolio.Add / Portfolio.Retrieve / <PortfolioInstance>.Remove — return "Error" instead of throwing; Remove OK on already-deleted; never-existed returns Error; Retrieve is array-like, filter optional
Portfolio is a legacy Classic Content file feature (Classic Content reached end of life on 24 Apr 2023 — prefer Content Builder Asset REST endpoints). The static namespace exposes Init, Add and Retrieve; Update and Remove exist only as instance methods (Portfolio.Init(key).Remove()), and Portfolio.Update as a static is undefined. Portfolio.Init never validates the key — it returns an instance carrying Update and Remove even for a key that does not exist and even when called with no argument, so it cannot serve as an existence check; the instance is a host object, so String(instance) throws Object reference not set to an instance of an object. Both the CustomerKey and the ObjectID are accepted as the key. The docs say Add and Remove return "OK" on success or throw on failure — they do not throw: Add() with no argument returns the plain string "Error", so test the return value rather than relying on try/catch. Remove is worse: it returns "OK" for an already-deleted item (so the return value is not a usable success signal — confirm with a follow-up Retrieve), while a key that never existed returns the plain string "Error" rather than throwing. Portfolio.Retrieve returns a collection that is array-like but not a real JS array (instanceof Array is false, though .length, .push, .slice and index access work), a filter that matches nothing yields a zero-length collection rather than null, and the filter argument is optional in practice (no argument returns every item), while a non-object filter throws Error Retrieving Portfolios. <PortfolioInstance>.Update has no working invocation at all — see its own entry.
Show test script
<script runat="server">
// OBSERVED: Portfolio.Add() with no argument returned the string "Error" (no throw).
// Portfolio.Init returned a working instance for a key that does not exist (and for no argument at all),
// so it is not an existence check; stringifying that instance throws.
// <PortfolioInstance>.Remove() returned "OK" for an already-deleted item; a never-existed key returns "Error".
// Portfolio.Retrieve() with no filter returned every item; instanceof Array was false.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
// Static namespace: Init/Add/Retrieve are functions; Update/Remove are undefined as statics (instance-only).
pre("typeof Portfolio.Init=" + (typeof Portfolio.Init));
pre("typeof Portfolio.Add=" + (typeof Portfolio.Add));
pre("typeof Portfolio.Update=" + (typeof Portfolio.Update));
pre("typeof Portfolio.Remove=" + (typeof Portfolio.Remove));
pre("typeof Portfolio.Retrieve=" + (typeof Portfolio.Retrieve));
// Add with no argument — non-destructive: returns "Error", no throw.
try {
var addRet = Portfolio.Add();
pre("Add() returned: " + Platform.Function.Stringify(addRet) + " (=='OK'? " + (String(addRet) === "OK") + ")");
} catch (e2) { pre("Add THREW: " + Platform.Function.Stringify(e2)); }
// Init does not validate the key, and the instance cannot be stringified.
try {
var ghost = Portfolio.Init("__qa_probe_nonexistent__");
pre("Init(unknown key) typeof=" + (typeof ghost) + " has Remove? " + (typeof ghost.Remove));
try { pre("String(instance)=" + String(ghost)); }
catch (eS) { pre("String(instance) THREW: " + String(eS)); }
// Remove on a key that never existed still reports "OK".
pre("Remove(unknown key) returned: " + Platform.Function.Stringify(ghost.Remove()));
} catch (e3) { pre("Init/Remove THREW: " + Platform.Function.Stringify(e3)); }
// Retrieve: filter optional, result array-LIKE but not a real Array.
try {
var all = Portfolio.Retrieve();
pre("Retrieve() no-filter length=" + all.length + " instanceof Array=" + (all instanceof Array));
var none = Portfolio.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: "__qa_probe_nonexistent__" });
pre("Retrieve(no match) length=" + none.length + " (null? " + (none === null) + ")");
} catch (e5) { pre("Retrieve THREW: " + Platform.Function.Stringify(e5)); }
// Non-object filter throws.
try { Portfolio.Retrieve("not-an-object"); }
catch (e6) { pre("Retrieve(string) THREW: " + String(e6)); }
</script>
<PortfolioInstance>.Update — no working invocation - always "Error" or "Error Updating Portfolio"
The official docs describe <PortfolioInstance>.Update(properties) as a normal update returning "OK" on success or throwing on failure. At runtime no working invocation was found, even though Init, Add, Retrieve and Remove all succeed on the very same item: every attempt either returned the string "Error" or threw Error Updating Portfolio, and the stored record never changed. Shapes swept without a single success — instances from Init(CustomerKey) and Init(ObjectID), single-field payloads ({DisplayName}, {Description}), payloads repeating the identifying fields ({CustomerKey, DisplayName, CategoryID}), payloads carrying the ObjectID, the full Add-shaped payload including FileName + FileLocation, an array-wrapped payload, and a no-op update writing the current DisplayName back onto a pre-existing (non-probe) item. There is no static Portfolio.Update either — that identifier is undefined. Treat the method as non-functional: to change a portfolio item, Remove it and Add it again, or use the Content Builder Asset REST endpoints.
Show test script
<script runat="server">
// OBSERVED: every <PortfolioInstance>.Update shape returned "Error" or threw "Error Updating Portfolio",
// and a follow-up Retrieve showed the stored DisplayName unchanged.
// Point KEY at a portfolio item that exists in your account.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
var KEY = "__set_me_to_an_existing_portfolio_customerkey__";
function currentName() {
var rows = Portfolio.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return rows.length ? rows[0].DisplayName : "(not found)";
}
pre("before: " + currentName());
var payloads = [
{ DisplayName: "__qa_upd__" },
{ Description: "__qa_upd__" },
{ CustomerKey: KEY, DisplayName: "__qa_upd__", CategoryID: 0 },
[{ CustomerKey: KEY, DisplayName: "__qa_upd__" }]
];
for (var i = 0; i < payloads.length; i++) {
try {
var ret = Portfolio.Init(KEY).Update(payloads[i]);
pre("payload " + i + " returned: " + Platform.Function.Stringify(ret));
} catch (e) { pre("payload " + i + " THREW: " + String(e)); }
}
pre("after: " + currentName());
// There is no static Portfolio.Update.
pre("typeof Portfolio.Update=" + (typeof Portfolio.Update));
</script>
QueryDefinition.Add — failures return "Error", not a throw
The official docs say QueryDefinition.Add(properties) returns "OK" or throws. Runtime-verified: invalid payloads return the plain string "Error" instead of throwing — including the docs’ Overwrite sample that SELECTs from the same Data Extension used as Target. Overwrite requires a different source DE; TargetUpdateType: "Update" can read and write the same DE only when that DE has at least one non-primary-key field. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: QueryDefinition.Add failures return "Error", they do
* NOT throw. Official docs: failures throw. Runtime: Overwrite selecting
* from the target DE returns the plain string "Error".
*
* Proves:
* 1. DEV Add returns "Error" (docs: throw).
* 2. DEV Add does not throw (docs: throw).
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
function resolveQueryCategoryId() {
var api = new Script.Util.WSProxy();
var fr = api.retrieve("DataFolder", ["ID"], { Property: "ContentType", SimpleOperator: "equals", Value: "queryactivity" });
return fr.Results && fr.Results.length ? fr.Results[0].ID : null;
}
function ensureTargetDe(key, name) {
var rows = DataExtension.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: key });
if (rows && rows.length) return;
DataExtension.Add({
Name: name,
CustomerKey: key,
Fields: [
{ Name: "Pk", FieldType: "Text", MaxLength: 50, IsPrimaryKey: true, IsRequired: true },
{ Name: "Val", FieldType: "Text", MaxLength: 50, IsRequired: false }
]
});
}
function deleteDe(key) {
var props = Platform.Function.CreateObject("DataExtension");
Platform.Function.SetObjectProperty(props, "CustomerKey", key);
var st = [0, 0, 0];
return Platform.Function.InvokeDelete(props, st, null);
}
var DE_KEY = "ssjs-guide-ts-qd-de";
var DE_NAME = "SSJS Guide TS QD Target";
var KEY = "ssjs-guide-ts-qd-add-err";
QueryDefinition.Init(KEY).Remove();
ensureTargetDe(DE_KEY, DE_NAME);
var cat = resolveQueryCategoryId();
var status = null;
assert("DEV Add failure does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Add({
Name: "SSJS Guide TS QD Add Err",
CustomerKey: KEY,
CategoryID: cat,
TargetUpdateType: "Overwrite",
TargetType: "DE",
Target: { Name: DE_NAME, CustomerKey: DE_KEY },
QueryText: "SELECT Pk, Val FROM [" + DE_NAME + "]"
});
}), "returned");
assert("DEV Add failure returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Add failure result is string", typeof status, "string");
deleteDe(DE_KEY);
</script>
<QueryDefinitionInstance>.Perform — returns a status string, not "OK"
The official docs annotate <QueryDefinitionInstance>.Perform(action) as @returns {Enum("OK")}, but at runtime Perform(action) returns a status string, never the enum "OK". QueryDefinition.Init(key) returns a working instance object and qd.Perform is a function with arity 3. On a valid query key, Perform("start") queues the run asynchronously and returns a success string ("QueryDefinition perform called successfully"); on an invalid / non-existent key it does not throw — it returns the failure as a string of the form "Exception occurred during [Schedule::Start] ErrorID = <number>". Because both success and failure come back as return strings, detect failure by inspecting the returned string, not by string-matching "OK" and not by relying on an exception.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Perform(action)
*
* Official docs: returns Enum("OK") and failures throw.
* Runtime:
* - success → "QueryDefinition perform called successfully"
* - missing key → Exception string, does NOT throw
*
* Proves:
* 1. DEV success string is not "OK".
* 2. DEV failure does not throw.
* 3. DEV failure string contains "Exception occurred during [Schedule::Start]".
* 4. Workaround: inspect the returned string, do not rely on try/catch.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
function resolveQueryCategoryId() {
var api = new Script.Util.WSProxy();
var fr = api.retrieve("DataFolder", ["ID"], { Property: "ContentType", SimpleOperator: "equals", Value: "queryactivity" });
return fr.Results && fr.Results.length ? fr.Results[0].ID : null;
}
function ensureTargetDe(key, name) {
var rows = DataExtension.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: key });
if (rows && rows.length) return;
DataExtension.Add({
Name: name,
CustomerKey: key,
Fields: [
{ Name: "Pk", FieldType: "Text", MaxLength: 50, IsPrimaryKey: true, IsRequired: true },
{ Name: "Val", FieldType: "Text", MaxLength: 50, IsRequired: false }
]
});
}
function deleteDe(key) {
var props = Platform.Function.CreateObject("DataExtension");
Platform.Function.SetObjectProperty(props, "CustomerKey", key);
var st = [0, 0, 0];
return Platform.Function.InvokeDelete(props, st, null);
}
var DE_KEY = "ssjs-guide-ts-qd-de";
var DE_NAME = "SSJS Guide TS QD Target";
var KEY = "ssjs-guide-ts-qd-perf-dev";
var SRC = "SSJSGUIDE_TYPES";
QueryDefinition.Init(KEY).Remove();
ensureTargetDe(DE_KEY, DE_NAME);
var cat = resolveQueryCategoryId();
assert("Add fixture for Perform differs", "" + QueryDefinition.Add({
Name: "SSJS Guide TS QD Perf Dev",
CustomerKey: KEY,
CategoryID: cat,
TargetUpdateType: "Overwrite",
TargetType: "DE",
Target: { Name: DE_NAME, CustomerKey: DE_KEY },
QueryText: "SELECT Pk FROM [" + SRC + "]"
}), "OK");
var ok = QueryDefinition.Init(KEY).Perform("start");
assert("DEV success is not \"OK\" (docs: \"OK\")", ("" + ok) === "OK" ? "true" : "false", "false");
assert("DEV success is QueryDefinition perform called successfully", "" + ok, "QueryDefinition perform called successfully");
var fail = null;
assert("DEV Perform(missing) does NOT throw (docs: throw)", invocationResult(function () {
fail = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Perform("start");
}), "returned");
assert("DEV typeof Perform failure is string", typeof fail, "string");
assert("DEV failure contains Schedule::Start Exception", ("" + fail).indexOf("Exception occurred during [Schedule::Start]") >= 0 ? "matched" : ("" + fail), "matched");
assert("workaround: failure is not the success string", ("" + fail) === "QueryDefinition perform called successfully" ? "true" : "false", "false");
assert("fixture cleanup", "" + QueryDefinition.Init(KEY).Remove(), "OK");
deleteDe(DE_KEY);
</script>
DeliveryProfile.Add — returns a CLR object, not "OK"
The official docs annotate DeliveryProfile.Add(properties) as returning the string "OK". At runtime a successful DeliveryProfile.Add returns a CLR object (typeof is clr; it stringifies to ExactTarget.Integration.WSDL.DeliveryProfile), not "OK". Reading any property off it throws “Use of Common Language Runtime (CLR) is not allowed”, so the object is opaque from SSJS — treat any non-throwing return as success. A failed Add (invalid/rejected properties) instead returns the string "Error adding DeliveryProfile.". Sibling instance methods <DeliveryProfileInstance>.Update(properties) and <DeliveryProfileInstance>.Remove() return the string "OK" as documented (a no-op / missing-record call returns the string "Error"). DeliveryProfile.Retrieve does not exist (typeof is undefined).
Show test script
<script runat="server">
// OBSERVED: DeliveryProfile.Add({..., SourceAddressType:"DefaultPrivateIPAddress"}) returned typeof=clr, String()="ExactTarget.Integration.WSDL.DeliveryProfile" (NOT "OK"); property read threw "Use of Common Language Runtime (CLR) is not allowed"; sibling <inst>.Remove() returned "OK"; DeliveryProfile.Retrieve is undefined. Throwaway profile was created then removed (net non-destructive). CONFIRMED.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
function dump(desc, v) {
pre(desc + " typeof=" + (typeof v));
try { pre(desc + " String()=" + String(v)); } catch (x1) { pre(desc + " String() threw"); }
try { pre(desc + " Stringify=" + Platform.Function.Stringify(v)); } catch (x2) { pre(desc + " Stringify threw"); }
}
pre("=== DeliveryProfile.Add — create throwaway, capture return type, then Remove ===");
pre("typeof DeliveryProfile.Retrieve=" + (typeof DeliveryProfile.Retrieve)); // undefined
var key = "__qa_dp_probe__";
try { key = "__qa_dp_" + Platform.Function.GUID().substring(0,8) + "__"; } catch (kx) {}
// THE CLAIM — DeliveryProfile.Add(properties) return value on SUCCESS.
// Docs @returns Enum("OK"). Runtime returns a CLR object, NOT "OK".
try {
var dp = {
"Name": key,
"CustomerKey": key,
"Description": "QA probe - auto-removed",
"SourceAddressType": "DefaultPrivateIPAddress"
};
var added = DeliveryProfile.Add(dp);
pre("Add RETURNED typeof=" + (typeof added)); // clr
dump("Add-return raw", added); // String() => ExactTarget.Integration.WSDL.DeliveryProfile
pre("is exactly 'OK'? " + (String(added) === "OK")); // false
try { pre("added.CustomerKey=" + added.CustomerKey); } catch (p1) { dump("property-read THREW", p1); } // CLR not allowed
} catch (e1) { dump("Add THREW", e1); }
// CLEANUP — sibling instance Remove() returns the string "OK".
try {
var rem = DeliveryProfile.Init(key).Remove();
pre("Remove String()=" + String(rem) + " ==='OK'? " + (String(rem) === "OK")); // OK / true
} catch (e3) { dump("Remove THREW", e3); }
pre("=== END ===");
</script>
SenderProfile.Add — returns a CLR object, not "OK"
The official docs annotate SenderProfile.Add(properties) as returning the string "OK". At runtime a successful SenderProfile.Add returns a CLR object (typeof is clr; it stringifies to ExactTarget.Integration.WSDL.SenderProfile), not "OK". Reading any property off it throws “Use of Common Language Runtime (CLR) is not allowed”, so the object is opaque from SSJS — treat any non-throwing return as success. This mirrors DeliveryProfile.Add. Sibling instance method <SenderProfileInstance>.Remove() returns the string "OK"; <SenderProfileInstance>.Update(properties) returns "OK" as documented; SenderProfile.Init binds a key and returns a working instance. SenderProfile.Retrieve does not exist (typeof is undefined).
Show test script
<script runat="server">
// OBSERVED: SenderProfile.Add({Name,CustomerKey,FromName,FromAddress,...}) returned typeof=clr, String()="ExactTarget.Integration.WSDL.SenderProfile" (NOT "OK", is==='OK' false); property read threw "Use of Common Language Runtime (CLR) is not allowed"; sibling <inst>.Remove() returned "OK"; SenderProfile.Retrieve is undefined. Throwaway profile was created then removed (net non-destructive). CONFIRMED.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
function dump(desc, v) {
pre(desc + " typeof=" + (typeof v));
try { pre(desc + " String()=" + String(v)); } catch (x1) { pre(desc + " String() threw"); }
try { pre(desc + " Stringify=" + Platform.Function.Stringify(v)); } catch (x2) { pre(desc + " Stringify threw"); }
}
pre("=== SenderProfile.Add — create throwaway, capture return type, then Remove (mirror of DeliveryProfile.Add) ===");
// Presence / arity of static members
pre("typeof SenderProfile.Add=" + (typeof SenderProfile.Add)); // function
pre("typeof SenderProfile.Init=" + (typeof SenderProfile.Init)); // function
pre("typeof SenderProfile.Retrieve=" + (typeof SenderProfile.Retrieve)); // undefined
var key = "__qa_sp_probe__";
try { key = "__qa_sp_" + Platform.Function.GUID().substring(0,8) + "__"; } catch (kx) {}
// THE CLAIM — SenderProfile.Add(properties) return value on SUCCESS.
// Docs @returns Enum("OK"). Runtime returns a CLR object, NOT "OK".
try {
var sp = {
"Name": key,
"CustomerKey": key,
"Description": "QA probe - auto-removed",
"FromName": "QA Probe",
"FromAddress": "qa-probe@example.com"
};
var added = SenderProfile.Add(sp);
pre("Add RETURNED typeof=" + (typeof added)); // clr
dump("Add-return raw", added); // String() => ExactTarget.Integration.WSDL.SenderProfile
pre("is exactly 'OK'? " + (String(added) === "OK")); // false
try { pre("added.CustomerKey=" + added.CustomerKey); } catch (p1) { dump("property-read THREW", p1); } // CLR not allowed
} catch (e1) { dump("Add THREW", e1); }
// CLEANUP — sibling instance Remove() returns the string "OK".
try {
var rem = SenderProfile.Init(key).Remove();
pre("Remove String()=" + String(rem) + " ==='OK'? " + (String(rem) === "OK")); // OK / true
} catch (e3) { dump("Remove THREW", e3); }
pre("=== END ===");
</script>
SendClassification.Add — returns a CLR object, not "OK"
The official docs annotate SendClassification.Add(properties) as returning the string "OK". At runtime, against real, owned ssjs-senderprofile + ssjs-deliveryprofile keys, a successful SendClassification.Add creates the record and returns a CLR object (typeof is clr; it stringifies to ExactTarget.Integration.WSDL.SenderProfile), not "OK". Enumerating its keys with for..in yields none and reading any property off it throws “Use of Common Language Runtime (CLR) is not allowed”, so the object is opaque from SSJS — treat any non-throwing return as success and read the created record back with SendClassification.Retrieve (a Retrieve immediately after Add returned the new record). The SenderProfileKey and DeliveryProfileKey in properties must reference existing profiles by external key; an unresolvable profile key makes the Add fail. This mirrors DeliveryProfile.Add and SenderProfile.Add. Sibling instance methods <SendClassificationInstance>.Update(properties) and <SendClassificationInstance>.Remove() return the string "OK" on success and the string "Error" (not a throw) on failure.
Show test script
<script runat="server">
// OBSERVED: SendClassification.Add({CustomerKey,Name,SenderProfileKey:"ssjs-senderprofile",DeliveryProfileKey:"ssjs-deliveryprofile"})
// returned typeof=clr, String()="ExactTarget.Integration.WSDL.SenderProfile" (NOT "OK"); for..in yielded no keys;
// a follow-up SendClassification.Retrieve returned the new record; sibling <inst>.Remove() returned "OK" and a
// subsequent Retrieve returned []. Throwaway classification was created then removed (net non-destructive). CONFIRMED.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
function dump(desc, v) {
pre(desc + " typeof=" + (typeof v));
try { pre(desc + " String()=" + String(v)); } catch (x1) { pre(desc + " String() threw"); }
}
pre("=== SendClassification.Add — create throwaway with real profiles, capture return, then Remove ===");
var key = "__qa_sc_probe__";
try { key = "__qa_sc_" + Platform.Function.GUID().substring(0,8) + "__"; } catch (kx) {}
// THE CLAIM — SendClassification.Add(properties) return value on SUCCESS.
// Docs @returns Enum("OK"). Runtime returns a CLR object, NOT "OK".
try {
var sc = {
"CustomerKey": key,
"Name": key,
"Description": "QA probe - auto-removed",
"SenderProfileKey": "ssjs-senderprofile",
"DeliveryProfileKey": "ssjs-deliveryprofile"
};
var added = SendClassification.Add(sc);
pre("Add RETURNED typeof=" + (typeof added)); // clr
dump("Add-return raw", added); // String() => ExactTarget.Integration.WSDL.SenderProfile
pre("is exactly 'OK'? " + (String(added) === "OK")); // false
var n = 0; for (var k in added) { n++; } pre("for..in key count=" + n); // 0
} catch (e1) { dump("Add THREW", e1); }
// READ-BACK — Retrieve confirms the record now exists.
try {
var rows = SendClassification.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: key });
pre("Retrieve-after-Add length=" + (rows ? rows.length : "null")); // 1
} catch (e2) { dump("Retrieve THREW", e2); }
// CLEANUP — sibling instance Remove() returns the string "OK".
try {
var rem = SendClassification.Init(key).Remove();
pre("Remove String()=" + String(rem) + " ==='OK'? " + (String(rem) === "OK")); // OK / true
var gone = SendClassification.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: key });
pre("Retrieve-after-Remove length=" + (gone ? gone.length : "null")); // 0
} catch (e3) { dump("Remove THREW", e3); }
pre("=== END ===");
</script>
Send.Definition.Add / AddWithDE — return a CLR object, not "OK"; AddWithDE only works when the documented fifth argument is omitted
Both statics are runtime-proven working against a real, owned send definition (ssjs-send) plus real send classification, email, list, and sendable Data Extension keys — but two things differ from the docs. First, the return value: the docs annotate these as returning the string "OK", while a successful call returns a CLR object (typeof is "clr", and String(result) is "ExactTarget.Integration.WSDL.EmailSendDefinition"). Enumerate nothing off it — treat any non-throwing return as success and read the record back with Send.Definition.Retrieve. Second, arity: AddWithDE is documented with five arguments (esdParams, sendClassificationKey, emailKey, sendableDataExtensionKey, publicationListKey), but only the four-argument form succeeds. Supplying any fifth argument throws and creates nothing — reproduced with a publication list name, a numeric list ID, and the Data Extension key repeated. For Add, all four documented arguments are required and listIds must contain existing list IDs; an unknown ID makes the call throw and nothing is created. The failure value in both cases is a plain string ("Error adding EmailSendDefinition.", typeof ex === "string"), so ex.message is undefined — catch it as a string, not as an Error object.
Show test script
<script runat="server">
// OBSERVED: Send.Definition.Add(4 args) and Send.Definition.AddWithDE(4 args) both returned typeof=clr,
// String()="ExactTarget.Integration.WSDL.EmailSendDefinition" (NOT "OK"), and the records were retrievable
// immediately afterwards. AddWithDE with a 5th argument THREW the raw STRING "Error adding EmailSendDefinition."
// and created nothing. Throwaway definitions were created then removed (net non-destructive). CONFIRMED.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
// Adapt these to real, existing keys/IDs in your account.
var SC_KEY = "your_send_classification_key";
var EMAIL_KEY = "your_email_key";
var LIST_ID = 12345; // must EXIST - an unknown ID makes Add throw
var DE_KEY = "your_sendable_de_key";
var PUB_LIST = "your_publication_list_name";
var addKey = "__qa_esd_add__";
var deKey = "__qa_esd_de__";
var deKey5 = "__qa_esd_de5__";
// THE CLAIM 1 — Add returns "OK". Runtime returns a CLR object.
try {
var a = Send.Definition.Add({ CustomerKey: addKey, Name: addKey, EmailSubject: "QA probe" }, SC_KEY, EMAIL_KEY, [LIST_ID]);
pre("Add typeof=" + (typeof a) + " String()=" + String(a)); // clr / ExactTarget.Integration.WSDL.EmailSendDefinition
pre("Add is exactly 'OK'? " + (String(a) === "OK")); // false
} catch (e1) { pre("Add THREW typeof=" + (typeof e1) + " String=" + String(e1)); }
// THE CLAIM 2 — AddWithDE takes five arguments. Only the four-argument form works.
try {
var b = Send.Definition.AddWithDE({ CustomerKey: deKey, Name: deKey, EmailSubject: "QA probe" }, SC_KEY, EMAIL_KEY, DE_KEY);
pre("AddWithDE(4) typeof=" + (typeof b) + " String()=" + String(b)); // clr / ...EmailSendDefinition
} catch (e2) { pre("AddWithDE(4) THREW typeof=" + (typeof e2) + " String=" + String(e2)); }
try {
var c = Send.Definition.AddWithDE({ CustomerKey: deKey5, Name: deKey5, EmailSubject: "QA probe" }, SC_KEY, EMAIL_KEY, DE_KEY, PUB_LIST);
pre("AddWithDE(5) typeof=" + (typeof c) + " String()=" + String(c));
} catch (e3) { pre("AddWithDE(5) THREW typeof=" + (typeof e3) + " String=" + String(e3)); }
// OBSERVED: AddWithDE(5) THREW typeof=string "Error adding EmailSendDefinition."
// READ-BACK — the four-argument calls created real records; the five-argument call created nothing.
var keys = [addKey, deKey, deKey5];
for (var i = 0; i < keys.length; i++) {
var rows = Send.Definition.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: keys[i] });
pre("Retrieve " + keys[i] + " length=" + (rows ? rows.length : "null")); // 1, 1, 0
}
// CLEANUP — instance Remove() returns "OK".
for (var j = 0; j < keys.length; j++) {
try { pre("Remove " + keys[j] + " -> " + String(Send.Definition.Init(keys[j]).Remove())); } catch (e4) { pre("Remove " + keys[j] + " THREW " + String(e4)); }
}
pre("=== END ===");
</script>
<SendDefinitionInstance>.Update — scalar properties only — nested properties throw "Error Updating ESD." while the WSProxy equivalent succeeds
The docs present <SendDefinitionInstance>.Update(properties) as a general update that takes the same property shape used to create a send definition. At runtime it is runtime-proven working for scalar properties only: Update({ Description: … }) and Update({ TestEmailAddr: … }) return the string "OK" and the change persists (verified by re-reading the record through Send.Definition.Retrieve). Payloads containing nested/complex properties throw the raw string "Error Updating ESD." — observed for Update({ Email: { ID: <id> } }) and Update({ SendDefinitionList: [ … ] }). The equivalent WSProxy updateItem("EmailSendDefinition", …) calls for those same nested properties return Status: "OK" with StatusMessage "EmailSendDefinition updated", so the restriction is specific to this Core method rather than to the operation. Use WSProxy when you need to change the email, audience, or any other nested member.
Show test script
<script runat="server">
// OBSERVED: Update({Description}) and Update({TestEmailAddr}) returned "OK" and persisted;
// Update({Email:{ID:..}}) and Update({SendDefinitionList:[..]}) THREW the raw STRING "Error Updating ESD.";
// the equivalent WSProxy updateItem for those nested properties returned Status "OK" / "EmailSendDefinition updated".
// Runs against a real, owned send definition. CONFIRMED.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
var ESD_KEY = "ssjs-send"; // adapt to a real send definition external key in your account
var EMAIL_ID = 12345; // adapt to a real email ID
// SCALAR — works.
try {
var u1 = Send.Definition.Init(ESD_KEY).Update({ Description: "QA probe " + Platform.Function.Now() });
pre("Update(scalar Description) String()=" + String(u1)); // OK
} catch (e1) { pre("Update(scalar) THREW typeof=" + (typeof e1) + " String=" + String(e1)); }
try {
var u2 = Send.Definition.Init(ESD_KEY).Update({ TestEmailAddr: "test@example.com" });
pre("Update(scalar TestEmailAddr) String()=" + String(u2)); // OK
} catch (e2) { pre("Update(TestEmailAddr) THREW String=" + String(e2)); }
// NESTED — throws.
try {
var u3 = Send.Definition.Init(ESD_KEY).Update({ Email: { ID: EMAIL_ID } });
pre("Update(nested Email) String()=" + String(u3));
} catch (e3) { pre("Update(nested Email) THREW typeof=" + (typeof e3) + " String=" + String(e3)); }
// OBSERVED: THREW typeof=string "Error Updating ESD."
// WSProxy CONTROL — the same nested update succeeds.
try {
var api = new Script.Util.WSProxy();
var w = api.updateItem("EmailSendDefinition", { CustomerKey: ESD_KEY, Email: { ID: EMAIL_ID } });
pre("WSProxy updateItem Status=" + w.Status + " Message=" + (w.Results && w.Results[0] ? w.Results[0].StatusMessage : ""));
} catch (e4) { pre("WSProxy updateItem THREW " + String(e4)); }
// OBSERVED: Status=OK, StatusMessage="EmailSendDefinition updated"
pre("=== END ===");
</script>
DataExtension.Retrieve — filter is optional at runtime
The official docs list the filter argument as required, but at runtime it is optional: calling DataExtension.Retrieve() with no arguments does not throw — it returns the full list of data extensions (each a plain object exposing Name, CustomerKey, ObjectID, CategoryID, Status, etc.). A filter that matches nothing returns a real empty array (Object.prototype.toString reports [object Array], typeof .length is number, .length === 0, zero enumerable keys) — not null and not undefined. Note the SFMC engine quirk that an empty array is falsy here, so guard on .length (results.length > 0) rather than truthiness of the array itself.
Show test script
<script runat="server">
// OBSERVED: filter is OPTIONAL at runtime — DataExtension.Retrieve() with NO args did NOT throw and returned
// OBSERVED: the full DE list ([object Array], length=44, elements expose Name/CustomerKey/ObjectID/...).
// OBSERVED: a non-matching filter returned a REAL empty array (typeof=object, [object Array], typeof .length=number,
// OBSERVED: .length===0, 0 enumerable keys, !== null, !== undefined) that is FALSY in this engine.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
pre("typeof DataExtension.Retrieve=" + (typeof DataExtension.Retrieve));
// P1 — no arguments (docs say filter is REQUIRED); expect the full DE list, no throw.
try {
var all = DataExtension.Retrieve();
pre("P1 no-arg did NOT throw; toStringTag=" + Object.prototype.toString.call(all) + " length=" + all.length);
if (all.length > 0) { pre("P1 element0.Name=" + all[0].Name + " CustomerKey=" + all[0].CustomerKey); }
} catch (e1) { pre("P1 THREW: " + e1); }
// P2 — filter matching nothing; expect a real empty array, not null/undefined, and falsy.
try {
var none = DataExtension.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: "__no_such_de_key_qa__zzz" });
pre("P2 typeof=" + (typeof none) + " ===null?" + (none === null) + " toStringTag=" + Object.prototype.toString.call(none) + " length=" + none.length + " falsy?" + (none ? "TRUTHY" : "FALSY"));
} catch (e2) { pre("P2 THREW: " + e2); }
</script>
<DataExtensionInstance>.Rows.Add / Update — return a row count, not "OK"
The official docs annotate <DataExtensionInstance>.Rows.Add(rowData) and <DataExtensionInstance>.Rows.Update(rowData, whereFieldNames, whereValues) as returning the string "OK". At runtime both return a number — the count of rows added / updated. Update returns 0 and does not throw when the WHERE clause matches no rows (the docs imply it throws). Add also accepts a single row object in addition to an array of objects.
Show test script
<script runat="server">
// OBSERVED: Add(singleObj)->number 1; Add([obj])->number 1; Update(match)->number 1; Update(no-match)->number 0 (no throw); none === "OK". Probe rows cleaned up via Rows.Remove.
Platform.Load("core", "1.1.5");
function pre(t) { Platform.Response.Write(t + "\n"); }
function dump(desc, v) {
pre(desc + " typeof=" + (typeof v) + " value=" + v);
try { pre(desc + " === 'OK'? " + (v === "OK")); } catch (x0) {}
try { pre(desc + " toStringTag=" + Object.prototype.toString.call(v)); } catch (x1) { pre(desc + " toStringTag threw"); }
}
pre("=== <DataExtensionInstance>.Rows.Add / Update — return shape ===");
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
pre("typeof de.Rows.Add=" + (typeof de.Rows.Add));
pre("typeof de.Rows.Update=" + (typeof de.Rows.Update));
// unique probe key so we never collide with existing data
var pk = "__qa_probe_" + Platform.Function.Now();
pk = ("" + pk).replace(/[^A-Za-z0-9_]/g, "_");
// A — Add with a SINGLE row object (docs say rowData is object[]).
try { dump("A Add(singleObj)", de.Rows.Add({ Pk: pk, Txt: "hello", Num: 7 })); } catch (ea) { pre("A THREW: " + ea); }
// B — Update matching the row we just added.
try { dump("B Update(match)", de.Rows.Update({ Txt: "updated" }, ["Pk"], [pk])); } catch (eb) { pre("B THREW: " + eb); }
// C — Update matching NOTHING (docs imply throw).
try { dump("C Update(no-match)", de.Rows.Update({ Txt: "nope" }, ["Pk"], ["__no_such_pk_zzz__"])); } catch (ec) { pre("C THREW: " + ec); }
// D — Add with an ARRAY of rows (the documented shape).
var pk2 = pk + "_arr";
try { dump("D Add([obj])", de.Rows.Add([{ Pk: pk2, Txt: "arr" }])); } catch (ed) { pre("D THREW: " + ed); }
// CLEANUP — remove both probe rows so we stay net-non-destructive.
try { dump("cleanup rr1", de.Rows.Remove(["Pk"], [pk])); } catch (z1) { pre("cleanup1 THREW: " + z1); }
try { dump("cleanup rr2", de.Rows.Remove(["Pk"], [pk2])); } catch (z2) { pre("cleanup2 THREW: " + z2); }
pre("=== END ===");
</script>
FilterDefinition write methods (Add / Update / Remove) — reads work (Init/Retrieve); the write methods Add/Update/Remove do not work at runtime — they return/throw "Error", not "OK" (no working invocation found)
The official docs state these return "OK" on success or throw on failure. Reads work: FilterDefinition.Init and FilterDefinition.Retrieve are verified against the owned definition (ssjs-datafilter-test, source DE SSJSGUIDE_TYPES) and are not part of this deviation. This entry covers the write methods Add (static), Update, and Remove (instance), for which no working invocation was found in our CloudPage tests. For Add, the documented simple-filter payload (Filter: {Property, SimpleOperator, Value} + DataSource: {Type, CustomerKey}) returns the plain string "Error" under Core "1" / "1.1.1" / "1.1.5" and creates nothing (Core Retrieve and WSProxy stay empty for the probe key). A LeftOperand/LogicalOperator/RightOperand complex Filter also returns "Error"; using a DataFilter property instead of Filter throws the raw string "Error adding FilterDefinition". For Update, three payload shapes were tried against the owned object and none succeeded: (1) a FULL Add-style payload threw "Error updating FilterDefinition"; (2) the same payload without DataSource also threw that string; (3) a metadata-only payload returned "Error". Description stayed empty and ObjectID was unchanged. For Remove, <instance>.Remove() returned "Error" (no throw) and a follow-up Retrieve still found the object. Observed WSProxy facts (reported, not interpreted as a cause): createItem / updateItem / deleteItem on FilterDefinition failed (delete reported a permission error). The SOAP describe marks Name/Description/CustomerKey/DataFilter as IsUpdatable: true, yet no working Core write was reproduced. A follow-up probe run additionally tried payload shapes derived from the working mcdev dataFilter REST implementation (sfmc-devtools DataFilter.js preDeployTasks() — create posts key/name/categoryId/description/filterDefinitionXml/derivedFromType: 2/derivedFromObjectId to /email/v1/filters/filterdefinition/): the REST-shape lowercase payload with a real filterDefinitionXml string, a PascalCase FilterDefinitionXml variant, CategoryID, capitalized Equals, field-ObjectID Property, ObjectID-based and full DataSource objects, and the docs’ SubscriberList shape. Every variant still returned "Error" or threw "Error adding FilterDefinition"/"Error updating FilterDefinition", and the CreateObject("FilterDefinition") + InvokeCreate SOAP path failed with “The user does not have permission to perform this operation.” — so the failure is not a payload-shape problem. Filters can still be created outside Core (for example mcdev dataFilter deploy or the REST endpoint above). Treat a non-"OK" result as failure.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Probe: FilterDefinition reads work; Core write methods Add/Update/Remove do not.
// Adapt "ssjs-datafilter-test" / "SSJSGUIDE_TYPES" to owned keys in your account.
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var r = FilterDefinition.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: "ssjs-datafilter-test" });
pre("Retrieve length=" + (r && r.length != undefined ? r.length : "n/a")); // OBSERVED: 1
} catch (e1) { pre("Retrieve THREW -> " + e1); }
try {
var addStatus = FilterDefinition.Add({
Name: "ssjs-guide-ts-fd-probe",
CustomerKey: "ssjs-guide-ts-fd-probe",
Filter: { Property: "Pk", SimpleOperator: "equals", Value: "test" },
DataSource: { Type: "DataExtension", CustomerKey: "SSJSGUIDE_TYPES" }
});
pre("Add RETURNED typeof=" + (typeof addStatus) + " value=" + addStatus); // OBSERVED: string "Error"
} catch (eAdd) { pre("Add THREW typeof=" + (typeof eAdd) + " String=" + ("" + eAdd)); }
try {
var fd2 = FilterDefinition.Init("ssjs-datafilter-test");
var updResult = fd2.Update({
Name: "ssjs-datafilter-test",
CustomerKey: "ssjs-datafilter-test",
Description: "Updated description",
Filter: { Property: "Pk", SimpleOperator: "equals", Value: "test" },
DataSource: { Type: "DataExtension", CustomerKey: "SSJSGUIDE_TYPES" }
});
pre("Update RETURNED typeof=" + (typeof updResult) + " value=" + updResult);
} catch (e3) { pre("Update THREW typeof=" + (typeof e3) + " String=" + ("" + e3)); } // OBSERVED: string "Error updating FilterDefinition"
try {
var remResult = FilterDefinition.Init("ssjs-datafilter-test").Remove();
pre("Remove RETURNED typeof=" + (typeof remResult) + " value=" + remResult); // OBSERVED: string "Error"
} catch (e4) { pre("Remove THREW -> " + e4); }
try {
var chk = FilterDefinition.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: "ssjs-datafilter-test" });
pre("Post-write Retrieve length=" + (chk && chk.length != undefined ? chk.length : "n/a")); // OBSERVED: 1
} catch (e5) { pre("Post-write Retrieve THREW -> " + e5); }
</script>
AccountUser.Init — returns an opaque stub and never validates the user key
The docs imply AccountUser.Init(targetUserKey, myClientID) hands back the user record. It does not: the returned instance is an opaque stub carrying only the write methods. Runtime-verified on a Parent BU CloudPage:
inst.ID,inst.Nameandinst.CustomerKeyall read backundefined.Stringify(inst)yields{"Activate":"function","Deactivate":"function","Remove":"function","Update":"function"}— note the undocumentedRemovemember.Initdoes not validatetargetUserKey. A key that matches no user returns an object exposing the sameUpdate/Activate/Deactivatemembers, andStringify()of the bogus stub is byte-identical to the real one. A bad key therefore only surfaces when an instance method is finally called.myClientIDaccepts a number and the same MID as a string, producing the same stub.
Use AccountUser.Retrieve() whenever you need to read user fields or to prove that a user exists.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Init returns an opaque stub and never validates targetUserKey.
// Resolves the running BU and a real user dynamically, so it runs in any account.
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var selfRows = Account.Retrieve({ Property: "ID", SimpleOperator: "greaterThan", Value: 0 });
var SELF_ID = +("" + selfRows[0].ID);
var users = AccountUser.Retrieve({ Property: "ID", SimpleOperator: "greaterThan", Value: 0 });
var REAL_KEY = "" + users[0].CustomerKey;
var inst = AccountUser.Init(REAL_KEY, SELF_ID);
assert("Init(realUserKey, MID) returns an object", typeof inst, "object");
// OBSERVED: object — but with no readable user fields on it.
assert("DEV inst.ID is undefined", typeof inst.ID, "undefined");
assert("DEV inst.Name is undefined", typeof inst.Name, "undefined");
assert("DEV inst.CustomerKey is undefined", typeof inst.CustomerKey, "undefined");
var bogus = AccountUser.Init("ssjs-guide-no-such-user-zzz", SELF_ID);
assert("DEV Init(nonsense key) still returns an object", typeof bogus, "object");
assert("DEV Init(nonsense key) exposes Update", typeof bogus.Update, "function");
assert("DEV bogus stub is indistinguishable from the real one", Stringify(bogus) === Stringify(inst) ? "true" : "false", "true");
// OBSERVED: all four PASS — Init never reports an unknown key.
// Workaround: read user data through Retrieve instead.
assert("workaround: a Retrieve row exposes a readable Name", typeof users[0].Name, "string");
</script>
AccountUser.Retrieve — the object[] result is not a real JavaScript Array
The docs give the return type as object[], which suggests a JavaScript array. At runtime result instanceof Array is false — both when the filter matches rows and when it matches nothing.
The collection is index- and length-addressable (.length, rows[0], and even a .push member), so a classic for loop over .length works and each row is the full AccountUser SOAP object. But Array.prototype methods and instanceof checks must not be relied on: guard with if (rows && rows.length) before indexing, and copy the entries into a real array first if you need array semantics. On no match the same shape comes back with .length of 0, stringifying as [].
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// The documented object[] return value is NOT an instanceof Array — on a match or a miss.
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var all = AccountUser.Retrieve({ Property: "ID", SimpleOperator: "greaterThan", Value: 0 });
assert("typeof matched result is object", typeof all, "object");
assert("matched result exposes .length", typeof all.length, "number");
assert("matched result has at least one row", all.length > 0 ? "true" : "false", "true");
assert("DEV matched result is NOT instanceof Array (docs: object[])", all instanceof Array ? "true" : "false", "false");
var miss = AccountUser.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: "ssjs-guide-no-such-user-key-zzz" });
assert("no-match result .length is 0", "" + miss.length, "0");
assert("no-match result stringifies as []", Stringify(miss), "[]");
assert("DEV no-match result is NOT instanceof Array", miss instanceof Array ? "true" : "false", "false");
// OBSERVED: both instanceof probes are false — use a .length guard instead.
assert("workaround: (rows && rows.length) is truthy on a match", (all && all.length) ? "true" : "false", "true");
assert("workaround: (rows && rows.length) is falsy on no match", (miss && miss.length) ? "true" : "false", "false");
</script>
AccountUser write methods (Add / Update / Activate / Deactivate) — reads work (Init/Retrieve); the write methods Add/Update/Activate/Deactivate return the string "Error" instead of throwing — blocked by a tenant permission gate on AccountUser writes, not by a defect in the method
The official docs state these return "OK" on success or throw on failure. Reads work: AccountUser.Init(targetUserKey, myClientID) returns an instance and AccountUser.Retrieve(filter) returns matching rows — both are verified working and are not part of this deviation. This entry covers only the write methods Add (static), Update, Activate, and Deactivate (instance). In our runtime tests those calls were blocked by a tenant permission gate on AccountUser writes, not by a defect in the method. Tested on a Parent BU session (the correct context for AccountUser edits): AccountUser.Add(properties) returned the plain string "Error" for a short payload and threw the raw string "Error adding AccountUser" for a full documented payload; <instance>.Update(properties), <instance>.Activate(), and <instance>.Deactivate() each returned the plain string "Error" (no throw). The equivalent WSProxy createItem("AccountUser", …) on the same run named the cause explicitly: StatusCode "Error", ErrorCode 11001, StatusMessage "User 0 does not have permission to edit ACCOUNTUSERS on account <Parent BU>.". On the same run Subscriber.Add returned "OK" and DataExtension.Retrieve succeeded, so the run had a working write/read path for other object types — the failure is specific to AccountUser writes from this session rather than a general write failure. A session whose user carries the ACCOUNTUSERS edit permission was not available, so the success ("OK") path was never exercised here. The documented deviation that remains regardless of permission is the return shape: these methods return the plain string "Error" instead of throwing, so always compare the returned value to "OK" rather than relying on try/catch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Probe: AccountUser reads work; the write methods Add/Update/Activate/Deactivate were blocked by a
// tenant permission gate on AccountUser writes (WSProxy control reported ErrorCode 11001,
// "does not have permission to edit ACCOUNTUSERS"), not by a defect in the methods.
// Adapt "myAccountUser" and the MID to a real user external key and business unit in your account.
// READS: Init returns an instance; Retrieve returns matching rows.
// WRITES (blocked in our session): Add returns "Error" (short payload) or throws the raw STRING
// "Error adding AccountUser" (full payload); <inst>.Update / .Activate / .Deactivate each return "Error" (no throw).
function pre(t) { Platform.Response.Write(t + "\n"); }
// P1 — Retrieve users (READ works).
try {
var r = AccountUser.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: "myAccountUser" });
pre("Retrieve typeof=" + (typeof r)); // OBSERVED: object
} catch (e1) { pre("Retrieve THREW -> " + e1); }
// P2 — Init a user instance and inspect it (READ works).
var acctUser;
try {
acctUser = AccountUser.Init("myAccountUser", 123456789);
pre("Init typeof Update=" + (typeof acctUser.Update) + " typeof Activate=" + (typeof acctUser.Activate)); // OBSERVED: function / function
} catch (e2) { pre("Init THREW -> " + e2); }
// P3 — Add a user (WRITE blocked -> "Error" or throws the raw STRING "Error adding AccountUser").
try {
var addResult = AccountUser.Add({ Name: "QA Probe", UserID: "qa_probe", Email: "qa_probe@example.com", ClientID: 123456789 });
pre("Add RETURNED typeof=" + (typeof addResult) + " value=" + addResult); // OBSERVED: string "Error"
} catch (e3) { pre("Add THREW typeof=" + (typeof e3) + " String=" + String(e3)); } // OBSERVED: typeof=string "Error adding AccountUser"
// P4 — Update the instance (WRITE blocked -> returns the string "Error", no throw).
try {
var updResult = acctUser.Update({ Name: "QA Probe Updated" });
pre("Update RETURNED typeof=" + (typeof updResult) + " value=" + updResult); // OBSERVED: string "Error"
} catch (e4) { pre("Update THREW -> " + e4); }
// P5 — Deactivate the instance (WRITE blocked -> returns the string "Error", no throw).
try {
var deactResult = acctUser.Deactivate();
pre("Deactivate RETURNED typeof=" + (typeof deactResult) + " value=" + deactResult); // OBSERVED: string "Error"
} catch (e5) { pre("Deactivate THREW -> " + e5); }
</script>
<SendInstance>.Tracking click & interval retrieval — Clicks.Retrieve / TotalByInterval.Retrieve, not ClickRetrieve / TotalByIntervalRetrieve
The official docs document per-send tracking as <SendInstance>.Tracking.ClickRetrieve(filter) and <SendInstance>.Tracking.TotalByIntervalRetrieve(type, startDate, endDate, groupBy). At runtime both of those names are undefined. The instance Tracking property is an object exposing two sub-objects — Clicks and TotalByInterval — each with a Retrieve method. The working calls are <SendInstance>.Tracking.Clicks.Retrieve(filter) and <SendInstance>.Tracking.TotalByInterval.Retrieve(type, startDate, endDate, groupBy). This mirrors the TriggeredSend.Tracking.Clicks / TriggeredSend.Tracking.TotalByInterval shape. The static Send.Tracking.Retrieve(filter) (no Send.Init required) is unaffected.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs: docs ClickRetrieve vs runtime Clicks.Retrieve.
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var s = Send.Init(1);
assert("DEV ClickRetrieve is undefined (docs name)", typeof s.Tracking.ClickRetrieve, "undefined");
assert("working member is Clicks.Retrieve", typeof s.Tracking.Clicks.Retrieve, "function");
/*
* Differs: docs TotalByIntervalRetrieve vs runtime TotalByInterval.Retrieve.
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var s = Send.Init(1);
assert("DEV TotalByIntervalRetrieve is undefined (docs name)", typeof s.Tracking.TotalByIntervalRetrieve, "undefined");
assert("working member is TotalByInterval.Retrieve", typeof s.Tracking.TotalByInterval.Retrieve, "function");
</script>
<SendInstance>.Remove — soft-cancels (Status Canceled); row stays Retrievable
Official docs describe Remove as deleting the send. At runtime Remove() returns "OK" and sets Status to "Canceled", but the send row remains Retrievable afterward — it is not a hard delete. A missing ID returns "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: <SendInstance>.Remove()
*
* CloudPage GET context. Proves:
* 1. Remove is an instance method.
* 2. Remove() returns "OK".
* 3. DEV: the send remains Retrievable with Status "Canceled" (not a hard
* delete — official docs / older prose: deletes the record).
* 4. Remove on a missing ID returns "Error" and does NOT throw.
*
* FIXTURE: empty-list send under ssjs-guide-ts-send-rm.
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var EMAIL_KEY = "ssjs-guide-ts-send-rm";
var LIST_KEY = "ssjs-guide-ts-send-rm-list";
var EMAIL_NAME = "SSJS Guide TS Send Rm";
try { Email.Init(EMAIL_KEY).Remove(); } catch (e0) {}
try { List.Init(LIST_KEY).Remove(); } catch (e1) {}
Email.Add({
CustomerKey: EMAIL_KEY,
Name: EMAIL_NAME,
HTMLBody: "<b>rm</b>",
TextBody: "rm",
Subject: "SSJS Guide Send Rm",
EmailType: "HTML",
CharacterSet: "US-ASCII"
});
List.Add({ CustomerKey: LIST_KEY, Name: "SSJS Guide TS Send Rm List", Type: "Public" });
var listId = List.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: LIST_KEY })[0].ID;
Send.Add(EMAIL_KEY, [listId]);
var sendId = Send.Retrieve({ Property: "EmailName", SimpleOperator: "equals", Value: EMAIL_NAME })[0].ID;
try { Send.Init(sendId).CancelSend(); } catch (e2) {}
var inst = Send.Init(sendId);
assert("typeof instance.Remove is function", typeof inst.Remove, "function");
assert("Remove() returns \"OK\"", "" + inst.Remove(), "OK");
var after = Send.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: sendId });
assert("DEV Remove leaves row Retrievable (docs: deletes)", "" + after.length, "1");
assert("DEV Remove sets Status to Canceled (docs: deletes)", "" + after[0].Status, "Canceled");
assert("Remove(missing) returns \"Error\"", "" + Send.Init(999999991).Remove(), "Error");
assert("Remove(missing) does NOT throw", invocationResult(function () { return Send.Init(999999991).Remove(); }), "returned");
Email.Init(EMAIL_KEY).Remove();
List.Init(LIST_KEY).Remove();
</script>
<SendInstance>.CancelSend — returns "status", not "OK"
The official docs describe <SendInstance>.CancelSend() as returning the enum "OK" on success and throwing an error “if something went wrong”. Neither part matches the runtime. CancelSend() returns a string and does not throw on failure — for a missing send it returns the status message "Send [ID: 999999999] not found. [ErrorID: 2063583072]" (a string, not "OK"). So the documented "OK" enum and the “throws on error” behaviour are both wrong: the method reports its outcome via a returned status string. On the success path the return is the literal string "status" rather than "OK". Do not compare the return against "OK" and do not rely on a thrown error to detect failure; inspect the returned status string instead.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs: CancelSend success token is "status", not "OK".
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var EMAIL_KEY = "ssjs-guide-ts-send-cstat";
var LIST_KEY = "ssjs-guide-ts-send-cstat-list";
var EMAIL_NAME = "SSJS Guide TS Send CStat";
try { Email.Init(EMAIL_KEY).Remove(); } catch (e0) {}
try { List.Init(LIST_KEY).Remove(); } catch (e1) {}
Email.Add({
CustomerKey: EMAIL_KEY,
Name: EMAIL_NAME,
HTMLBody: "<b>cstat</b>",
TextBody: "cstat",
Subject: "SSJS Guide Send CStat",
EmailType: "HTML",
CharacterSet: "US-ASCII"
});
List.Add({ CustomerKey: LIST_KEY, Name: "SSJS Guide TS Send CStat List", Type: "Public" });
var listId = List.Retrieve({ Property: "CustomerKey", SimpleOperator: "equals", Value: LIST_KEY })[0].ID;
Send.Add(EMAIL_KEY, [listId]);
var sendId = Send.Retrieve({ Property: "EmailName", SimpleOperator: "equals", Value: EMAIL_NAME })[0].ID;
assert("DEV CancelSend success token is \"status\" (docs: \"OK\")", "" + Send.Init(sendId).CancelSend(), "status");
assert("success token is not \"OK\"", ("" + Send.Init(sendId).CancelSend()) === "OK" ? "true" : "false", "false");
try { Send.Init(sendId).Remove(); } catch (e2) {}
Email.Init(EMAIL_KEY).Remove();
List.Init(LIST_KEY).Remove();
</script>
DateTime.SystemDateToLocalDate / LocalDateToSystemDate — return Date objects, not strings
The official docs type the Core-library DateTime conversion methods as returning a string, but at runtime DateTime.SystemDateToLocalDate(...) and DateTime.LocalDateToSystemDate(...) return genuine Date objects (typeof "object", Object.prototype.toString reports [object Date], .constructor === Date, and getFullYear() / getHours() / getTime() all work — identical to new Date(); only instanceof Date is false, due to the engine-wide instanceof-on-builtins bug, so test with .constructor === Date). They also coerce transparently to a string via String(value), "" + value, or Write(value).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: CONFIRMED live on CloudPage. typeof DateTime = "object"; both
// DateTime.SystemDateToLocalDate and DateTime.LocalDateToSystemDate are "function".
// SystemDateToLocalDate(Now()) => typeof "object" (=== null false), coerces to string
// "Mon, 20 Jul 2026 06:47:17 GMT-06:00" via String()/(""+), getFullYear() => 2026 works
// (genuine Date-like object, NOT a string). LocalDateToSystemDate(local) => typeof "object",
// round-trips back to the original system Now() "Sun, 19 Jul 2026 22:47:17" (inverse, +8h offset).
// Claim that docs type these as string but runtime returns objects coercing to string = CONFIRMED.
Platform.Response.Write("=== DateTime conversion probe ===\n");
Platform.Response.Write("Now: " + Platform.Function.Now() + "\n\n");
// Probe 0: DateTime object + method existence
try {
Platform.Response.Write("typeof DateTime: " + typeof DateTime + "\n");
Platform.Response.Write("typeof DateTime.SystemDateToLocalDate: " + typeof DateTime.SystemDateToLocalDate + "\n");
Platform.Response.Write("typeof DateTime.LocalDateToSystemDate: " + typeof DateTime.LocalDateToSystemDate + "\n");
} catch (e) {
Platform.Response.Write("P0 THREW -> " + (typeof e == "object" ? Platform.Function.Stringify(e) : e) + "\n");
}
// Probe 1: SystemDateToLocalDate with a concrete Date value — observe return type
try {
var local = DateTime.SystemDateToLocalDate(Platform.Function.Now());
// EXPECTED: typeof "object" (CLR DateTime), NOT "string".
Platform.Response.Write("typeof SystemDateToLocalDate return: " + typeof local + "\n");
Platform.Response.Write("=== null? " + (local === null) + "\n");
// EXPECTED: coerces transparently to a string via String()/concatenation.
Platform.Response.Write("as String(): " + String(local) + "\n");
Platform.Response.Write("as (\"\"+): " + ("" + local) + "\n");
try {
Platform.Response.Write("getFullYear(): " + local.getFullYear() + "\n");
} catch (e2) {
Platform.Response.Write("getFullYear() THREW -> " + (typeof e2 == "object" ? Platform.Function.Stringify(e2) : e2) + "\n");
}
} catch (e) {
Platform.Response.Write("P1 THREW -> " + (typeof e == "object" ? Platform.Function.Stringify(e) : e) + "\n");
}
// Probe 2: LocalDateToSystemDate round-trip — observe return type + inverse behaviour
try {
var nowSystem = Platform.Function.Now();
var localD = DateTime.SystemDateToLocalDate(nowSystem);
var back = DateTime.LocalDateToSystemDate(localD);
Platform.Response.Write("typeof LocalDateToSystemDate return: " + typeof back + "\n"); // EXPECTED "object"
Platform.Response.Write("=== null? " + (back === null) + "\n");
Platform.Response.Write("back as String(): " + String(back) + "\n");
Platform.Response.Write("original system Now(): " + nowSystem + "\n");
Platform.Response.Write("local (converted): " + String(localD) + "\n");
} catch (e) {
Platform.Response.Write("P2 THREW -> " + (typeof e == "object" ? Platform.Function.Stringify(e) : e) + "\n");
}
</script>
<WSProxyInstance>.setClientId — returns null, not void
The official docs type the return as void, but at runtime setClientId(options) returns a genuine JS null (typeof "object", strict === null is true), not undefined — matching its sibling resetClientIds. The impersonation works as follows: after setClientId({ ID: <otherBU> }) the very next retrieve runs in the target ClientId’s context (a request for a BU the caller cannot access is rejected with “MemberID … does not have access to ClientID …”, proving the context switched), and after resetClientIds() the baseline BU context is restored. The single argument is an object ({ ID, UserID }) — accepted at runtime for both { ID: 0 } and { ID: 12345 } without throwing; it is not a number as some docs suggest.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage, MCDEV_Training_QA): setClientId({ ID }) returns genuine JS null --
// typeof result: object; result === null: true; result === undefined: false. Confirms return is
// null, NOT void/undefined. typeof api.setClientId is "clrmethodinfo" (.length is undefined, so
// arity is not a JS number). Single OBJECT argument { ID } accepted for both { ID: 0 } and
// { ID: 12345 } without throwing (number-typed arg claim would be wrong). Non-destructive:
// configuring the client context does not mutate data; resetClientIds() also returns null.
Platform.Response.Write("=== setClientId NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var apiA = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.setClientId: " + (typeof apiA.setClientId) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.setClientId.length (arity): " + apiA.setClientId.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: call with single OBJECT argument { ID: 0 }. Observe return value + strict type.
try {
var apiB = new Script.Util.WSProxy();
var result = apiB.setClientId({ ID: 0 });
Platform.Response.Write("B typeof result: " + (typeof result) + "\n"); // OBSERVED: object
Platform.Response.Write("B result === null: " + (result === null) + "\n"); // OBSERVED: true
Platform.Response.Write("B result === undefined: " + (result === undefined) + "\n"); // OBSERVED: false
Platform.Response.Write("B typeof result == 'undefined': " + (typeof result == "undefined") + "\n"); // OBSERVED: false
var reset = apiB.resetClientIds();
Platform.Response.Write("B reset === null: " + (reset === null) + "\n"); // OBSERVED: true
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: call with a plausible non-zero client-id object { ID: 12345 }. Non-destructive; then reset.
try {
var apiC = new Script.Util.WSProxy();
var resultC = apiC.setClientId({ ID: 12345 });
Platform.Response.Write("C typeof result: " + (typeof resultC) + "\n"); // OBSERVED: object
Platform.Response.Write("C result === null: " + (resultC === null) + "\n"); // OBSERVED: true
var resetC = apiC.resetClientIds();
Platform.Response.Write("C reset === null: " + (resetC === null) + "\n"); // OBSERVED: true
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.resetClientIds — returns null, not void
The official docs type the return as void, but at runtime resetClientIds() returns a genuine JS null (typeof "object", strict === null is true), not undefined. The method itself works as documented: a retrieve after setClientId({ ID: <otherBU> }) targets the impersonated BU, and after resetClientIds() the very next retrieve returns to the script’s own default BU context (identical result set to the pre-impersonation baseline).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Probe: WSProxy.resetClientIds() return value. Docs type return as void; engine returns
// genuine JS null (typeof "object", === null). Method restores the script's default BU context.
// OBSERVED (live CloudPage): typeof prox.resetClientIds = "clrmethodinfo"; typeof result = "object";
// OBSERVED: result === null: true; result === undefined: false; String(result) = "null" -> CONFIRMED (returns null, not void).
try {
var api = new Script.Util.WSProxy();
var result = api.resetClientIds();
// EXPECTED: result === null (typeof "object"), NOT undefined.
Platform.Response.Write("typeof result: " + typeof result + "\n"); // OBSERVED: "object"
Platform.Response.Write("result === null: " + (result === null) + "\n"); // OBSERVED: true
Platform.Response.Write("result === undefined: " + (result === undefined) + "\n"); // OBSERVED: false
} catch (e) {
Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n");
}
</script>
Date.prototype.getMilliseconds() — off-by-one for some values
MDN specifies getMilliseconds() returns the exact milliseconds (0–999) of the date. In the SFMC Jint engine some values come back one less than they were set: a date constructed with 123 ms reports 122; 555 → 554, 666 → 665, 777 → 776. Other values (0, 111, 678, 888, 999) are exact. typeof d.getMilliseconds is function, and constructing with 678 ms (via new Date(2020,0,1,12,30,45,678) or new Date(1577881845678)) correctly returns 678 — so the method is not globally broken, only off-by-one for specific values. Never compare sub-second precision — round or avoid milliseconds.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (live CloudPage): typeof getMilliseconds = function; 678 (both constructions) -> 678 exact;
// OBSERVED off-by-one: 123->122, 555->554, 666->665, 777->776, 122->121, 554->553; exact: 0,111,888,999,678
// typeof check
try {
var d0 = new Date(2020, 0, 1, 12, 30, 45, 678);
Platform.Response.Write("typeof d.getMilliseconds = " + (typeof d0.getMilliseconds) + "\n");
} catch (e) { Platform.Response.Write("THREW typeof -> " + Platform.Function.Stringify(e) + "\n"); }
// Known-millisecond constructions (both return 678, exact)
try {
var da = new Date(2020, 0, 1, 12, 30, 45, 678);
Platform.Response.Write("new Date(2020,0,1,12,30,45,678).getMilliseconds() = " + da.getMilliseconds() + " (std JS expects 678)\n");
var db = new Date(1577881845678);
Platform.Response.Write("new Date(1577881845678).getMilliseconds() = " + db.getMilliseconds() + " (std JS expects 678)\n");
} catch (e) { Platform.Response.Write("THREW ctor -> " + Platform.Function.Stringify(e) + "\n"); }
// Off-by-one sample sweep
try {
var samples = [123, 555, 666, 777, 0, 111, 888, 999, 678, 122, 554];
for (var i = 0; i < samples.length; i++) {
var ms = samples[i];
var d = new Date(2026, 0, 1, 0, 0, 0, ms);
Platform.Response.Write("set " + ms + " -> got " + d.getMilliseconds() + "\n");
}
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Date.now() — returns a Date object, not a number
MDN specifies Date.now() as a static method that returns a Number (milliseconds since the epoch). In the SFMC Jint engine the method does exist (typeof Date.now is "function") but it instead returns a Date object (typeof Date.now() is "object"; it stringifies to a date-time string). Numeric coercion (Date.now() + 0 or Date.now() * 1) recovers the epoch milliseconds as a clean number, but any code that treats the return value as a number without coercion will break. Prefer new Date().getTime(), which returns a clean number.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof Date.now = "function"; Date.now() returns a Date OBJECT
// (typeof "object", stringifies to "Sun, 19 Jul 2026 23:08:17 GMT-06:00"), NOT a number.
// Numeric coercion (+0 / *1) recovers epoch ms as number 1784524097293. new Date().getTime()
// returns a clean number. VERDICT: CONFIRMED.
Platform.Response.Write("P1 typeof Date.now: " + (typeof Date.now) + "\n");
try {
var n = Date.now();
Platform.Response.Write("P2 typeof Date.now(): " + (typeof n) + "\n");
Platform.Response.Write("P2 raw value: " + n + "\n");
Platform.Response.Write("P3 (Date.now() + 0): " + (Date.now() + 0) + "\n");
Platform.Response.Write("P3 typeof (Date.now() + 0): " + (typeof (Date.now() + 0)) + "\n");
var t = new Date().getTime();
Platform.Response.Write("P4 typeof new Date().getTime(): " + (typeof t) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Date.UTC() — year-only form returns a nonsense value, not NaN
With year + month (and further components) Date.UTC() returns the correct UTC timestamp. But the year-only form Date.UTC(2026) returns a nonsense small number (observed -21597974) instead of a valid timestamp — and does not return NaN. Always pass at least year and month, e.g. Date.UTC(2026, 0, 1).
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Compare year-only vs year+month forms of Date.UTC()
try {
// EXPECTED (SFMC): year-only returns a nonsense small number (e.g. -21597974), not NaN
var yearOnly = Date.UTC(2026);
Platform.Response.Write("Date.UTC(2026): " + yearOnly + " | isNaN=" + isNaN(yearOnly) + "\n");
// EXPECTED (SFMC): year+month returns a correct large UTC timestamp
var yearMonth = Date.UTC(2026, 0, 1);
Platform.Response.Write("Date.UTC(2026,0,1): " + yearMonth + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Function.prototype.toString() — returns "[object Function]", not source
MDN specifies that fn.toString() returns a string containing the function source code. In the SFMC Jint engine it instead returns the generic [object Function] object tag. String(fn) and ("" + fn) yield "function" rather than the source. fn.toString itself is a function, but native/built-in functions (e.g. parseInt) do not even expose a working toString — calling it throws Object expected: toString, and String(parseInt) throws. Do not rely on reading a function’s source at runtime for introspection or hashing.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): foo.toString() = "[object Function]" (typeof string); String(foo) = "function"; ("" + foo) = "function"; typeof foo.toString = "function"; parseInt.toString() THREW "Object expected: toString"; String(parseInt) THREW "Input string was not in a correct format." => CONFIRMED
function foo(a, b) { return a + b; }
// Probe 1: user function toString() returns the object tag, not source
try {
var t = foo.toString();
Platform.Response.Write("foo.toString() = [" + t + "] (typeof " + (typeof t) + ")\n");
} catch (e) { Platform.Response.Write("THREW -> " + e.message + "\n"); }
// Probe 2: String(foo) coerces to "function", not source
try { Platform.Response.Write("String(foo) = [" + String(foo) + "]\n"); }
catch (e) { Platform.Response.Write("THREW -> " + e.message + "\n"); }
// Probe 3: concat coercion ("" + foo) also yields "function"
try { Platform.Response.Write("(\"\" + foo) = [" + ("" + foo) + "]\n"); }
catch (e) { Platform.Response.Write("THREW -> " + e.message + "\n"); }
// Probe 4: foo.toString is itself a function
try { Platform.Response.Write("typeof foo.toString = " + (typeof foo.toString) + "\n"); }
catch (e) { Platform.Response.Write("THREW -> " + e.message + "\n"); }
// Probe 5: native function toString throws (no source introspection at all)
try { Platform.Response.Write("parseInt.toString() = [" + parseInt.toString() + "]\n"); }
catch (e) { Platform.Response.Write("parseInt.toString() THREW -> " + e.message + "\n"); }
// Probe 6: String() of a native function throws
try { Platform.Response.Write("String(parseInt) = [" + String(parseInt) + "]\n"); }
catch (e) { Platform.Response.Write("String(parseInt) THREW -> " + e.message + "\n"); }
</script>
Function.prototype.length — throws instead of returning arity
MDN specifies that fn.length returns the function’s arity (the number of declared parameters). In the SFMC Jint engine reading fn.length throws Object reference not set to an instance of an object., and fn.hasOwnProperty("length") is false. There is no runtime way to read a function’s declared parameter count — track expected arity yourself.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): sum2.length THREW "Object reference not set to an instance of an object."; sum2.hasOwnProperty("length") = false. CONFIRMED.
function sum2(a, b) { return a + b; }
// Probe 1: reading fn.length throws
try { Platform.Response.Write("sum2.length = " + sum2.length + "\n"); }
catch (e) { Platform.Response.Write("sum2.length THREW -> " + e.message + "\n"); }
// Probe 2: the property is not even own
try { Platform.Response.Write("hasOwnProperty(length) = " + sum2.hasOwnProperty("length") + "\n"); }
catch (e) { Platform.Response.Write("hasOwnProperty THREW -> " + e.message + "\n"); }
</script>
Function.prototype.constructor identity is broken
MDN specifies that a function’s constructor is the Function object, so fn.constructor === Function is true. In the SFMC engine fn instanceof Function is true (as expected) but fn.constructor === Function is false — the constructor-identity link is broken. Notably this is specific to Function: ({}).constructor === Object and [].constructor === Array both return true, so only the Function identity is affected. Test “is this a function” with instanceof Function (or typeof fn === "function"), never with a fn.constructor === Function comparison.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): foo.constructor === Function -> false (identity broken);
// typeof foo.constructor -> function; foo.constructor === foo.constructor -> true (stable);
// ({}).constructor === Object -> true; [].constructor === Array -> true;
// foo instanceof Function -> true; typeof foo -> function. CONFIRMED.
/**
* Sample function used to probe constructor identity.
*/
function foo() {}
try {
// constructor identity link for a function is broken -> false
Platform.Response.Write("foo.constructor === Function: " + (foo.constructor === Function) + "\n");
// the constructor is still a function object
Platform.Response.Write("typeof foo.constructor: " + (typeof foo.constructor) + "\n");
// ...and stable across reads
Platform.Response.Write("foo.constructor === foo.constructor: " + (foo.constructor === foo.constructor) + "\n");
// Object/Array constructor identities DO work
Platform.Response.Write("({}).constructor === Object: " + (({}).constructor === Object) + "\n");
Platform.Response.Write("[].constructor === Array: " + ([].constructor === Array) + "\n");
// instanceof and typeof both correctly identify a function
Platform.Response.Write("foo instanceof Function: " + (foo instanceof Function) + "\n");
Platform.Response.Write("typeof foo: " + (typeof foo) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Number constants — classic ES3 statics return wrong values; ES6 statics undefined
The classic ES3 Number constants DO exist in the SFMC Jint engine (typeof Number.MAX_VALUE === "number"), but several return wrong values: Number.MAX_VALUE is correct (1.79769313486232e+308), yet Number.MIN_VALUE is -1.79769313486232e+308 (a large negative, not the standard smallest-positive 5e-324), and Number.POSITIVE_INFINITY / Number.NEGATIVE_INFINITY have their signs swapped — POSITIVE_INFINITY reads back as negative (< 0, not > MAX_VALUE) and NEGATIVE_INFINITY reads back as positive. Number.NaN behaves correctly (Number.NaN === Number.NaN is false). The ES6 statics are genuinely undefined (Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.EPSILON, Number.isInteger, Number.isNaN). Do not trust Number.MIN_VALUE or the Number.*_INFINITY constants; use the global NaN/Infinity identifiers or numeric literals instead. Note the global Infinity is itself unreliable — see Known Bugs.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// OBSERVED (runtime, CloudPage): classic ES3 statics are DEFINED (typeof=number) but
// several return WRONG values; ES6 statics are undefined. NaN behaves correctly.
// MAX_VALUE -> 1.79769313486232e+308 (correct, >0)
// MIN_VALUE -> -1.79769313486232e+308 (WRONG: large negative, not 5e-324)
// POSITIVE_INFINITY -> String "-infinity", <0, NOT > MAX_VALUE (WRONG sign)
// NEGATIVE_INFINITY -> String "infinity", >0, NEGATIVE_INFINITY<0 is false (WRONG sign)
// NaN -> Number.NaN===Number.NaN is false (correct)
// ES6: MAX_SAFE_INTEGER/MIN_SAFE_INTEGER/EPSILON/isInteger/isNaN all typeof=undefined
try {
Platform.Response.Write("typeof Number = " + (typeof Number) + "\n");
Platform.Response.Write("MAX_VALUE typeof=" + (typeof Number.MAX_VALUE) + " val=" + String(Number.MAX_VALUE) + " >0? " + (Number.MAX_VALUE > 0) + "\n");
Platform.Response.Write("MIN_VALUE typeof=" + (typeof Number.MIN_VALUE) + " val=" + String(Number.MIN_VALUE) + " <0? " + (Number.MIN_VALUE < 0) + "\n");
Platform.Response.Write("POSITIVE_INFINITY typeof=" + (typeof Number.POSITIVE_INFINITY) + " val=" + String(Number.POSITIVE_INFINITY) + " >MAX? " + (Number.POSITIVE_INFINITY > Number.MAX_VALUE) + "\n");
Platform.Response.Write("NEGATIVE_INFINITY typeof=" + (typeof Number.NEGATIVE_INFINITY) + " val=" + String(Number.NEGATIVE_INFINITY) + " <0? " + (Number.NEGATIVE_INFINITY < 0) + "\n");
Platform.Response.Write("NaN self-eq? " + (Number.NaN === Number.NaN) + "\n");
Platform.Response.Write("MAX_SAFE_INTEGER typeof=" + (typeof Number.MAX_SAFE_INTEGER) + "\n");
Platform.Response.Write("MIN_SAFE_INTEGER typeof=" + (typeof Number.MIN_SAFE_INTEGER) + "\n");
Platform.Response.Write("EPSILON typeof=" + (typeof Number.EPSILON) + "\n");
Platform.Response.Write("isInteger typeof=" + (typeof Number.isInteger) + "\n");
Platform.Response.Write("isNaN typeof=" + (typeof Number.isNaN) + "\n");
Platform.Response.Write("global NaN=" + NaN + " | global Infinity=" + Infinity + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Object.prototype.propertyIsEnumerable — always returns false
MDN specifies propertyIsEnumerable(prop) returns true for an own enumerable property. In the SFMC Jint engine it is present (typeof = function) but broken — it returns false even for own enumerable properties. Unlike the sibling isPrototypeOf, calling it does not hang the engine; it simply always returns false. Use hasOwnProperty for own-property checks instead.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof Object.prototype.propertyIsEnumerable = "function"; the
// call returns and does NOT hang (unlike isPrototypeOf). own-enumerable 'foo' -> false (BROKEN;
// standard JS returns true); inherited 'toString' -> false; absent 'nope' -> false; control
// hasOwnProperty('foo') -> true. Claim CONFIRMED: present but always returns false.
Platform.Response.Write("typeof Object.prototype.propertyIsEnumerable = " + (typeof Object.prototype.propertyIsEnumerable) + "\n");
try {
var obj = {};
obj.foo = "bar";
// own enumerable property: standard JS = true, SFMC = false (broken)
Platform.Response.Write("propertyIsEnumerable('foo') [own enumerable] = " + obj.propertyIsEnumerable("foo") + "\n");
// inherited property: standard JS = false, SFMC = false
Platform.Response.Write("propertyIsEnumerable('toString') [inherited] = " + obj.propertyIsEnumerable("toString") + "\n");
// absent property: standard JS = false, SFMC = false
Platform.Response.Write("propertyIsEnumerable('nope') [absent] = " + obj.propertyIsEnumerable("nope") + "\n");
// control: hasOwnProperty works correctly -> true
Platform.Response.Write("hasOwnProperty('foo') = " + obj.hasOwnProperty("foo") + "\n");
} catch (e) { Platform.Response.Write("THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Object ES5/ES6 statics — most are missing
MDN documents a large set of Object statics. In the SFMC Jint engine only Object.defineProperty and Object.getPrototypeOf are present and working — keys, values, entries, assign, create, freeze, isFrozen, defineProperties, getOwnPropertyNames, getOwnPropertyDescriptor, seal, isSealed, preventExtensions, and isExtensible are all undefined. Use a for...in loop with hasOwnProperty for key/value enumeration; there is no runtime immutability or extensibility control. See Object Methods for per-member workarounds.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage 2026-07-20): Object.defineProperty = function (callable, sets o.x=42),
// Object.getPrototypeOf = function (returns object). ALL of keys, values, entries, assign, create,
// freeze, isFrozen, defineProperties, getOwnPropertyNames, getOwnPropertyDescriptor, seal, isSealed,
// preventExtensions, isExtensible = undefined. Claim CONFIRMED.
// IMPORTANT: probe statics via DIRECT dot access, NOT Object[varName] bracket access — the Jint engine
// returns undefined for defineProperty/getPrototypeOf when accessed via a computed bracket key (false negative).
Platform.Response.Write("Object.keys = " + (typeof Object.keys) + "\n");
Platform.Response.Write("Object.values = " + (typeof Object.values) + "\n");
Platform.Response.Write("Object.entries = " + (typeof Object.entries) + "\n");
Platform.Response.Write("Object.assign = " + (typeof Object.assign) + "\n");
Platform.Response.Write("Object.create = " + (typeof Object.create) + "\n");
Platform.Response.Write("Object.freeze = " + (typeof Object.freeze) + "\n");
Platform.Response.Write("Object.isFrozen = " + (typeof Object.isFrozen) + "\n");
Platform.Response.Write("Object.defineProperties = " + (typeof Object.defineProperties) + "\n");
Platform.Response.Write("Object.getOwnPropertyNames = " + (typeof Object.getOwnPropertyNames) + "\n");
Platform.Response.Write("Object.getOwnPropertyDescriptor = " + (typeof Object.getOwnPropertyDescriptor) + "\n");
Platform.Response.Write("Object.seal = " + (typeof Object.seal) + "\n");
Platform.Response.Write("Object.isSealed = " + (typeof Object.isSealed) + "\n");
Platform.Response.Write("Object.preventExtensions = " + (typeof Object.preventExtensions) + "\n");
Platform.Response.Write("Object.isExtensible = " + (typeof Object.isExtensible) + "\n");
// Present and working:
Platform.Response.Write("Object.defineProperty = " + (typeof Object.defineProperty) + "\n");
Platform.Response.Write("Object.getPrototypeOf = " + (typeof Object.getPrototypeOf) + "\n");
try {
var o = {};
Object.defineProperty(o, "x", { value: 42 });
Platform.Response.Write("Object.defineProperty CALL ok, o.x = " + o.x + "\n");
} catch (e) { Platform.Response.Write("defineProperty CALL THREW -> " + Platform.Function.Stringify(e) + "\n"); }
try {
Platform.Response.Write("Object.getPrototypeOf CALL, typeof result = " + (typeof Object.getPrototypeOf({})) + "\n");
} catch (e) { Platform.Response.Write("getPrototypeOf CALL THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
RegExp instanceof is always false — use .constructor === RegExp
MDN specifies that re instanceof RegExp is true for any regular expression, whether created as a literal (/…/) or with new RegExp(…). In the SFMC Jint engine instanceof RegExp is always false — even for new RegExp("abc"). A regex literal also fails instanceof Object (also false), and typeof /abc/ is the non-standard "regexp". For RegExp, re.constructor === RegExp correctly returns true, so use the constructor comparison (not instanceof) to detect a RegExp.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof RegExp=function; typeof /abc/=regexp;
// /abc/ instanceof RegExp=false; /abc/ instanceof Object=false;
// (new RegExp("abc")) instanceof RegExp=false; /abc/.constructor === RegExp=true
// => CONFIRMED: instanceof RegExp is always false; detect via .constructor === RegExp
try {
Platform.Response.Write("typeof RegExp = " + (typeof RegExp) + "\n");
var r = /abc/;
Platform.Response.Write("typeof (/abc/) = " + (typeof r) + "\n");
Platform.Response.Write("(/abc/) instanceof RegExp = " + (r instanceof RegExp) + "\n");
Platform.Response.Write("(/abc/) instanceof Object = " + (r instanceof Object) + "\n");
var ctor = new RegExp("abc");
Platform.Response.Write("(new RegExp('abc')) instanceof RegExp = " + (ctor instanceof RegExp) + "\n");
Platform.Response.Write("(/abc/).constructor === RegExp = " + (r.constructor === RegExp) + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
RegExp.ignoreCase / multiline — undefined
MDN specifies RegExp.prototype.ignoreCase and RegExp.prototype.multiline as boolean accessors reflecting the i and m flags. In the SFMC Jint engine both are undefined on every RegExp, even when the corresponding flag is set — the flags themselves still work at match time (/x/i is case-insensitive, /^…/m matches line starts), you just cannot read them back. source, global, and lastIndex are the only readable accessors. Track the i/m flags yourself if you need them.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof re.ignoreCase=undefined, re.ignoreCase=undefined; typeof re.multiline=undefined, re.multiline=undefined; re.global=true (boolean); re.source="ab" (string); re.lastIndex=0 (number). Behaviour: /AB/i.test("ab xyz")=true vs /AB/.test=false; /^world/m.test("hello\nworld")=true vs no-m=false. CONFIRMED: ignoreCase/multiline reflection undefined but the i/m flags still work at match time; source/global/lastIndex readable.
// Probe: RegExp instance flag-reflection properties on /ab/gim
try {
var re = /ab/gim;
Platform.Response.Write("typeof re.ignoreCase: " + (typeof re.ignoreCase) + "\n");
Platform.Response.Write("re.ignoreCase: " + re.ignoreCase + "\n");
Platform.Response.Write("typeof re.multiline: " + (typeof re.multiline) + "\n");
Platform.Response.Write("re.multiline: " + re.multiline + "\n");
Platform.Response.Write("typeof re.global: " + (typeof re.global) + " | re.global: " + re.global + "\n");
Platform.Response.Write("typeof re.source: " + (typeof re.source) + " | re.source: " + re.source + "\n");
Platform.Response.Write("typeof re.lastIndex: " + (typeof re.lastIndex) + " | re.lastIndex: " + re.lastIndex + "\n");
} catch (e) { Platform.Response.Write("PROBE1 THREW: " + Platform.Function.Stringify(e) + "\n"); }
// Probe: i flag STILL WORKS behaviourally (case-insensitive match)
try {
Platform.Response.Write("/AB/i.test('ab xyz'): " + (/AB/i.test("ab xyz")) + "\n");
Platform.Response.Write("/AB/.test('ab xyz') (no i): " + (/AB/.test("ab xyz")) + "\n");
} catch (e) { Platform.Response.Write("PROBE2 THREW: " + Platform.Function.Stringify(e) + "\n"); }
// Probe: m flag STILL WORKS behaviourally (multiline anchor)
try {
Platform.Response.Write("/^world/m.test('hello\\nworld'): " + (/^world/m.test("hello\nworld")) + "\n");
Platform.Response.Write("/^world/.test('hello\\nworld') (no m): " + (/^world/.test("hello\nworld")) + "\n");
} catch (e) { Platform.Response.Write("PROBE3 THREW: " + Platform.Function.Stringify(e) + "\n"); }
</script>
String.match — no-match returns [] not null; matches lack .index
MDN specifies String.prototype.match(regexp) returns null when there is no match, and that (for a non-global pattern) the returned match array carries an index property giving the match position. In the SFMC Jint engine a no-match returns an empty array [], not null, so the classic if (str.match(re) === null) guard never fires — test result.length instead. Returned matches also expose no .index property. The captured element [0] (and, for a non-global pattern, the capture groups) are otherwise correct.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): "a1b2".match(/\d/) => ["1"] (length 1, no .index); "abc".match(/\d/) => [] (empty array, NOT null). VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("match hit", function(){ return Platform.Function.Stringify("a1b2".match(/\d/)); });
P("match nomatch is null?", function(){ var m="abc".match(/\d/); return (m===null?"null":"NOT null: "+Platform.Function.Stringify(m)); });
</script>
String.split — split("") does not split into characters
MDN specifies String.prototype.split("") with an empty-string separator splits the string into an array of its individual characters. In the SFMC Jint engine the empty-separator form does not split — "abc".split("") returns ["abc"] (a single-element array), not ["a", "b", "c"]. Splitting on a real separator ("a,b,c".split(",")) works per spec. For per-character access, iterate with charAt (guarding the index against .length) instead of split("").
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): "a,b,c".split(",") => ["a","b","c"]; "abc".split("") => ["abc"] (NOT per-character). VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("split(',')", function(){ return Platform.Function.Stringify("a,b,c".split(",")); });
P("split('') empty", function(){ return Platform.Function.Stringify("abc".split("")); });
</script>
Number.toLocaleString — locale argument ignored — no grouping separators
MDN specifies Number.prototype.toLocaleString([locales[, options]]) returns a locale-aware string with grouping separators (e.g. (123456.789).toLocaleString("de-DE") → "123.456,789"). In the SFMC Jint engine the locale/options arguments are ignored and the method returns the plain number string with no grouping — (123456.789).toLocaleString() and (123456.789).toLocaleString("de-DE") both return "123456.789". Use AMPscript’s FormatNumber(value, format, culture) via Platform.Function.TreatAsContent for real locale-aware number formatting.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): toLocaleString()="123456.789"; toLocaleString("de-DE")="123456.789" (locale ignored, no grouping). VERDICT: CONFIRMED.
Platform.Response.Write("toLocaleString(): " + (123456.789).toLocaleString() + "\n");
Platform.Response.Write("toLocaleString('de-DE'): " + (123456.789).toLocaleString("de-DE") + "\n");
</script>
Date.toLocaleDateString — locale argument ignored — fixed English format
MDN specifies Date.prototype.toLocaleDateString([locales[, options]]) returns a locale-aware date string. In the SFMC Jint engine the locale/options arguments are ignored and a fixed English-style format is returned — new Date(2020, 0, 15).toLocaleDateString() returns "Wed, 15 Jan 2020". Use AMPscript’s FormatDate(date, format, ..., culture) via Platform.Function.TreatAsContent for real locale-aware date formatting.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): new Date(2020,0,15).toLocaleDateString()="Wed, 15 Jan 2020" (locale ignored, fixed English format). VERDICT: CONFIRMED.
var d = new Date(2020, 0, 15);
Platform.Response.Write("toLocaleDateString(): " + d.toLocaleDateString() + "\n");
</script>
String.toLocaleUpperCase — locale argument ignored — behaves like toUpperCase()
MDN specifies String.prototype.toLocaleUpperCase([locales]) performs locale-aware uppercasing (e.g. Turkish dotted/dotless i handling). In the SFMC Jint engine the locale argument is ignored and the method behaves exactly like toUpperCase() — "abc".toLocaleUpperCase() returns "ABC" with no locale-specific casing.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): "abc".toLocaleUpperCase()="ABC" (locale ignored, plain toUpperCase). VERDICT: CONFIRMED.
Platform.Response.Write("toLocaleUpperCase(): " + "abc".toLocaleUpperCase() + "\n");
</script>
String.toLocaleLowerCase — locale argument ignored — behaves like toLowerCase()
MDN specifies String.prototype.toLocaleLowerCase([locales]) performs locale-aware lowercasing. In the SFMC Jint engine the locale argument is ignored and the method behaves exactly like toLowerCase() — "ABC".toLocaleLowerCase() returns "abc" with no locale-specific casing.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): "ABC".toLocaleLowerCase()="abc" (locale ignored, plain toLowerCase). VERDICT: CONFIRMED.
Platform.Response.Write("toLocaleLowerCase(): " + "ABC".toLocaleLowerCase() + "\n");
</script>
Array.slice — no-argument slice() throws instead of copying the whole array
MDN specifies Array.prototype.slice() with no arguments returns a shallow copy of the entire array. In the SFMC Jint engine the no-argument form throws Index was outside the bounds of the array. — you must pass an explicit start index, e.g. arr.slice(0), to copy the whole array. Positive and negative indices (slice(1, 3), slice(-2), slice(1, -1)) otherwise behave per spec.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): slice(1,3)="1,2"; slice(-2)="3,4"; slice(1,-1)="1,2,3"; slice(0)="0,1,2,3,4"; slice() THROWS "Index was outside the bounds of the array." VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("slice(1,3)", function(){ return [0,1,2,3,4].slice(1,3).join(","); });
P("slice(-2)", function(){ return [0,1,2,3,4].slice(-2).join(","); });
P("slice(0)", function(){ return [0,1,2,3,4].slice(0).join(","); });
P("slice() NOARG", function(){ return [0,1,2,3,4].slice().join(","); });
</script>
Array.sort — no-argument sort() throws instead of sorting lexicographically
MDN specifies Array.prototype.sort() with no comparator sorts elements as strings (lexicographic order). In the SFMC Jint engine the no-argument form throws Failed to compare two elements in the array. — always pass an explicit compare function, e.g. arr.sort(function (a, b) { return a - b; }). A supplied comparator otherwise sorts per spec.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): sort(cmp)="1,1,3,4,5"; sort() THROWS "Failed to compare two elements in the array." VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("sort(cmp)", function(){ var a=[3,1,4,1,5]; a.sort(function(x,y){return x-y;}); return a.join(","); });
P("sort() NOARG", function(){ var a=[3,1,4,1,5]; a.sort(); return a.join(","); });
</script>
parseInt — trailing non-numeric characters yield NaN instead of the leading number
MDN specifies the global parseInt(str[, radix]) parses the leading numeric portion and ignores trailing non-numeric characters — parseInt("10px", 10) is 10. In the SFMC Jint engine a string with trailing non-numeric characters returns NaN (parseInt("10px", 10) is NaN). Radix parsing itself follows the spec (parseInt("255", 16) → 597, parseInt("0x1F") → 31). Strip non-digit characters before parsing.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): parseInt("42",10)=42; parseInt("255",16)=597; parseInt("0x1F")=31; parseInt("10px",10)=NaN. VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("parseInt('255',16)", function(){ return parseInt("255",16); });
P("parseInt('10px',10)", function(){ return parseInt("10px",10); });
</script>
parseFloat — NaN on trailing non-numeric chars; 32-bit precision
MDN specifies the global parseFloat(str) parses the leading numeric portion, ignoring trailing non-numeric characters, and returns a double-precision float. In the SFMC Jint engine a string with trailing non-numeric characters returns NaN (parseFloat("1.5kg") is NaN), and results use 32-bit precision — parseFloat("3.14") returns 3.14000010490417, so parseFloat("3.14") === 3.14 is false. Never compare parsed floats with ===; use a tolerance.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): parseFloat("3.14")=3.14000010490417; parseFloat("3.14")===3.14 => false; parseFloat("1.5kg")=NaN. VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("parseFloat('3.14')", function(){ return parseFloat("3.14"); });
P("parseFloat('3.14')===3.14", function(){ return parseFloat("3.14") === 3.14; });
P("parseFloat('1.5kg')", function(){ return parseFloat("1.5kg"); });
</script>
isFinite — returns true for a non-numeric string
MDN specifies the global isFinite(value) applies ToNumber first and returns false when the conversion yields NaN, so isFinite("abc") is false. In the SFMC Jint engine isFinite("abc") and isFinite(Number("abc")) both return true. The NaN handling itself is spec-correct — isFinite(NaN), isFinite(undefined), isFinite(0 / 0) and isFinite(Infinity) all return false, and isFinite(42) / isFinite("42") return true. isFinite("") and isFinite(null) also return true, but that matches MDN, where ToNumber("") and ToNumber(null) are both 0. Guard untrusted input by coercing explicitly and testing the result with isNaN(Number(value)) first. See isFinite Returns true for a Non-Numeric String.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (SFMC CloudPage, live): isFinite("abc")=true (spec: false); isFinite(Number("abc"))=true (spec: false); isFinite("")=true and isFinite(null)=true (both spec-correct); isFinite(NaN)=false. VERDICT: CONFIRMED.
function P(l,f){try{Platform.Response.Write(l+" => "+f()+"\n");}catch(e){Platform.Response.Write(l+" => ERR: "+e+"\n");}}
P("isFinite('abc')", function(){ return isFinite("abc"); });
P("isFinite('')", function(){ return isFinite(""); });
P("isFinite(null)", function(){ return isFinite(null); });
P("isFinite(Number('abc'))", function(){ return isFinite(Number("abc")); });
P("isFinite(NaN)", function(){ return isFinite(NaN); });
</script>
.Update</code></a> — properties argument is optional; an Active definition rejects the update</h3>
The official docs list properties as a required argument and mention no state requirement. At runtime the argument is effectively optional - Update() with no arguments returns the string "OK" - and there is an undocumented state gate: calling Update on a definition whose TriggeredSendStatus is Active returns the string "Error" with LastMessage An active TriggeredSendDefinition can not be updated or have it's content refreshed and LastErrorCode 17003. Call Pause() first, update, then Start() again. A successful update returns "OK" with LastMessage TriggeredSendDefinition updated. Passing a non-object (for example a string) throws Error Updating TSD. with LastMessage Invalid cast from 'Char' to 'Double'..
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Update() with NO arguments returns "OK". Update on an ACTIVE definition returns "Error"
// with LastMessage "An active TriggeredSendDefinition can not be updated..." / LastErrorCode 17003.
var ts = TriggeredSend.Init("your_tsd_customer_key");
Platform.Response.Write("Pause: " + ts.Pause() + "\n");
Platform.Response.Write("Update(no args): " + ts.Update() + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("Update({Description}): " + ts.Update({ Description: "probe " + Platform.Function.Now() }) + "\n");
Platform.Response.Write("Start: " + ts.Start() + "\n");
Platform.Response.Write("Update while Active: " + ts.Update({ Description: "x" }) + " | " + TriggeredSend.LastMessage + " | " + TriggeredSend.LastErrorCode + "\n");
</script>
</div>
.Publish</code></a> — returns "OK" but does not make the definition active</h3>
The official docs describe Publish() as the call that makes a triggered send definition active. At runtime it returns the string "OK" with LastMessage TriggeredSendDefinition updated, but a follow-up WSProxy retrieve showed TriggeredSendStatus still New - only the subsequent Start() moved it to Active. Extra arguments are ignored rather than rejected: Publish("x") also returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Publish() -> "OK" / "TriggeredSendDefinition updated", but the retrieved status is still "New".
// Start() is what sets TriggeredSendStatus "Active".
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("before: " + status() + "\n");
Platform.Response.Write("Publish(): " + ts.Publish() + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("after Publish: " + status() + "\n");
Platform.Response.Write("Start(): " + ts.Start() + "\n");
Platform.Response.Write("after Start: " + status() + "\n");
</script>
</div>
.Send</code></a> — accepts a third subscriberKey argument; works on an Inactive definition; returns "Error" for a bad address</h3>
Runtime-proven with real sends - a successful call returns the string "OK" with LastMessage Created TriggeredSend. Four details differ from the official docs. (1) Arity: the docs document Send(emailAddress, sendTimeAttributes), but a third subscriberKey argument is accepted and still returns "OK"; arguments beyond the third are ignored (a four-argument call also returns "OK"). (2) State: the definition does not have to be Active - a send against an Inactive definition still returned "OK" / Created TriggeredSend. (3) Failure mode: an invalid address does not throw; it returns the string "Error" with LastMessage Unable to queue Triggered Send request. There are no valid subscribers.. (4) Calling Send() with no arguments throws the usage string Usage: Send(EmailAddress [, sendTimeAttributes]). TriggeredSend.LastRequestID was 0 after a successful send.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Send(addr) -> "OK" / "Created TriggeredSend"; a THIRD subscriberKey arg is accepted;
// an Inactive definition still sends; an invalid address returns the STRING "Error" (no throw);
// Send() with no arguments throws "Usage: Send(EmailAddress [, sendTimeAttributes])".
var ts = TriggeredSend.Init("your_tsd_customer_key");
Platform.Response.Write("Send(addr): " + ts.Send("you@example.com") + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("Send(addr, attrs, subKey): " + ts.Send("you@example.com", { Foo: "bar" }, "sub-key") + "\n");
Platform.Response.Write("Send(bad): " + ts.Send("not-an-address") + " | " + TriggeredSend.LastMessage + "\n");
try { ts.Send(); } catch (e) { Platform.Response.Write("Send() THREW -> " + String(e) + "\n"); }
</script>
</div>
encodeURI / encodeURIComponent — space becomes + and hex escapes are lowercase (form-urlencoded, not RFC 3986)
MDN specifies both functions encode a space as %20 and emit uppercase hex digits. The SFMC Jint engine encodes as application/x-www-form-urlencoded instead: a space becomes + and every escape uses lowercase hex (encodeURIComponent("/") is "%2f", not "%2F"). The reserved sets are otherwise correct — encodeURI leaves ; / ? : @ & = + $ , # intact while encodeURIComponent escapes them. Round-tripping through the matching decode function still recovers the original string, so the quirk only matters when the encoded text is compared literally or consumed by an RFC-3986 strict parser.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// space and hex casing
Platform.Response.Write("encodeURI('a b/c?d=1'): " + encodeURI("a b/c?d=1") + "\n"); // a+b/c?d=1
Platform.Response.Write("encodeURIComponent('a b/c?d=1'): " + encodeURIComponent("a b/c?d=1") + "\n"); // a+b%2fc%3fd%3d1
Platform.Response.Write("encodeURIComponent('/'): " + encodeURIComponent("/") + "\n"); // %2f (spec: %2F)
// reserved set of encodeURI is left intact
Platform.Response.Write("encodeURI('/?:@&=+$,#'): " + encodeURI("/?:@&=+$,#") + "\n");
</script>
decodeURI — decodes reserved escapes and + → space; behaves like decodeURIComponent
MDN specifies decodeURI preserves the escape sequences for the URI-syntax characters ; / ? : @ & = + $ , #, leaves a literal + unchanged, and throws a URIError on a malformed escape. The SFMC Jint engine does none of that: decodeURI("%2F") returns "/", decodeURI("a+b") returns "a b", and a truncated escape such as "%E0%A4%A" is returned unchanged instead of throwing. The result is that decodeURI and decodeURIComponent are indistinguishable — never rely on decodeURI to keep a URI’s structure intact.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// reserved escapes are decoded (spec: preserved)
Platform.Response.Write("decodeURI('%2F'): " + decodeURI("%2F") + "\n"); // /
Platform.Response.Write("decodeURI('%3A'): " + decodeURI("%3A") + "\n"); // :
// + becomes a space (spec: stays "+")
Platform.Response.Write("decodeURI('a+b'): " + decodeURI("a+b") + "\n"); // a b
// malformed escape does not throw (spec: URIError)
try { Platform.Response.Write("decodeURI('%E0%A4%A'): " + decodeURI("%E0%A4%A") + "\n"); }
catch (e) { Platform.Response.Write("threw: " + e.message + "\n"); }
</script>
decodeURIComponent — a literal + is decoded to a space
MDN specifies decodeURIComponent only converts %XX escapes and leaves a literal + as a +. The SFMC Jint engine decodes + to a space, matching application/x-www-form-urlencoded. That makes it the exact inverse of the engine’s own encodeURIComponent (which emits + for a space), but it silently corrupts any value that legitimately contains a plus sign — for example a phone number or a base64 payload. Escape such input as %2B before decoding.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
Platform.Response.Write("decodeURIComponent('+'): [" + decodeURIComponent("+") + "]\n"); // a single space
Platform.Response.Write("decodeURIComponent('a+b'): " + decodeURIComponent("a+b") + "\n"); // a b
Platform.Response.Write("decodeURIComponent('%2B'): " + decodeURIComponent("%2B") + "\n"); // + (workaround)
Platform.Response.Write("roundtrip: " + decodeURIComponent(encodeURIComponent("a b/c?d=1")) + "\n");
</script>
Platform.Function.UpdateData — requires arrays for every filter and update name/value argument
The official reference allows scalar strings for a single filter column and value. At runtime, UpdateData accepts only the five-argument array form: whereFieldNames, whereFieldValues, fieldNames, and fieldValues must all be nonempty, positionally aligned arrays. Scalar forms throw; wrap single columns and values in one-element arrays.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
var field = Platform.Function.CreateObject("DataExtensionField");
Platform.Function.SetObjectProperty(field, "Name", name); Platform.Function.SetObjectProperty(field, "FieldType", "Text");
Platform.Function.SetObjectProperty(field, "MaxLength", len); Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey);
Platform.Function.SetObjectProperty(field, "IsRequired", isKey); Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); Platform.Function.SetObjectProperty(de, "Name", name);
addField(de, "Id", "50", "true"); addField(de, "Txt", "50", "false"); var status = [0, 0]; return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); var status = [0, 0]; return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_ud_arrays_2138_name", deKey = "ssjsg_ud_arrays_2138_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("setup: row inserted", Platform.Function.InsertData(deName, ["Id", "Txt"], ["a", "seed"]), 1);
assertThrows("DEV scalar whereFieldNames/whereFieldValues throw (docs: strings accepted)", function () { return Platform.Function.UpdateData(deName, "Id", "a", ["Txt"], ["wrong"]); });
assertThrows("DEV scalar fieldNames/fieldValues throw (docs: arrays required)", function () { return Platform.Function.UpdateData(deName, ["Id"], ["a"], "Txt", "wrong"); });
assert("workaround one-element arrays are accepted", Platform.Function.UpdateData(deName, ["Id"], ["a"], ["Txt"], ["right"]), 1);
assert("workaround array-form update commits", String(Platform.Function.Lookup(deName, "Txt", "Id", "a")), "right");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>
Platform.Load — returns null (not void); an empty/null library name is accepted silently
Two things about Platform.Load differ from the way it is documented:
- It is described as a
void function, but it returns the literal null - typeof is "object" and result === null is true. Never test the result to decide whether a load succeeded; a rejected load throws instead.
libraryName is documented as Required, yet an empty string and null are both accepted silently: the call succeeds, becomes a no-op, and the Core aliases simply never appear. A typo that evaluates to "" or null is swallowed rather than reported. libraryName is also matched case-insensitively ("Core", "CORE").
Version handling is looser than the single documented "1.1.5" too. Accepted: "1", "1.0", "1.1", "1.0.0" and every revision "1.1.0" through "1.1.6". Rejected (throws): "1.1.7" and above, "1.2", "1.3", "0", "2". As an undocumented quirk, 32767 acts as a “newest” sentinel in the minor and revision slots ("1.32767", "1.1.32767" load) but not in the major slot. A rejected load is inert - it throws, changes nothing, and never aborts the page - and loading Core twice in one request is idempotent.
Show test script
<script runat="server">
/* No Platform.Load at the top - Platform.* is available without it, and the
load itself is what is under test. */
function show(id, value) { Platform.Response.Write(id + " -> [" + value + "]\n"); }
// 1. The return value is the literal null, not undefined - despite the docs saying void.
var result = Platform.Load("core", "1.1.5");
show("typeof Platform.Load(...)", typeof result); // OBSERVED: object
show("result === null", result === null); // OBSERVED: true
show("result === undefined", result === undefined); // OBSERVED: false
// 2. libraryName is documented as Required, but "" and null are accepted silently.
try { Platform.Load("", "1.1.5"); show("empty libraryName", "no throw"); }
catch (e1) { show("empty libraryName", "threw: " + e1.message); } // OBSERVED: no throw
try { Platform.Load(null, "1.1.5"); show("null libraryName", "no throw"); }
catch (e2) { show("null libraryName", "threw: " + e2.message); } // OBSERVED: no throw
// ... while an unknown name DOES throw, echoing the parsed version numbers.
try { Platform.Load("bogus", "1.1.5"); show("unknown libraryName", "no throw"); }
catch (e3) { show("unknown libraryName", "threw"); } // OBSERVED: threw
// 3. Version strings other than "1.1.5" are accepted, and 32767 is a "newest" sentinel.
function tryVersion(ver) {
try { Platform.Load("core", ver); return "ok"; } catch (ex) { return "throw"; }
}
show("version \"1\"", tryVersion("1")); // OBSERVED: ok
show("version \"1.1.6\"", tryVersion("1.1.6")); // OBSERVED: ok
show("version \"1.1.7\"", tryVersion("1.1.7")); // OBSERVED: throw
show("version \"1.2\"", tryVersion("1.2")); // OBSERVED: throw
show("version \"1.1.32767\"", tryVersion("1.1.32767")); // OBSERVED: ok (sentinel)
show("version \"32767\"", tryVersion("32767")); // OBSERVED: throw (no sentinel in major slot)
// 4. A rejected load is inert and a repeat load is idempotent - Core still works.
// A bare-name typeof must be evaluated INSIDE a function: at the top level of a
// script block a not-yet-defined name is a parse-time risk that aborts the page.
function typeOf(fn) { try { return fn(); } catch (ex) { return "THREW"; } }
show("Core still usable after the failures", typeOf(function () { return typeof Stringify; })); // OBSERVED: function
</script>
.Remove</code></a> — missing key returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Remove() as returning "OK" or throwing on failure. At runtime a nonexistent list key returns the plain string "Error" and does not throw — callers must check the return value; try/catch alone is not enough. A successful delete still returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
assert("DEV Remove on a nonexistent key returns \"Error\" (docs: throws)", "" + List.Init("ssjs-guide-no-such-list-zzz").Remove(), "Error");
assert("DEV Remove on a nonexistent key does NOT throw (docs: throws)", invocationResult(function () { return List.Init("ssjs-guide-no-such-list-zzz").Remove(); }), "returned");
</script>
</div>
.Subscribers.Add</code></a> — failed Add returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Add() as returning "OK" or throwing on failure. At runtime invalid or incomplete properties return the plain string "Error" and do not throw — callers must check the return value; try/catch alone is not enough.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Add() returns \"Error\" (docs: throws)", "" + list.Subscribers.Add(), "Error");
assert("DEV Add() does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Add(); }), "returned");
</script>
</div>
.Subscribers.Unsubscribe</code></a> — missing subscriber returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Unsubscribe() as returning "OK" or throwing on failure. At runtime a missing subscriber returns the plain string "Error" and does not throw. A successful call sets Status to Unsubscribed but leaves the membership row on the list.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Unsubscribe(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"), "Error");
assert("DEV Unsubscribe(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"); }), "returned");
</script>
</div>
.Subscribers.Update</code></a> — failed Update returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
</div>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
The official docs list properties as a required argument and mention no state requirement. At runtime the argument is effectively optional - Update() with no arguments returns the string "OK" - and there is an undocumented state gate: calling Update on a definition whose TriggeredSendStatus is Active returns the string "Error" with LastMessage An active TriggeredSendDefinition can not be updated or have it's content refreshed and LastErrorCode 17003. Call Pause() first, update, then Start() again. A successful update returns "OK" with LastMessage TriggeredSendDefinition updated. Passing a non-object (for example a string) throws Error Updating TSD. with LastMessage Invalid cast from 'Char' to 'Double'..
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Update() with NO arguments returns "OK". Update on an ACTIVE definition returns "Error"
// with LastMessage "An active TriggeredSendDefinition can not be updated..." / LastErrorCode 17003.
var ts = TriggeredSend.Init("your_tsd_customer_key");
Platform.Response.Write("Pause: " + ts.Pause() + "\n");
Platform.Response.Write("Update(no args): " + ts.Update() + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("Update({Description}): " + ts.Update({ Description: "probe " + Platform.Function.Now() }) + "\n");
Platform.Response.Write("Start: " + ts.Start() + "\n");
Platform.Response.Write("Update while Active: " + ts.Update({ Description: "x" }) + " | " + TriggeredSend.LastMessage + " | " + TriggeredSend.LastErrorCode + "\n");
</script>
.Publish</code></a> — returns "OK" but does not make the definition active</h3>
The official docs describe Publish() as the call that makes a triggered send definition active. At runtime it returns the string "OK" with LastMessage TriggeredSendDefinition updated, but a follow-up WSProxy retrieve showed TriggeredSendStatus still New - only the subsequent Start() moved it to Active. Extra arguments are ignored rather than rejected: Publish("x") also returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Publish() -> "OK" / "TriggeredSendDefinition updated", but the retrieved status is still "New".
// Start() is what sets TriggeredSendStatus "Active".
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("before: " + status() + "\n");
Platform.Response.Write("Publish(): " + ts.Publish() + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("after Publish: " + status() + "\n");
Platform.Response.Write("Start(): " + ts.Start() + "\n");
Platform.Response.Write("after Start: " + status() + "\n");
</script>
</div>
.Send</code></a> — accepts a third subscriberKey argument; works on an Inactive definition; returns "Error" for a bad address</h3>
Runtime-proven with real sends - a successful call returns the string "OK" with LastMessage Created TriggeredSend. Four details differ from the official docs. (1) Arity: the docs document Send(emailAddress, sendTimeAttributes), but a third subscriberKey argument is accepted and still returns "OK"; arguments beyond the third are ignored (a four-argument call also returns "OK"). (2) State: the definition does not have to be Active - a send against an Inactive definition still returned "OK" / Created TriggeredSend. (3) Failure mode: an invalid address does not throw; it returns the string "Error" with LastMessage Unable to queue Triggered Send request. There are no valid subscribers.. (4) Calling Send() with no arguments throws the usage string Usage: Send(EmailAddress [, sendTimeAttributes]). TriggeredSend.LastRequestID was 0 after a successful send.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Send(addr) -> "OK" / "Created TriggeredSend"; a THIRD subscriberKey arg is accepted;
// an Inactive definition still sends; an invalid address returns the STRING "Error" (no throw);
// Send() with no arguments throws "Usage: Send(EmailAddress [, sendTimeAttributes])".
var ts = TriggeredSend.Init("your_tsd_customer_key");
Platform.Response.Write("Send(addr): " + ts.Send("you@example.com") + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("Send(addr, attrs, subKey): " + ts.Send("you@example.com", { Foo: "bar" }, "sub-key") + "\n");
Platform.Response.Write("Send(bad): " + ts.Send("not-an-address") + " | " + TriggeredSend.LastMessage + "\n");
try { ts.Send(); } catch (e) { Platform.Response.Write("Send() THREW -> " + String(e) + "\n"); }
</script>
</div>
encodeURI / encodeURIComponent — space becomes + and hex escapes are lowercase (form-urlencoded, not RFC 3986)
MDN specifies both functions encode a space as %20 and emit uppercase hex digits. The SFMC Jint engine encodes as application/x-www-form-urlencoded instead: a space becomes + and every escape uses lowercase hex (encodeURIComponent("/") is "%2f", not "%2F"). The reserved sets are otherwise correct — encodeURI leaves ; / ? : @ & = + $ , # intact while encodeURIComponent escapes them. Round-tripping through the matching decode function still recovers the original string, so the quirk only matters when the encoded text is compared literally or consumed by an RFC-3986 strict parser.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// space and hex casing
Platform.Response.Write("encodeURI('a b/c?d=1'): " + encodeURI("a b/c?d=1") + "\n"); // a+b/c?d=1
Platform.Response.Write("encodeURIComponent('a b/c?d=1'): " + encodeURIComponent("a b/c?d=1") + "\n"); // a+b%2fc%3fd%3d1
Platform.Response.Write("encodeURIComponent('/'): " + encodeURIComponent("/") + "\n"); // %2f (spec: %2F)
// reserved set of encodeURI is left intact
Platform.Response.Write("encodeURI('/?:@&=+$,#'): " + encodeURI("/?:@&=+$,#") + "\n");
</script>
decodeURI — decodes reserved escapes and + → space; behaves like decodeURIComponent
MDN specifies decodeURI preserves the escape sequences for the URI-syntax characters ; / ? : @ & = + $ , #, leaves a literal + unchanged, and throws a URIError on a malformed escape. The SFMC Jint engine does none of that: decodeURI("%2F") returns "/", decodeURI("a+b") returns "a b", and a truncated escape such as "%E0%A4%A" is returned unchanged instead of throwing. The result is that decodeURI and decodeURIComponent are indistinguishable — never rely on decodeURI to keep a URI’s structure intact.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// reserved escapes are decoded (spec: preserved)
Platform.Response.Write("decodeURI('%2F'): " + decodeURI("%2F") + "\n"); // /
Platform.Response.Write("decodeURI('%3A'): " + decodeURI("%3A") + "\n"); // :
// + becomes a space (spec: stays "+")
Platform.Response.Write("decodeURI('a+b'): " + decodeURI("a+b") + "\n"); // a b
// malformed escape does not throw (spec: URIError)
try { Platform.Response.Write("decodeURI('%E0%A4%A'): " + decodeURI("%E0%A4%A") + "\n"); }
catch (e) { Platform.Response.Write("threw: " + e.message + "\n"); }
</script>
decodeURIComponent — a literal + is decoded to a space
MDN specifies decodeURIComponent only converts %XX escapes and leaves a literal + as a +. The SFMC Jint engine decodes + to a space, matching application/x-www-form-urlencoded. That makes it the exact inverse of the engine’s own encodeURIComponent (which emits + for a space), but it silently corrupts any value that legitimately contains a plus sign — for example a phone number or a base64 payload. Escape such input as %2B before decoding.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
Platform.Response.Write("decodeURIComponent('+'): [" + decodeURIComponent("+") + "]\n"); // a single space
Platform.Response.Write("decodeURIComponent('a+b'): " + decodeURIComponent("a+b") + "\n"); // a b
Platform.Response.Write("decodeURIComponent('%2B'): " + decodeURIComponent("%2B") + "\n"); // + (workaround)
Platform.Response.Write("roundtrip: " + decodeURIComponent(encodeURIComponent("a b/c?d=1")) + "\n");
</script>
Platform.Function.UpdateData — requires arrays for every filter and update name/value argument
The official reference allows scalar strings for a single filter column and value. At runtime, UpdateData accepts only the five-argument array form: whereFieldNames, whereFieldValues, fieldNames, and fieldValues must all be nonempty, positionally aligned arrays. Scalar forms throw; wrap single columns and values in one-element arrays.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
var field = Platform.Function.CreateObject("DataExtensionField");
Platform.Function.SetObjectProperty(field, "Name", name); Platform.Function.SetObjectProperty(field, "FieldType", "Text");
Platform.Function.SetObjectProperty(field, "MaxLength", len); Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey);
Platform.Function.SetObjectProperty(field, "IsRequired", isKey); Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); Platform.Function.SetObjectProperty(de, "Name", name);
addField(de, "Id", "50", "true"); addField(de, "Txt", "50", "false"); var status = [0, 0]; return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); var status = [0, 0]; return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_ud_arrays_2138_name", deKey = "ssjsg_ud_arrays_2138_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("setup: row inserted", Platform.Function.InsertData(deName, ["Id", "Txt"], ["a", "seed"]), 1);
assertThrows("DEV scalar whereFieldNames/whereFieldValues throw (docs: strings accepted)", function () { return Platform.Function.UpdateData(deName, "Id", "a", ["Txt"], ["wrong"]); });
assertThrows("DEV scalar fieldNames/fieldValues throw (docs: arrays required)", function () { return Platform.Function.UpdateData(deName, ["Id"], ["a"], "Txt", "wrong"); });
assert("workaround one-element arrays are accepted", Platform.Function.UpdateData(deName, ["Id"], ["a"], ["Txt"], ["right"]), 1);
assert("workaround array-form update commits", String(Platform.Function.Lookup(deName, "Txt", "Id", "a")), "right");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>
Platform.Load — returns null (not void); an empty/null library name is accepted silently
Two things about Platform.Load differ from the way it is documented:
- It is described as a
void function, but it returns the literal null - typeof is "object" and result === null is true. Never test the result to decide whether a load succeeded; a rejected load throws instead.
libraryName is documented as Required, yet an empty string and null are both accepted silently: the call succeeds, becomes a no-op, and the Core aliases simply never appear. A typo that evaluates to "" or null is swallowed rather than reported. libraryName is also matched case-insensitively ("Core", "CORE").
Version handling is looser than the single documented "1.1.5" too. Accepted: "1", "1.0", "1.1", "1.0.0" and every revision "1.1.0" through "1.1.6". Rejected (throws): "1.1.7" and above, "1.2", "1.3", "0", "2". As an undocumented quirk, 32767 acts as a “newest” sentinel in the minor and revision slots ("1.32767", "1.1.32767" load) but not in the major slot. A rejected load is inert - it throws, changes nothing, and never aborts the page - and loading Core twice in one request is idempotent.
Show test script
<script runat="server">
/* No Platform.Load at the top - Platform.* is available without it, and the
load itself is what is under test. */
function show(id, value) { Platform.Response.Write(id + " -> [" + value + "]\n"); }
// 1. The return value is the literal null, not undefined - despite the docs saying void.
var result = Platform.Load("core", "1.1.5");
show("typeof Platform.Load(...)", typeof result); // OBSERVED: object
show("result === null", result === null); // OBSERVED: true
show("result === undefined", result === undefined); // OBSERVED: false
// 2. libraryName is documented as Required, but "" and null are accepted silently.
try { Platform.Load("", "1.1.5"); show("empty libraryName", "no throw"); }
catch (e1) { show("empty libraryName", "threw: " + e1.message); } // OBSERVED: no throw
try { Platform.Load(null, "1.1.5"); show("null libraryName", "no throw"); }
catch (e2) { show("null libraryName", "threw: " + e2.message); } // OBSERVED: no throw
// ... while an unknown name DOES throw, echoing the parsed version numbers.
try { Platform.Load("bogus", "1.1.5"); show("unknown libraryName", "no throw"); }
catch (e3) { show("unknown libraryName", "threw"); } // OBSERVED: threw
// 3. Version strings other than "1.1.5" are accepted, and 32767 is a "newest" sentinel.
function tryVersion(ver) {
try { Platform.Load("core", ver); return "ok"; } catch (ex) { return "throw"; }
}
show("version \"1\"", tryVersion("1")); // OBSERVED: ok
show("version \"1.1.6\"", tryVersion("1.1.6")); // OBSERVED: ok
show("version \"1.1.7\"", tryVersion("1.1.7")); // OBSERVED: throw
show("version \"1.2\"", tryVersion("1.2")); // OBSERVED: throw
show("version \"1.1.32767\"", tryVersion("1.1.32767")); // OBSERVED: ok (sentinel)
show("version \"32767\"", tryVersion("32767")); // OBSERVED: throw (no sentinel in major slot)
// 4. A rejected load is inert and a repeat load is idempotent - Core still works.
// A bare-name typeof must be evaluated INSIDE a function: at the top level of a
// script block a not-yet-defined name is a parse-time risk that aborts the page.
function typeOf(fn) { try { return fn(); } catch (ex) { return "THREW"; } }
show("Core still usable after the failures", typeOf(function () { return typeof Stringify; })); // OBSERVED: function
</script>
.Remove</code></a> — missing key returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Remove() as returning "OK" or throwing on failure. At runtime a nonexistent list key returns the plain string "Error" and does not throw — callers must check the return value; try/catch alone is not enough. A successful delete still returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
assert("DEV Remove on a nonexistent key returns \"Error\" (docs: throws)", "" + List.Init("ssjs-guide-no-such-list-zzz").Remove(), "Error");
assert("DEV Remove on a nonexistent key does NOT throw (docs: throws)", invocationResult(function () { return List.Init("ssjs-guide-no-such-list-zzz").Remove(); }), "returned");
</script>
</div>
.Subscribers.Add</code></a> — failed Add returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Add() as returning "OK" or throwing on failure. At runtime invalid or incomplete properties return the plain string "Error" and do not throw — callers must check the return value; try/catch alone is not enough.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Add() returns \"Error\" (docs: throws)", "" + list.Subscribers.Add(), "Error");
assert("DEV Add() does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Add(); }), "returned");
</script>
</div>
.Subscribers.Unsubscribe</code></a> — missing subscriber returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Unsubscribe() as returning "OK" or throwing on failure. At runtime a missing subscriber returns the plain string "Error" and does not throw. A successful call sets Status to Unsubscribed but leaves the membership row on the list.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Unsubscribe(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"), "Error");
assert("DEV Unsubscribe(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"); }), "returned");
</script>
</div>
.Subscribers.Update</code></a> — failed Update returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
</div>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
The official docs describe Publish() as the call that makes a triggered send definition active. At runtime it returns the string "OK" with LastMessage TriggeredSendDefinition updated, but a follow-up WSProxy retrieve showed TriggeredSendStatus still New - only the subsequent Start() moved it to Active. Extra arguments are ignored rather than rejected: Publish("x") also returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Publish() -> "OK" / "TriggeredSendDefinition updated", but the retrieved status is still "New".
// Start() is what sets TriggeredSendStatus "Active".
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("before: " + status() + "\n");
Platform.Response.Write("Publish(): " + ts.Publish() + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("after Publish: " + status() + "\n");
Platform.Response.Write("Start(): " + ts.Start() + "\n");
Platform.Response.Write("after Start: " + status() + "\n");
</script>
.Send</code></a> — accepts a third subscriberKey argument; works on an Inactive definition; returns "Error" for a bad address</h3>
Runtime-proven with real sends - a successful call returns the string "OK" with LastMessage Created TriggeredSend. Four details differ from the official docs. (1) Arity: the docs document Send(emailAddress, sendTimeAttributes), but a third subscriberKey argument is accepted and still returns "OK"; arguments beyond the third are ignored (a four-argument call also returns "OK"). (2) State: the definition does not have to be Active - a send against an Inactive definition still returned "OK" / Created TriggeredSend. (3) Failure mode: an invalid address does not throw; it returns the string "Error" with LastMessage Unable to queue Triggered Send request. There are no valid subscribers.. (4) Calling Send() with no arguments throws the usage string Usage: Send(EmailAddress [, sendTimeAttributes]). TriggeredSend.LastRequestID was 0 after a successful send.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Send(addr) -> "OK" / "Created TriggeredSend"; a THIRD subscriberKey arg is accepted;
// an Inactive definition still sends; an invalid address returns the STRING "Error" (no throw);
// Send() with no arguments throws "Usage: Send(EmailAddress [, sendTimeAttributes])".
var ts = TriggeredSend.Init("your_tsd_customer_key");
Platform.Response.Write("Send(addr): " + ts.Send("you@example.com") + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("Send(addr, attrs, subKey): " + ts.Send("you@example.com", { Foo: "bar" }, "sub-key") + "\n");
Platform.Response.Write("Send(bad): " + ts.Send("not-an-address") + " | " + TriggeredSend.LastMessage + "\n");
try { ts.Send(); } catch (e) { Platform.Response.Write("Send() THREW -> " + String(e) + "\n"); }
</script>
</div>
encodeURI / encodeURIComponent — space becomes + and hex escapes are lowercase (form-urlencoded, not RFC 3986)
MDN specifies both functions encode a space as %20 and emit uppercase hex digits. The SFMC Jint engine encodes as application/x-www-form-urlencoded instead: a space becomes + and every escape uses lowercase hex (encodeURIComponent("/") is "%2f", not "%2F"). The reserved sets are otherwise correct — encodeURI leaves ; / ? : @ & = + $ , # intact while encodeURIComponent escapes them. Round-tripping through the matching decode function still recovers the original string, so the quirk only matters when the encoded text is compared literally or consumed by an RFC-3986 strict parser.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// space and hex casing
Platform.Response.Write("encodeURI('a b/c?d=1'): " + encodeURI("a b/c?d=1") + "\n"); // a+b/c?d=1
Platform.Response.Write("encodeURIComponent('a b/c?d=1'): " + encodeURIComponent("a b/c?d=1") + "\n"); // a+b%2fc%3fd%3d1
Platform.Response.Write("encodeURIComponent('/'): " + encodeURIComponent("/") + "\n"); // %2f (spec: %2F)
// reserved set of encodeURI is left intact
Platform.Response.Write("encodeURI('/?:@&=+$,#'): " + encodeURI("/?:@&=+$,#") + "\n");
</script>
decodeURI — decodes reserved escapes and + → space; behaves like decodeURIComponent
MDN specifies decodeURI preserves the escape sequences for the URI-syntax characters ; / ? : @ & = + $ , #, leaves a literal + unchanged, and throws a URIError on a malformed escape. The SFMC Jint engine does none of that: decodeURI("%2F") returns "/", decodeURI("a+b") returns "a b", and a truncated escape such as "%E0%A4%A" is returned unchanged instead of throwing. The result is that decodeURI and decodeURIComponent are indistinguishable — never rely on decodeURI to keep a URI’s structure intact.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// reserved escapes are decoded (spec: preserved)
Platform.Response.Write("decodeURI('%2F'): " + decodeURI("%2F") + "\n"); // /
Platform.Response.Write("decodeURI('%3A'): " + decodeURI("%3A") + "\n"); // :
// + becomes a space (spec: stays "+")
Platform.Response.Write("decodeURI('a+b'): " + decodeURI("a+b") + "\n"); // a b
// malformed escape does not throw (spec: URIError)
try { Platform.Response.Write("decodeURI('%E0%A4%A'): " + decodeURI("%E0%A4%A") + "\n"); }
catch (e) { Platform.Response.Write("threw: " + e.message + "\n"); }
</script>
decodeURIComponent — a literal + is decoded to a space
MDN specifies decodeURIComponent only converts %XX escapes and leaves a literal + as a +. The SFMC Jint engine decodes + to a space, matching application/x-www-form-urlencoded. That makes it the exact inverse of the engine’s own encodeURIComponent (which emits + for a space), but it silently corrupts any value that legitimately contains a plus sign — for example a phone number or a base64 payload. Escape such input as %2B before decoding.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
Platform.Response.Write("decodeURIComponent('+'): [" + decodeURIComponent("+") + "]\n"); // a single space
Platform.Response.Write("decodeURIComponent('a+b'): " + decodeURIComponent("a+b") + "\n"); // a b
Platform.Response.Write("decodeURIComponent('%2B'): " + decodeURIComponent("%2B") + "\n"); // + (workaround)
Platform.Response.Write("roundtrip: " + decodeURIComponent(encodeURIComponent("a b/c?d=1")) + "\n");
</script>
Platform.Function.UpdateData — requires arrays for every filter and update name/value argument
The official reference allows scalar strings for a single filter column and value. At runtime, UpdateData accepts only the five-argument array form: whereFieldNames, whereFieldValues, fieldNames, and fieldValues must all be nonempty, positionally aligned arrays. Scalar forms throw; wrap single columns and values in one-element arrays.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
var field = Platform.Function.CreateObject("DataExtensionField");
Platform.Function.SetObjectProperty(field, "Name", name); Platform.Function.SetObjectProperty(field, "FieldType", "Text");
Platform.Function.SetObjectProperty(field, "MaxLength", len); Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey);
Platform.Function.SetObjectProperty(field, "IsRequired", isKey); Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); Platform.Function.SetObjectProperty(de, "Name", name);
addField(de, "Id", "50", "true"); addField(de, "Txt", "50", "false"); var status = [0, 0]; return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); var status = [0, 0]; return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_ud_arrays_2138_name", deKey = "ssjsg_ud_arrays_2138_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("setup: row inserted", Platform.Function.InsertData(deName, ["Id", "Txt"], ["a", "seed"]), 1);
assertThrows("DEV scalar whereFieldNames/whereFieldValues throw (docs: strings accepted)", function () { return Platform.Function.UpdateData(deName, "Id", "a", ["Txt"], ["wrong"]); });
assertThrows("DEV scalar fieldNames/fieldValues throw (docs: arrays required)", function () { return Platform.Function.UpdateData(deName, ["Id"], ["a"], "Txt", "wrong"); });
assert("workaround one-element arrays are accepted", Platform.Function.UpdateData(deName, ["Id"], ["a"], ["Txt"], ["right"]), 1);
assert("workaround array-form update commits", String(Platform.Function.Lookup(deName, "Txt", "Id", "a")), "right");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>
Platform.Load — returns null (not void); an empty/null library name is accepted silently
Two things about Platform.Load differ from the way it is documented:
- It is described as a
void function, but it returns the literal null - typeof is "object" and result === null is true. Never test the result to decide whether a load succeeded; a rejected load throws instead.
libraryName is documented as Required, yet an empty string and null are both accepted silently: the call succeeds, becomes a no-op, and the Core aliases simply never appear. A typo that evaluates to "" or null is swallowed rather than reported. libraryName is also matched case-insensitively ("Core", "CORE").
Version handling is looser than the single documented "1.1.5" too. Accepted: "1", "1.0", "1.1", "1.0.0" and every revision "1.1.0" through "1.1.6". Rejected (throws): "1.1.7" and above, "1.2", "1.3", "0", "2". As an undocumented quirk, 32767 acts as a “newest” sentinel in the minor and revision slots ("1.32767", "1.1.32767" load) but not in the major slot. A rejected load is inert - it throws, changes nothing, and never aborts the page - and loading Core twice in one request is idempotent.
Show test script
<script runat="server">
/* No Platform.Load at the top - Platform.* is available without it, and the
load itself is what is under test. */
function show(id, value) { Platform.Response.Write(id + " -> [" + value + "]\n"); }
// 1. The return value is the literal null, not undefined - despite the docs saying void.
var result = Platform.Load("core", "1.1.5");
show("typeof Platform.Load(...)", typeof result); // OBSERVED: object
show("result === null", result === null); // OBSERVED: true
show("result === undefined", result === undefined); // OBSERVED: false
// 2. libraryName is documented as Required, but "" and null are accepted silently.
try { Platform.Load("", "1.1.5"); show("empty libraryName", "no throw"); }
catch (e1) { show("empty libraryName", "threw: " + e1.message); } // OBSERVED: no throw
try { Platform.Load(null, "1.1.5"); show("null libraryName", "no throw"); }
catch (e2) { show("null libraryName", "threw: " + e2.message); } // OBSERVED: no throw
// ... while an unknown name DOES throw, echoing the parsed version numbers.
try { Platform.Load("bogus", "1.1.5"); show("unknown libraryName", "no throw"); }
catch (e3) { show("unknown libraryName", "threw"); } // OBSERVED: threw
// 3. Version strings other than "1.1.5" are accepted, and 32767 is a "newest" sentinel.
function tryVersion(ver) {
try { Platform.Load("core", ver); return "ok"; } catch (ex) { return "throw"; }
}
show("version \"1\"", tryVersion("1")); // OBSERVED: ok
show("version \"1.1.6\"", tryVersion("1.1.6")); // OBSERVED: ok
show("version \"1.1.7\"", tryVersion("1.1.7")); // OBSERVED: throw
show("version \"1.2\"", tryVersion("1.2")); // OBSERVED: throw
show("version \"1.1.32767\"", tryVersion("1.1.32767")); // OBSERVED: ok (sentinel)
show("version \"32767\"", tryVersion("32767")); // OBSERVED: throw (no sentinel in major slot)
// 4. A rejected load is inert and a repeat load is idempotent - Core still works.
// A bare-name typeof must be evaluated INSIDE a function: at the top level of a
// script block a not-yet-defined name is a parse-time risk that aborts the page.
function typeOf(fn) { try { return fn(); } catch (ex) { return "THREW"; } }
show("Core still usable after the failures", typeOf(function () { return typeof Stringify; })); // OBSERVED: function
</script>
.Remove</code></a> — missing key returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Remove() as returning "OK" or throwing on failure. At runtime a nonexistent list key returns the plain string "Error" and does not throw — callers must check the return value; try/catch alone is not enough. A successful delete still returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
assert("DEV Remove on a nonexistent key returns \"Error\" (docs: throws)", "" + List.Init("ssjs-guide-no-such-list-zzz").Remove(), "Error");
assert("DEV Remove on a nonexistent key does NOT throw (docs: throws)", invocationResult(function () { return List.Init("ssjs-guide-no-such-list-zzz").Remove(); }), "returned");
</script>
</div>
.Subscribers.Add</code></a> — failed Add returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Add() as returning "OK" or throwing on failure. At runtime invalid or incomplete properties return the plain string "Error" and do not throw — callers must check the return value; try/catch alone is not enough.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Add() returns \"Error\" (docs: throws)", "" + list.Subscribers.Add(), "Error");
assert("DEV Add() does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Add(); }), "returned");
</script>
</div>
.Subscribers.Unsubscribe</code></a> — missing subscriber returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Unsubscribe() as returning "OK" or throwing on failure. At runtime a missing subscriber returns the plain string "Error" and does not throw. A successful call sets Status to Unsubscribed but leaves the membership row on the list.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Unsubscribe(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"), "Error");
assert("DEV Unsubscribe(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"); }), "returned");
</script>
</div>
.Subscribers.Update</code></a> — failed Update returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
</div>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
Runtime-proven with real sends - a successful call returns the string "OK" with LastMessage Created TriggeredSend. Four details differ from the official docs. (1) Arity: the docs document Send(emailAddress, sendTimeAttributes), but a third subscriberKey argument is accepted and still returns "OK"; arguments beyond the third are ignored (a four-argument call also returns "OK"). (2) State: the definition does not have to be Active - a send against an Inactive definition still returned "OK" / Created TriggeredSend. (3) Failure mode: an invalid address does not throw; it returns the string "Error" with LastMessage Unable to queue Triggered Send request. There are no valid subscribers.. (4) Calling Send() with no arguments throws the usage string Usage: Send(EmailAddress [, sendTimeAttributes]). TriggeredSend.LastRequestID was 0 after a successful send.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Send(addr) -> "OK" / "Created TriggeredSend"; a THIRD subscriberKey arg is accepted;
// an Inactive definition still sends; an invalid address returns the STRING "Error" (no throw);
// Send() with no arguments throws "Usage: Send(EmailAddress [, sendTimeAttributes])".
var ts = TriggeredSend.Init("your_tsd_customer_key");
Platform.Response.Write("Send(addr): " + ts.Send("you@example.com") + " | " + TriggeredSend.LastMessage + "\n");
Platform.Response.Write("Send(addr, attrs, subKey): " + ts.Send("you@example.com", { Foo: "bar" }, "sub-key") + "\n");
Platform.Response.Write("Send(bad): " + ts.Send("not-an-address") + " | " + TriggeredSend.LastMessage + "\n");
try { ts.Send(); } catch (e) { Platform.Response.Write("Send() THREW -> " + String(e) + "\n"); }
</script>
encodeURI / encodeURIComponent — space becomes + and hex escapes are lowercase (form-urlencoded, not RFC 3986)
MDN specifies both functions encode a space as %20 and emit uppercase hex digits. The SFMC Jint engine encodes as application/x-www-form-urlencoded instead: a space becomes + and every escape uses lowercase hex (encodeURIComponent("/") is "%2f", not "%2F"). The reserved sets are otherwise correct — encodeURI leaves ; / ? : @ & = + $ , # intact while encodeURIComponent escapes them. Round-tripping through the matching decode function still recovers the original string, so the quirk only matters when the encoded text is compared literally or consumed by an RFC-3986 strict parser.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// space and hex casing
Platform.Response.Write("encodeURI('a b/c?d=1'): " + encodeURI("a b/c?d=1") + "\n"); // a+b/c?d=1
Platform.Response.Write("encodeURIComponent('a b/c?d=1'): " + encodeURIComponent("a b/c?d=1") + "\n"); // a+b%2fc%3fd%3d1
Platform.Response.Write("encodeURIComponent('/'): " + encodeURIComponent("/") + "\n"); // %2f (spec: %2F)
// reserved set of encodeURI is left intact
Platform.Response.Write("encodeURI('/?:@&=+$,#'): " + encodeURI("/?:@&=+$,#") + "\n");
</script>
decodeURI — decodes reserved escapes and + → space; behaves like decodeURIComponent
MDN specifies decodeURI preserves the escape sequences for the URI-syntax characters ; / ? : @ & = + $ , #, leaves a literal + unchanged, and throws a URIError on a malformed escape. The SFMC Jint engine does none of that: decodeURI("%2F") returns "/", decodeURI("a+b") returns "a b", and a truncated escape such as "%E0%A4%A" is returned unchanged instead of throwing. The result is that decodeURI and decodeURIComponent are indistinguishable — never rely on decodeURI to keep a URI’s structure intact.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// reserved escapes are decoded (spec: preserved)
Platform.Response.Write("decodeURI('%2F'): " + decodeURI("%2F") + "\n"); // /
Platform.Response.Write("decodeURI('%3A'): " + decodeURI("%3A") + "\n"); // :
// + becomes a space (spec: stays "+")
Platform.Response.Write("decodeURI('a+b'): " + decodeURI("a+b") + "\n"); // a b
// malformed escape does not throw (spec: URIError)
try { Platform.Response.Write("decodeURI('%E0%A4%A'): " + decodeURI("%E0%A4%A") + "\n"); }
catch (e) { Platform.Response.Write("threw: " + e.message + "\n"); }
</script>
decodeURIComponent — a literal + is decoded to a space
MDN specifies decodeURIComponent only converts %XX escapes and leaves a literal + as a +. The SFMC Jint engine decodes + to a space, matching application/x-www-form-urlencoded. That makes it the exact inverse of the engine’s own encodeURIComponent (which emits + for a space), but it silently corrupts any value that legitimately contains a plus sign — for example a phone number or a base64 payload. Escape such input as %2B before decoding.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
Platform.Response.Write("decodeURIComponent('+'): [" + decodeURIComponent("+") + "]\n"); // a single space
Platform.Response.Write("decodeURIComponent('a+b'): " + decodeURIComponent("a+b") + "\n"); // a b
Platform.Response.Write("decodeURIComponent('%2B'): " + decodeURIComponent("%2B") + "\n"); // + (workaround)
Platform.Response.Write("roundtrip: " + decodeURIComponent(encodeURIComponent("a b/c?d=1")) + "\n");
</script>
Platform.Function.UpdateData — requires arrays for every filter and update name/value argument
The official reference allows scalar strings for a single filter column and value. At runtime, UpdateData accepts only the five-argument array form: whereFieldNames, whereFieldValues, fieldNames, and fieldValues must all be nonempty, positionally aligned arrays. Scalar forms throw; wrap single columns and values in one-element arrays.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
var threw = false, msg = "";
try { fn(); } catch (ex) { threw = true; msg = ex.message; }
Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
var field = Platform.Function.CreateObject("DataExtensionField");
Platform.Function.SetObjectProperty(field, "Name", name); Platform.Function.SetObjectProperty(field, "FieldType", "Text");
Platform.Function.SetObjectProperty(field, "MaxLength", len); Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey);
Platform.Function.SetObjectProperty(field, "IsRequired", isKey); Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); Platform.Function.SetObjectProperty(de, "Name", name);
addField(de, "Id", "50", "true"); addField(de, "Txt", "50", "false"); var status = [0, 0]; return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
var de = Platform.Function.CreateObject("DataExtension"); Platform.Function.SetObjectProperty(de, "CustomerKey", key); var status = [0, 0]; return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_ud_arrays_2138_name", deKey = "ssjsg_ud_arrays_2138_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("setup: row inserted", Platform.Function.InsertData(deName, ["Id", "Txt"], ["a", "seed"]), 1);
assertThrows("DEV scalar whereFieldNames/whereFieldValues throw (docs: strings accepted)", function () { return Platform.Function.UpdateData(deName, "Id", "a", ["Txt"], ["wrong"]); });
assertThrows("DEV scalar fieldNames/fieldValues throw (docs: arrays required)", function () { return Platform.Function.UpdateData(deName, ["Id"], ["a"], "Txt", "wrong"); });
assert("workaround one-element arrays are accepted", Platform.Function.UpdateData(deName, ["Id"], ["a"], ["Txt"], ["right"]), 1);
assert("workaround array-form update commits", String(Platform.Function.Lookup(deName, "Txt", "Id", "a")), "right");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>
Platform.Load — returns null (not void); an empty/null library name is accepted silently
Two things about Platform.Load differ from the way it is documented:
- It is described as a
voidfunction, but it returns the literalnull-typeofis"object"andresult === nullistrue. Never test the result to decide whether a load succeeded; a rejected load throws instead. libraryNameis documented as Required, yet an empty string andnullare both accepted silently: the call succeeds, becomes a no-op, and the Core aliases simply never appear. A typo that evaluates to""ornullis swallowed rather than reported.libraryNameis also matched case-insensitively ("Core","CORE").
Version handling is looser than the single documented "1.1.5" too. Accepted: "1", "1.0", "1.1", "1.0.0" and every revision "1.1.0" through "1.1.6". Rejected (throws): "1.1.7" and above, "1.2", "1.3", "0", "2". As an undocumented quirk, 32767 acts as a “newest” sentinel in the minor and revision slots ("1.32767", "1.1.32767" load) but not in the major slot. A rejected load is inert - it throws, changes nothing, and never aborts the page - and loading Core twice in one request is idempotent.
Show test script
<script runat="server">
/* No Platform.Load at the top - Platform.* is available without it, and the
load itself is what is under test. */
function show(id, value) { Platform.Response.Write(id + " -> [" + value + "]\n"); }
// 1. The return value is the literal null, not undefined - despite the docs saying void.
var result = Platform.Load("core", "1.1.5");
show("typeof Platform.Load(...)", typeof result); // OBSERVED: object
show("result === null", result === null); // OBSERVED: true
show("result === undefined", result === undefined); // OBSERVED: false
// 2. libraryName is documented as Required, but "" and null are accepted silently.
try { Platform.Load("", "1.1.5"); show("empty libraryName", "no throw"); }
catch (e1) { show("empty libraryName", "threw: " + e1.message); } // OBSERVED: no throw
try { Platform.Load(null, "1.1.5"); show("null libraryName", "no throw"); }
catch (e2) { show("null libraryName", "threw: " + e2.message); } // OBSERVED: no throw
// ... while an unknown name DOES throw, echoing the parsed version numbers.
try { Platform.Load("bogus", "1.1.5"); show("unknown libraryName", "no throw"); }
catch (e3) { show("unknown libraryName", "threw"); } // OBSERVED: threw
// 3. Version strings other than "1.1.5" are accepted, and 32767 is a "newest" sentinel.
function tryVersion(ver) {
try { Platform.Load("core", ver); return "ok"; } catch (ex) { return "throw"; }
}
show("version \"1\"", tryVersion("1")); // OBSERVED: ok
show("version \"1.1.6\"", tryVersion("1.1.6")); // OBSERVED: ok
show("version \"1.1.7\"", tryVersion("1.1.7")); // OBSERVED: throw
show("version \"1.2\"", tryVersion("1.2")); // OBSERVED: throw
show("version \"1.1.32767\"", tryVersion("1.1.32767")); // OBSERVED: ok (sentinel)
show("version \"32767\"", tryVersion("32767")); // OBSERVED: throw (no sentinel in major slot)
// 4. A rejected load is inert and a repeat load is idempotent - Core still works.
// A bare-name typeof must be evaluated INSIDE a function: at the top level of a
// script block a not-yet-defined name is a parse-time risk that aborts the page.
function typeOf(fn) { try { return fn(); } catch (ex) { return "THREW"; } }
show("Core still usable after the failures", typeOf(function () { return typeof Stringify; })); // OBSERVED: function
</script>
.Remove</code></a> — missing key returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Remove() as returning "OK" or throwing on failure. At runtime a nonexistent list key returns the plain string "Error" and does not throw — callers must check the return value; try/catch alone is not enough. A successful delete still returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
assert("DEV Remove on a nonexistent key returns \"Error\" (docs: throws)", "" + List.Init("ssjs-guide-no-such-list-zzz").Remove(), "Error");
assert("DEV Remove on a nonexistent key does NOT throw (docs: throws)", invocationResult(function () { return List.Init("ssjs-guide-no-such-list-zzz").Remove(); }), "returned");
</script>
</div>
.Subscribers.Add</code></a> — failed Add returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Add() as returning "OK" or throwing on failure. At runtime invalid or incomplete properties return the plain string "Error" and do not throw — callers must check the return value; try/catch alone is not enough.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Add() returns \"Error\" (docs: throws)", "" + list.Subscribers.Add(), "Error");
assert("DEV Add() does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Add(); }), "returned");
</script>
</div>
.Subscribers.Unsubscribe</code></a> — missing subscriber returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Unsubscribe() as returning "OK" or throwing on failure. At runtime a missing subscriber returns the plain string "Error" and does not throw. A successful call sets Status to Unsubscribed but leaves the membership row on the list.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Unsubscribe(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"), "Error");
assert("DEV Unsubscribe(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"); }), "returned");
</script>
</div>
.Subscribers.Update</code></a> — failed Update returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
</div>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
The official docs annotate <ListInstance>.Remove() as returning "OK" or throwing on failure. At runtime a nonexistent list key returns the plain string "Error" and does not throw — callers must check the return value; try/catch alone is not enough. A successful delete still returns "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
assert("DEV Remove on a nonexistent key returns \"Error\" (docs: throws)", "" + List.Init("ssjs-guide-no-such-list-zzz").Remove(), "Error");
assert("DEV Remove on a nonexistent key does NOT throw (docs: throws)", invocationResult(function () { return List.Init("ssjs-guide-no-such-list-zzz").Remove(); }), "returned");
</script>
.Subscribers.Add</code></a> — failed Add returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Add() as returning "OK" or throwing on failure. At runtime invalid or incomplete properties return the plain string "Error" and do not throw — callers must check the return value; try/catch alone is not enough.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Add() returns \"Error\" (docs: throws)", "" + list.Subscribers.Add(), "Error");
assert("DEV Add() does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Add(); }), "returned");
</script>
</div>
.Subscribers.Unsubscribe</code></a> — missing subscriber returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Unsubscribe() as returning "OK" or throwing on failure. At runtime a missing subscriber returns the plain string "Error" and does not throw. A successful call sets Status to Unsubscribed but leaves the membership row on the list.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Unsubscribe(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"), "Error");
assert("DEV Unsubscribe(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"); }), "returned");
</script>
</div>
.Subscribers.Update</code></a> — failed Update returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
</div>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
The official docs annotate <ListInstance>.Subscribers.Add() as returning "OK" or throwing on failure. At runtime invalid or incomplete properties return the plain string "Error" and do not throw — callers must check the return value; try/catch alone is not enough.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Add() returns \"Error\" (docs: throws)", "" + list.Subscribers.Add(), "Error");
assert("DEV Add() does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Add(); }), "returned");
</script>
.Subscribers.Unsubscribe</code></a> — missing subscriber returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Unsubscribe() as returning "OK" or throwing on failure. At runtime a missing subscriber returns the plain string "Error" and does not throw. A successful call sets Status to Unsubscribed but leaves the membership row on the list.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Unsubscribe(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"), "Error");
assert("DEV Unsubscribe(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"); }), "returned");
</script>
</div>
.Subscribers.Update</code></a> — failed Update returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
</div>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
The official docs annotate <ListInstance>.Subscribers.Unsubscribe() as returning "OK" or throwing on failure. At runtime a missing subscriber returns the plain string "Error" and does not throw. A successful call sets Status to Unsubscribed but leaves the membership row on the list.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Unsubscribe(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"), "Error");
assert("DEV Unsubscribe(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Unsubscribe("no-such-ssjs-guide-ts@gmail.com"); }), "returned");
</script>
.Subscribers.Update</code></a> — failed Update returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
</div>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
The official docs annotate <ListInstance>.Subscribers.Update() as returning "OK" or throwing on failure. At runtime a missing subscriber (or an unresolved bare email when SubscriberKey differs from EmailAddress) returns the plain string "Error" and does not throw.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
assert("DEV Update(missing) returns \"Error\" (docs: throws)", "" + list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"), "Error");
assert("DEV Update(missing) does NOT throw (docs: throws)", invocationResult(function () { return list.Subscribers.Update("no-such-ssjs-guide-ts@gmail.com", "Active"); }), "returned");
</script>
.Subscribers.Upsert</code></a> — failed Upsert returns "Error", does not throw</h3>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
</div>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
The official docs annotate <ListInstance>.Subscribers.Upsert() as returning "OK" or throwing on failure. At runtime invalid calls return the plain string "Error" and do not throw — callers must check the return value.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var list = List.Init("ssjs-guide-no-such-list-zzz");
var noArg = null;
assert("DEV Upsert() does NOT throw (docs: throws)", invocationResult(function () { noArg = list.Subscribers.Upsert(); }), "returned");
assert("DEV Upsert() returns \"Error\" (docs: throws)", "" + noArg, "Error");
</script>
Platform.Function.Base64Encode — standard interoperable Base64
The official docs state the output “can only be decoded by the matching Base64Decode() function.” Runtime testing shows the output is standard, interoperable Base64 — any Base64 decoder (in any language) can decode it. There is nothing proprietary about the encoding.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Encode produces STANDARD, interoperable Base64 (nothing proprietary).
try {
var input = "Hello, SFMC!";
var encoded = Platform.Function.Base64Encode(input);
// OBSERVED: encoded === "SGVsbG8sIFNGTUMh" (match: true) - standard Base64, decodable anywhere.
Platform.Response.Write("input: " + input + "\n");
Platform.Response.Write("encoded: " + encoded + "\n");
Platform.Response.Write("EXPECTED standard Base64: SGVsbG8sIFNGTUMh\n");
Platform.Response.Write("match: " + (encoded === "SGVsbG8sIFNGTUMh") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.Base64Decode — decodes any standard Base64
The official docs imply this only decodes values produced by the matching Base64Encode() function. Runtime testing shows it decodes any valid standard Base64 string, regardless of how it was produced.
Show test script
<script runat="server">
Platform.Load("core","1.1.5");
// Base64Decode decodes ANY valid standard Base64 string, not just Base64Encode output.
try {
// This string was produced by an external/standard encoder, not by Base64Encode.
var external = "SGVsbG8sIFNGTUMh";
var decoded = Platform.Function.Base64Decode(external);
// OBSERVED: decoded === "Hello, SFMC!" (match: true) - decodes standard Base64 regardless of origin.
Platform.Response.Write("external base64: " + external + "\n");
Platform.Response.Write("decoded: " + decoded + "\n");
Platform.Response.Write("match: " + (decoded === "Hello, SFMC!") + "\n");
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Platform.Function.EndImpressionRegion — returns null, not void
The official docs type the return as void, but at runtime the call always returns a genuine JS null (typeof "object", strict === null is true, === undefined is false) — even when called with no matching BeginImpressionRegion. The optional closeAll argument accepts a boolean, and string values are coerced (still returns null). EndImpressionRegion itself never throws in this context; only the paired BeginImpressionRegion raises the ResolvedValueParameter literal-only error on a plain CloudPage GET.
Core-library equivalent: the bare-name EndImpressionRegion() Core form differs in return type — it returns undefined (typeof "undefined"), whereas this Platform.Function form returns a genuine null (typeof "object", === null).
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// EndImpressionRegion returns a genuine JS null (not void), even with no matching Begin.
try {
var r = Platform.Function.EndImpressionRegion();
Platform.Response.Write("typeof: " + (typeof r) + "\n");
Platform.Response.Write("=== null: " + (r === null) + "\n");
Platform.Response.Write("=== undefined: " + (r === undefined) + "\n");
// OBSERVED: EndImpressionRegion() returned typeof "object", === null true, === undefined false (genuine JS null, not void)
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
// The optional closeAll argument accepts a boolean; string values are coerced.
try {
var r2 = Platform.Function.EndImpressionRegion(true);
Platform.Response.Write("closeAll=true === null: " + (r2 === null) + "\n");
var r3 = Platform.Function.EndImpressionRegion("true");
Platform.Response.Write("closeAll=\"true\" === null: " + (r3 === null) + "\n");
// OBSERVED: both EndImpressionRegion(true) and EndImpressionRegion("true") returned null; boolean and coerced-string args accepted, no throw
} catch (e) { Platform.Response.Write("ERROR: " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpRequest.timeout — undocumented but works
Not listed as a configuration property in the official docs (which only mention that send() times out after 30 seconds). The timeout property does exist on a Script.Util.HttpRequest instance, defaults to 30 (i.e. seconds, matching the documented fixed 30-second send() timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting req.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So the property is undocumented-but-present and stored/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default req.timeout = 30 (seconds, typeof clr); after
// req.timeout = 5000 it reads back 5000; but req.timeout = 2 against
// https://httpbin.org/delay/10 did NOT time out - request ran ~10253 ms and returned
// statusCode 200. => property exists + is writable/readable, but a lowered value is
// NOT enforced (unit is seconds, not milliseconds; "applied at runtime" only partly true).
// B1: read the DEFAULT timeout value before setting it
try {
var reqA = new Script.Util.HttpRequest("https://postman-echo.com/post");
Platform.Response.Write("B1 typeof req.timeout = " + (typeof reqA.timeout) + "\n");
Platform.Response.Write("B1 default req.timeout = " + reqA.timeout + "\n"); // OBSERVED: 30
} catch (e) { Platform.Response.Write("B1 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B2: SET timeout and READ IT BACK (write takes effect)
try {
var reqB = new Script.Util.HttpRequest("https://postman-echo.com/post");
reqB.timeout = 5000;
Platform.Response.Write("B2 after set 5000, req.timeout = " + reqB.timeout + "\n"); // OBSERVED: 5000
} catch (e) { Platform.Response.Write("B2 THREW -> " + Platform.Function.Stringify(e) + "\n"); }
// B3: LOW timeout (2) against a ~10s no-early-byte endpoint - is it enforced?
try {
var reqC = new Script.Util.HttpRequest("https://httpbin.org/delay/10");
reqC.method = "GET";
reqC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = reqC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10253 ms (timeout=2 NOT enforced)
Platform.Response.Write("B3 statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + "\n");
} catch (e) { Platform.Response.Write("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e) + "\n"); }
</script>
Script.Util.HttpGet.timeout — undocumented but works
Not listed as a configuration property in the official docs. The timeout property does exist on a new Script.Util.HttpGet(url) instance, defaults to 30 (i.e. seconds — matching the documented fixed 30-second timeout — not milliseconds), and is writable (a value written to it reads back unchanged). However, a lowered timeout value is not actually enforced at runtime: setting getReq.timeout = 2 against a ~10-second endpoint does not abort early — the request runs the full ~10 s and returns HTTP 200. So this behaves identically to Script.Util.HttpRequest.timeout: undocumented-but-present and writable/readable, but it does not shorten the effective request timeout below the platform default.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): default getReq.timeout = 30 (seconds, typeof clr); after
// getReq.timeout = 5000 it reads back 5000 and getReq.timeout = 2 reads back 2; but
// getReq.timeout = 2 against https://postman-echo.com/delay/10 did NOT time out -
// .send() ran ~10143 ms and returned statusCode 200. => property exists + is
// writable/readable, but a lowered value is NOT enforced (unit is seconds, not
// milliseconds; "applied end-to-end at runtime" was WRONG). Same as HttpRequest.timeout.
function pre(t) { Platform.Response.Write(t + "\n"); }
// B1: default .timeout value + typeof on a fresh Script.Util.HttpGet instance
try {
var getA = new Script.Util.HttpGet("https://postman-echo.com/get");
pre("B1 typeof getReq.timeout = " + (typeof getA.timeout));
pre("B1 default getReq.timeout = " + getA.timeout); // OBSERVED: 30 (seconds)
} catch (e) { pre("B1 THREW -> " + Platform.Function.Stringify(e)); }
// B2: SET timeout and READ IT BACK (writable)
try {
var getB = new Script.Util.HttpGet("https://postman-echo.com/get");
getB.timeout = 5000;
pre("B2 after set 5000, getReq.timeout = " + getB.timeout); // OBSERVED: 5000
} catch (e) { pre("B2 THREW -> " + Platform.Function.Stringify(e)); }
// B3: LOW timeout (2) against a ~10s slow endpoint - is it enforced (abort early)?
try {
var getC = new Script.Util.HttpGet("https://postman-echo.com/delay/10");
getC.timeout = 2;
var t0 = (new Date()).getTime();
var respC = getC.send();
var t1 = (new Date()).getTime();
// OBSERVED: NO timeout - statusCode 200, elapsed ~10143 ms (timeout=2 NOT enforced)
pre("B3 .send() statusCode = " + respC.statusCode + " elapsedMs = " + (t1 - t0) + " (timeout=2)");
} catch (e) { pre("B3 TIMED OUT / THREW -> " + Platform.Function.Stringify(e)); }
</script>
<DataExtensionInstance>.Fields.Retrieve — field metadata also carries an undocumented ObjectID
The official reference’s example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal and DefaultValue. At runtime every field object also carries an ObjectID (a non-empty string), which is the identifier needed to address the column through the SOAP API. The returned collection is also host-backed rather than a genuine JS Array: it reports as [object Array] and exposes .length and .push, but instanceof Array is false, so guard with a .length check.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): every field object exposes a non-empty string ObjectID
// in addition to the six properties the official example response documents.
function pre(t) { Platform.Response.Write(t + "\n"); }
var fields = DataExtension.Init("yourDataExtensionKey").Fields.Retrieve();
pre("instanceof Array = " + (fields instanceof Array)); // OBSERVED: false
pre("toString = " + Object.prototype.toString.call(fields)); // OBSERVED: [object Array]
for (var i = 0; i < fields.length; i++) {
pre(("" + fields[i].Name) + " ObjectID typeof = " + (typeof fields[i].ObjectID)); // OBSERVED: string
}
</script>
<QueryDefinitionInstance>.Update — failures return "Error", not a throw
The official docs say Update failures throw. Runtime-verified: Update on a key that does not resolve returns the plain string "Error" instead of throwing. Always compare the return value against "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Update failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Update on a missing key returns "Error" (docs: throw).
* 2. DEV Update on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Update(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Update({ Name: "nope" });
}), "returned");
assert("DEV Update(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Update failure result is string", typeof status, "string");
</script>
<QueryDefinitionInstance>.Remove — failures return "Error", not a throw
The official docs say Remove failures throw. Runtime-verified: Remove on a key that never existed returns the plain string "Error" instead of throwing. A successful Remove soft-deletes the row (Status: Inactive); Core Retrieve no longer finds it, while WSProxy like may still list Inactive leftovers. Confirm deletion with Core Retrieve.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: <QueryDefinitionInstance>.Remove failures return
* "Error", they do NOT throw.
*
* Proves:
* 1. DEV Remove on a missing key returns "Error" (docs: throw).
* 2. DEV Remove on a missing key does not throw.
* 3. DEV typeof the failure result is string.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function invocationResult(fn) {
try { fn(); return "returned"; } catch (ex) { return "threw"; }
}
var status = null;
assert("DEV Remove(missing) does NOT throw (docs: throw)", invocationResult(function () {
status = QueryDefinition.Init("ssjs-guide-no-such-qd-zzz").Remove();
}), "returned");
assert("DEV Remove(missing) returns \"Error\" (docs: throw)", "" + status, "Error");
assert("DEV typeof Remove failure result is string", typeof status, "string");
</script>
<DataExtensionInstance>.Fields.Retrieve — field objects include an undocumented ObjectID
The official docs example response lists only Name, FieldType, IsPrimaryKey, MaxLength, Ordinal, and DefaultValue. At runtime each returned field object also carries an undocumented ObjectID (string, a GUID) and an undocumented StorageType (string, e.g. "Plain") property. The collection reports as a JS Array via Object.prototype.toString.call(...) → [object Array] and has a numeric .length, but note the host-array quirk that fields instanceof Array is false. IsRequired is not returned (undefined). Sibling methods <DataExtensionInstance>.Fields.Add and <DataExtensionInstance>.Fields.UpdateSendableField return the string "OK" on success and the string "Error" on failure — they do not throw, contrary to the docs’ “throws on failure” wording.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// READ-ONLY probe of <DataExtensionInstance>.Fields.Retrieve() against SSJSGUIDE_VERIFY.
// Proves: return is [object Array] with numeric .length; each element carries the
// undocumented ObjectID (GUID) and StorageType ("Plain") beyond the documented fields;
// documented IsRequired is not returned (undefined). Host-array quirk: instanceof Array === false.
// OBSERVED: length=7; every element has keys Name, ObjectID, FieldType, IsPrimaryKey,
// OBSERVED: MaxLength, Ordinal, DefaultValue, StorageType. toStringTag=[object Array];
// OBSERVED: instanceof Array=false; [0].ObjectID is a GUID string; [0].IsRequired=undefined.
// OBSERVED VERDICT: CONFIRMED — undocumented ObjectID present (plus undocumented StorageType).
function pre(t) { Platform.Response.Write(t + "\n"); }
try {
var de = DataExtension.Init("SSJSGUIDE_VERIFY");
var fields = de.Fields.Retrieve();
pre("typeof=" + (typeof fields));
pre("toStringTag=" + Object.prototype.toString.call(fields));
pre("instanceof Array? " + (fields instanceof Array));
pre("length=" + fields.length);
for (var i = 0; i < fields.length; i++) {
var f = fields[i], keys = "";
for (var k in f) { keys += k + "(" + (typeof f[k]) + ")=" + f[k] + "; "; }
pre("[" + i + "] " + keys);
}
if (fields.length > 0) {
pre("[0].ObjectID typeof=" + (typeof fields[0].ObjectID) + " value=" + fields[0].ObjectID);
pre("[0].StorageType typeof=" + (typeof fields[0].StorageType) + " value=" + fields[0].StorageType);
pre("[0].IsRequired typeof=" + (typeof fields[0].IsRequired));
}
} catch (e) { pre("THREW: " + e); }
</script>
DateTime.TimeZone.Retrieve — filter is optional; returns a CLR collection
The official docs present filter as a required argument. At runtime it is optional — DateTime.TimeZone.Retrieve() with no argument returns the full time-zone list, each row carrying ID (number) and Name (string). A filter that matches nothing returns an empty collection rather than null, so .length can be read unconditionally, but a malformed filter (a plain string, or an object missing the Property / SimpleOperator / Value trio) raises a time-zone retrieval error instead of returning an empty list. The returned value behaves as a JavaScript array: it is indexable, exposes .length, reports [object Array], carries array methods such as push and slice, and serializes normally through Stringify(); only instanceof Array is false, which is the engine-wide instanceof-on-builtins bug rather than anything specific to this method. Platform.Load("core", ...) is genuinely required — before the load DateTime is undefined and the call throws.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: no-argument call returned the full time-zone list (length > 0); a filter
// matching nothing returned an empty collection with .length === 0 (not null); the
// result is indexable with .length but instanceof Array is false and push is undefined;
// a malformed filter threw a time-zone retrieval error.
Platform.Response.Write("=== DateTime.TimeZone.Retrieve probe ===\n");
// Probe A: no argument -> full list
try {
var all = DateTime.TimeZone.Retrieve();
Platform.Response.Write("A no-arg length: " + all.length + "\n");
Platform.Response.Write("A first row: " + all[0].ID + " = " + all[0].Name + "\n");
Platform.Response.Write("A instanceof Array: " + (all instanceof Array) + "\n");
Platform.Response.Write("A typeof push: " + (typeof all.push) + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: filter that matches nothing -> empty collection, not null
try {
var none = DateTime.TimeZone.Retrieve({ Property: "ID", SimpleOperator: "equals", Value: -999 });
Platform.Response.Write("B === null? " + (none === null) + "\n");
Platform.Response.Write("B length: " + none.length + "\n");
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: malformed filter -> throws
try {
var bad = DateTime.TimeZone.Retrieve("not-a-filter");
Platform.Response.Write("C no throw; length: " + bad.length + "\n");
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
</script>
<WSProxyInstance>.performBatch — action verb is case-insensitive; each Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing), whereas a bogus verb is rejected with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array, one entry per input item) } — an empty items array returns Status "OK" with Results.length 0. Each Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performBatch(type, items, action).
// Claim: action verb is case-insensitive; each Results entry carries an Object wrapper and a
// Task sub-object. Probes use EMPTY / INVALID inputs only, so nothing real is started.
// OBSERVED (live CloudPage): CONFIRMED. typeof performBatch = "clrmethodinfo".
// B (empty items, "start"): Status "OK", StatusMessage "" (string), RequestID present,
// Results object with length 0. C (bogus ObjectID, "start") & D (bogus ObjectID, "Start")
// behave IDENTICALLY -> Status "InvalidRequest", Results.length 1, Results[0].StatusCode
// "Error"; Results[0] carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID
// => verb is case-insensitive AND the undocumented Object/Task per-item detail exists.
// E (bogus verb "definitelyNotARealVerb"): Status "Error", message "… is not an action that
// can be Performed on a InteractionDefinition" -> unknown verbs are rejected differently,
// confirming "start"/"Start" are both accepted. Nothing real ran (all ObjectIDs invalid).
Platform.Response.Write("=== performBatch NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performBatch: " + (typeof api.performBatch) + "\n");
Platform.Response.Write("A api.performBatch.length (arity): " + api.performBatch.length + "\n");
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: EMPTY items array + lowercase "start" -- nothing to act on, so no real run.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performBatch("QueryDefinition", [], "start");
Platform.Response.Write("B Status: " + rB.Status + "\n"); // OBSERVED: "OK"
Platform.Response.Write("B typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B Results length: " + rB.Results.length + "\n"); // OBSERVED: 0
} catch (e) {
Platform.Response.Write("B THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: INVALID (nonexistent) ObjectID + lowercase "start" -- object does not exist,
// so no real mutation. Inspect per-item Results entry for Object/Task sub-objects.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "start");
Platform.Response.Write("C Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("C Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
var e0 = rC.Results[0];
if (e0 !== null) {
Platform.Response.Write("C Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("C typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("C typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("C typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("C THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "Start");
Platform.Response.Write("D (Start) Status: " + rD.Status + "\n"); // OBSERVED: "InvalidRequest" (same as C)
Platform.Response.Write("D (Start) Results[0].StatusCode: " + rD.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as C)
} catch (e) {
Platform.Response.Write("D (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe E: clearly-invalid action verb -- rejected differently, confirming start/Start are
// recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiE = new Script.Util.WSProxy();
var rE = apiE.performBatch("QueryDefinition", [{ ObjectID: "00000000-0000-0000-0000-000000000000" }], "definitelyNotARealVerb");
Platform.Response.Write("E (bad verb) Status: " + rE.Status + "\n"); // OBSERVED: "Error"
Platform.Response.Write("E (bad verb) Results[0].StatusMessage: " + rE.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} catch (e) {
Platform.Response.Write("E (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
<WSProxyInstance>.performItem — action verb is case-insensitive; the single Results entry carries a Task sub-object
The official docs list the action argument as Enum('Start') without noting case behaviour. The perform verb is case-insensitive — lowercase "start" and capitalised "Start" are parsed identically (both reach the same per-item processing and, given an invalid all-zero ObjectID, return exactly the same result), whereas a bogus verb is rejected differently with "… is not an action that can be Performed …". The return object is { Status (string), StatusMessage (string, empty on success), RequestID (string), Results (Array with a single entry for the acted-on item) }, and that Results entry exposes not just StatusCode/StatusMessage/OrdinalID/ErrorCode (with ErrorCode/OrdinalID as numbers) but also an Object wrapper (the acted-on API object) and a Task sub-object — per-item detail the official docs do not describe. This mirrors the sibling performBatch.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// NON-DESTRUCTIVE verification of <WSProxyInstance>.performItem(type, item, action).
// Claim (A5): action verb is case-insensitive; the single Results entry carries an Object
// wrapper and a Task sub-object. Probes use INVALID (all-zero) ObjectID only, so nothing
// real is started/run. Mirrors the sibling performBatch verification.
// OBSERVED (live CloudPage): CONFIRMED. typeof performItem = "clrmethodinfo".
// B (bogus ObjectID, "start") & C (bogus ObjectID, "Start") behave IDENTICALLY ->
// Status "InvalidRequest", Results.length 1, Results[0].StatusCode "Error"; Results[0]
// carries Object (object) + Task (object) + numeric ErrorCode/OrdinalID; StatusMessage
// is a string ("" at top level). D (bogus verb "definitelyNotARealVerb"): Status "Error",
// message "… is not an action that can be Performed on a InteractionDefinition" -> unknown
// verbs rejected differently, so "start"/"Start" are both accepted (case-insensitive).
// Nothing real ran (all-zero ObjectID rejected as InvalidRequest).
Platform.Response.Write("=== performItem NON-DESTRUCTIVE verification ===\n");
// Probe A: presence + arity
try {
var api = new Script.Util.WSProxy();
Platform.Response.Write("A typeof api.performItem: " + (typeof api.performItem) + "\n"); // OBSERVED: clrmethodinfo
Platform.Response.Write("A api.performItem.length (arity): " + api.performItem.length + "\n"); // OBSERVED: undefined
} catch (e) {
Platform.Response.Write("A THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe B: INVALID (nonexistent, all-zero) ObjectID + lowercase "start" -- the object does
// not exist so no real perform runs. Inspect return shape + per-item Object/Task sub-objects.
try {
var apiB = new Script.Util.WSProxy();
var rB = apiB.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "start");
Platform.Response.Write("B (start) Status: " + rB.Status + "\n"); // OBSERVED: "InvalidRequest"
Platform.Response.Write("B (start) typeof StatusMessage: " + (typeof rB.StatusMessage) + " value=" + Platform.Function.Stringify(rB.StatusMessage) + "\n"); // OBSERVED: string ""
Platform.Response.Write("B (start) has RequestID: " + (typeof rB.RequestID != "undefined") + "\n"); // OBSERVED: true
Platform.Response.Write("B (start) typeof Results: " + (typeof rB.Results) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) Results length: " + rB.Results.length + "\n"); // OBSERVED: 1
var e0 = rB.Results[0];
if (e0 !== null && typeof e0 != "undefined") {
Platform.Response.Write("B (start) Results[0].StatusCode: " + e0.StatusCode + "\n"); // OBSERVED: "Error"
Platform.Response.Write("B (start) Results[0].StatusMessage: " + e0.StatusMessage + "\n"); // OBSERVED: invalid-state message
Platform.Response.Write("B (start) typeof Results[0].Object: " + (typeof e0.Object) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].Task: " + (typeof e0.Task) + "\n"); // OBSERVED: object
Platform.Response.Write("B (start) typeof Results[0].ErrorCode: " + (typeof e0.ErrorCode) + "\n"); // OBSERVED: number
Platform.Response.Write("B (start) typeof Results[0].OrdinalID: " + (typeof e0.OrdinalID) + "\n"); // OBSERVED: number
}
} catch (e) {
Platform.Response.Write("B (start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe C: same bogus input with capitalised "Start" -- identical behaviour proves the
// verb is case-insensitive.
try {
var apiC = new Script.Util.WSProxy();
var rC = apiC.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "Start");
Platform.Response.Write("C (Start) Status: " + rC.Status + "\n"); // OBSERVED: "InvalidRequest" (same as B)
Platform.Response.Write("C (Start) Results length: " + rC.Results.length + "\n"); // OBSERVED: 1
if (rC.Results[0] !== null && typeof rC.Results[0] != "undefined") {
Platform.Response.Write("C (Start) Results[0].StatusCode: " + rC.Results[0].StatusCode + "\n"); // OBSERVED: "Error" (same as B)
}
} catch (e) {
Platform.Response.Write("C (Start) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
// Probe D: clearly-invalid action verb -- should be rejected differently, confirming that
// start/Start are recognised verbs. Bogus ObjectID keeps it non-destructive.
try {
var apiD = new Script.Util.WSProxy();
var rD = apiD.performItem("QueryDefinition", { ObjectID: "00000000-0000-0000-0000-000000000000" }, "definitelyNotARealVerb");
Platform.Response.Write("D (bad verb) Status: " + rD.Status + "\n"); // OBSERVED: "Error"
if (rD.Results && rD.Results[0] !== null && typeof rD.Results[0] != "undefined") {
Platform.Response.Write("D (bad verb) Results[0].StatusMessage: " + rD.Results[0].StatusMessage + "\n"); // OBSERVED: "… is not an action that can be Performed …"
} else {
Platform.Response.Write("D (bad verb) StatusMessage: " + rD.StatusMessage + "\n");
}
} catch (e) {
Platform.Response.Write("D (bad verb) THREW -> " + Platform.Function.Stringify(e) + "\n");
}
Platform.Response.Write("=== done ===\n");
</script>
Number.prototype.toString() — only 2, 8, 10, 16 supported
MDN specifies Number.prototype.toString(radix) accepts any radix from 2 to 36. In the SFMC Jint engine only radix 2, 8, 10, and 16 work — every other base throws "Invalid Base.". Fractional values are also truncated to their integer part before non-decimal conversion ((3.5).toString(2) → "100", not "11.1"), while the default/base-10 form keeps the fraction ((3.5).toString() → "3.5"). Restrict toString radix conversions to those four bases.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// Number.prototype.toString(radix): only 2/8/10/16 supported; others throw "Invalid Base.";
// fractions truncate before non-decimal conversion. Each probe in its own try/catch.
function probe(label, fn) {
try {
var r = fn();
Platform.Response.Write(label + " = \"" + r + "\" (typeof " + (typeof r) + ")\n");
} catch (ex) {
Platform.Response.Write(label + " THREW: " + (ex && ex.message ? ex.message : Platform.Function.Stringify(ex)) + "\n");
}
}
// OBSERVED: supported bases work -> (255).toString(16)="ff", (5).toString(2)="101",
// (8).toString(8)="10", (255).toString(2)="11111111", (255).toString(10)="255"
probe("(255).toString(16)", function () { return (255).toString(16); });
probe("(5).toString(2)", function () { return (5).toString(2); });
probe("(8).toString(8)", function () { return (8).toString(8); });
probe("(255).toString(2)", function () { return (255).toString(2); });
probe("(255).toString(10)", function () { return (255).toString(10); });
// OBSERVED: no radix arg defaults to base 10 -> (255).toString()="255"
probe("(255).toString()", function () { return (255).toString(); });
// OBSERVED: unsupported radixes 3, 5, 36 all THREW "Invalid Base."
probe("(255).toString(3)", function () { return (255).toString(3); });
probe("(255).toString(5)", function () { return (255).toString(5); });
probe("(255).toString(36)", function () { return (255).toString(36); });
// OBSERVED: (3.5).toString(2)="100" (fraction truncated); (3.5).toString()="3.5" (base-10 keeps fraction)
probe("(3.5).toString(2)", function () { return (3.5).toString(2); });
probe("(3.5).toString()", function () { return (3.5).toString(); });
</script>
Number.prototype.toExponential() — no-arg form pads trailing zeros
MDN specifies that toExponential() with no argument uses the minimal number of digits needed to represent the value. In the SFMC Jint engine the no-arg form instead pads the significand with trailing zeros ((3.14159).toExponential() → "3.1415900000000000e+0"). Always pass an explicit fractionDigits argument for predictable output.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED (live CloudPage): typeof=function; no-arg PADS trailing zeros
// (12345).toExponential() -> "1.2345000000000000e+4"
// (3.14159).toExponential() -> "3.1415900000000000e+0"
// (12345).toExponential(2) -> "1.23e+4"; (3.14159).toExponential(4) -> "3.1416e+0"
// (0.00012).toExponential(3) -> "1.200e-4" => CONFIRMED (live-verified)
Platform.Response.Write("=== toExponential probe ===\n");
// Probe 1: does the method exist?
try {
Platform.Response.Write("typeof (12345).toExponential = " + (typeof (12345).toExponential) + "\n");
} catch (e) { Platform.Response.Write("P1 THREW -> " + e.message + "\n"); }
// Probe 2: no-arg form (MDN: minimal digits; SFMC pads trailing zeros)
try {
Platform.Response.Write("(12345).toExponential() = " + (12345).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P2 THREW -> " + e.message + "\n"); }
// Probe 3: no-arg on the value used in the claim body
try {
Platform.Response.Write("(3.14159).toExponential() = " + (3.14159).toExponential() + "\n");
} catch (e) { Platform.Response.Write("P3 THREW -> " + e.message + "\n"); }
// Probe 4: explicit fractionDigits = 2
try {
Platform.Response.Write("(12345).toExponential(2) = " + (12345).toExponential(2) + "\n");
} catch (e) { Platform.Response.Write("P4 THREW -> " + e.message + "\n"); }
// Probe 5: explicit fractionDigits = 4 (claim body example)
try {
Platform.Response.Write("(3.14159).toExponential(4) = " + (3.14159).toExponential(4) + "\n");
} catch (e) { Platform.Response.Write("P5 THREW -> " + e.message + "\n"); }
// Probe 6: small number with explicit digits
try {
Platform.Response.Write("(0.00012).toExponential(3) = " + (0.00012).toExponential(3) + "\n");
} catch (e) { Platform.Response.Write("P6 THREW -> " + e.message + "\n"); }
Platform.Response.Write("=== done ===\n");
</script>
.Start / .Pause</code></a> — Pause results in status "Inactive", not "Paused"; surplus arguments ignored</h3>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
</div>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>
</div>
Both calls are runtime-proven and return the string "OK" with LastMessage TriggeredSendDefinition updated. Two details are not in the official docs. First, the resulting state after Pause() reads back as TriggeredSendStatus: "Inactive", not "Paused" - verified by a follow-up WSProxy retrieve; Start() sets "Active". Second, both methods take no arguments per the docs, and surplus arguments are silently ignored rather than rejected: Start("x") and Pause("x") also return "OK".
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
// OBSERVED: Start() -> "OK" and status "Active"; Pause() -> "OK" and status "Inactive" (NOT "Paused").
// Start("x") / Pause("x") also return "OK" - surplus arguments are ignored.
var KEY = "your_tsd_customer_key";
var ts = TriggeredSend.Init(KEY);
var api = new Script.Util.WSProxy();
function status() {
var r = api.retrieve("TriggeredSendDefinition", ["CustomerKey", "TriggeredSendStatus"],
{ Property: "CustomerKey", SimpleOperator: "equals", Value: KEY });
return r.Results.length ? r.Results[0].TriggeredSendStatus : "(none)";
}
Platform.Response.Write("Start(): " + ts.Start() + " -> " + status() + "\n");
Platform.Response.Write("Pause(): " + ts.Pause() + " -> " + status() + "\n");
Platform.Response.Write("Start(\"x\"): " + ts.Start("x") + "\n");
Platform.Response.Write("Pause(\"x\"): " + ts.Pause("x") + "\n");
</script>
Format — short-form d uses a four-digit year
The official Format reference shows predefined short-form d as a two-digit year (e.g. 8/5/24 for the sample August 2024 instant). On a published CloudPage the same input returns a four-digit year (8/5/2024). Related short-form g already documents a four-digit year and matches runtime. Prefer the four-digit result when writing assertions or display expectations.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Differs-from-docs: predefined short-form date code `d`
*
* Proves:
* 1. DEV Format(date, "d") is "8/5/2024" for the page sample instant
* (official Salesforce docs example: "8/5/24").
* 2. Related short-form `g` already documents a 4-digit year and matches.
*
* EXPECTED OUTPUT: every line starts with PASS.
*/
function assert(id, actual, expected) {
Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
var date = "2024-08-05T13:41:23.000-06:00";
assert("DEV d short-form uses 4-digit year (official docs: 8/5/24)", "" + Format(date, "d"), "8/5/2024");
assert("g short-form keeps documented 4-digit year", "" + Format(date, "g"), "8/5/2024 1:41 PM");
</script>