The <script runat="server"> Block

SSJS is embedded using an HTML <script> tag with the runat="server" attribute:

<html>
<body>
  <h1>My Page</h1>

  <script runat="server">
  Write("<p>This text comes from SSJS.</p>");
  </script>

  <p>This is regular HTML.</p>
</body>
</html>

The Write() output replaces the script block in the final rendered page. The subscriber sees:

<html>
<body>
  <h1>My Page</h1>

  <p>This text comes from SSJS.</p>

  <p>This is regular HTML.</p>
</body>
</html>
Show test script
<script runat="server">
/*
 * Chapter: script runat=server
 * Proves:
 *   1. Script body executes (this assert is proof).
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("script executed", "true", "true");
</script>

The language Attribute

You can optionally specify the language:

<script runat="server" language="JavaScript">
// Explicitly sets JavaScript mode
</script>

This is usually omitted — runat="server" alone is sufficient and implies JavaScript. The language attribute is relevant when a page mixes AMPscript script blocks:

<script runat="server" language="ampscript">
/* AMPscript block */
Set @name = "World"
</script>

<script runat="server" language="JavaScript">
/* SSJS block */
var name = Variable.GetValue("@name");
Write("Hello, " + name);
</script>
Show test script
<script runat="server">
/*
 * Chapter: language attribute
 * Proves:
 *   1. NON-ASSERTABLE: attribute is markup, not SSJS.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("markup-only", "documented", "documented");
</script>

Multiple Script Blocks

A single page can have multiple <script runat="server"> blocks. They all share the same execution scope — variables declared in one block are visible in subsequent blocks.

<script runat="server">
// Block 1: Load libraries and declare helpers
Platform.Load("core", "1.1.5");

function formatDate(dateString,dateFormat,timeFormat,isoLocale) {
    if(!dateFormat) {
      dateFormat = "MM/DD/YYYY";
    }
    Platform.Variable.SetValue("@formatDate_string",dateString);
    Platform.Variable.SetValue("@formatDate_date",dateFormat);
    Platform.Variable.SetValue("@formatDate_time",timeFormat);
    Platform.Variable.SetValue("@formatDate_iso",isoLocale);
    return Platform.Function.TreatAsContent("%%=FormatDate(@formatDate_string, @formatDate_date, @formatDate_time, @formatDate_iso)=%%");
}

</script>

<h1>Welcome</h1>

<script runat="server">
// Block 2: Use helpers defined in Block 1
var today = formatDate(Platform.Function.Now());
Write("<p>Today is " + today + "</p>");
</script>

All blocks execute in order before the page is assembled. The output of each block appears at that block’s position in the HTML.

Show test script
<script runat="server">
/*
 * Chapter: Multiple Script Blocks
 * Proves:
 *   1. Top-level var persists within one script (stand-in for shared scope).
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var shared = "block1";
assert("shared var", shared, "block1");
</script>

Hoisting Behavior

Function declarations are hoisted within their script block but not across blocks. Blocks execute top-to-bottom in page order and share one global scope, so a later block can call a function an earlier block defined — but an earlier block cannot reach forward to a function only defined later. Calling a not-yet-defined function throws Object expected: <name>.

<script runat="server">
// ❌ This will error — greet() is defined in the next block
Write(greet("World"));
</script>

<script runat="server">
function greet(name) {
    return "Hello, " + name + "!";
}
</script>

The forward reference throws Object expected: greet at the point of the call. The page itself still returns HTTP 200 — the error surfaces in the rendered body where the failing block would have written, not as a server error. Everything the earlier block wrote before the failing line is still emitted.

Safe pattern: Put all function declarations in the first script block:

<script runat="server">
Platform.Load("core", "1.1.5");

// All function declarations at the top
function greet(name) {
    return "Hello, " + name + "!";
}

function getSubscriberData(sk) {
    return Platform.Function.Lookup("Subscribers", "Email", "SubscriberKey", sk);
}
</script>

<h1>%%=v(@salutation)=%%</h1>

<script runat="server">
// Usage in later blocks is fine
var sk = Platform.Request.GetQueryStringParameter("sk");
Write(greet(getSubscriberData(sk)));
</script>
Show test script
<script runat="server">
/*
 * Chapter: Hoisting
 * Proves:
 *   1. Function declaration hoisted within the block.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var ok = false; try { hoist(); ok = true; } catch (e) { ok = false; }
function hoist() {}
assert("fn hoisted", ok ? "true" : "false", "true");
</script>

AMPscript Inside SSJS

AMPscript and SSJS run in the same document and can share data through the Variable namespace:

%%[
  SET @subscriberKey = _subscriberKey
]%%

<script runat="server">
// Read the AMPscript variable in SSJS
var sk = Variable.GetValue("@subscriberKey");

// Write back to AMPscript
var email = Platform.Function.Lookup("Subscribers", "Email", "SubscriberKey", sk);
Variable.SetValue("@email", email);
</script>

<!-- Use the SSJS-set variable in AMPscript -->
<p>Email: %%=v(@email)=%%</p>

Or use Platform.Variable.GetValue / Platform.Variable.SetValue for the same purpose.

Show test script
<script runat="server">
/*
 * Chapter: AMPscript Inside SSJS
 * Proves:
 *   1. Variable bridge works without embedding AMPscript delimiters in SSJS.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
Platform.Variable.SetValue("@embed", "via-ssjs");
assert("bridge", Platform.Variable.GetValue("@embed"), "via-ssjs");
</script>

Ctrl: Tags (Alternative Syntax)

SFMC also supports older ctrl:field, ctrl:var, and ctrl:eval tags for inline SSJS output. These are legacy and rarely used in new development:

<ctrl:eval>Platform.Function.Now()</ctrl:eval>
<ctrl:var name="myVariable" />
<ctrl:field name="FirstName" />

Stick to <script runat="server"> + Write() for all new work.

→ Next: Execution Contexts

Show test script
<script runat="server">
/*
 * Chapter: Ctrl Tags
 * Proves:
 *   1. NON-ASSERTABLE: Ctrl: markup is not executable inside this HTML script harness.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("markup-only", "documented", "documented");
</script>