Calling external AI from CloudPages and SSJS — how-to
The pattern for calling an external LLM from Marketing Cloud — where the call belongs (ahead of time, not at render time), how to handle auth and failure, and the data, latency, and cost guardrails that keep it from breaking a page or a budget. The how-to, with the gotchas built in.
Sometimes the model you need isn't Einstein and isn't Agentforce — it's an external LLM you call yourself to generate copy, classify an inbound reply, or summarize a record. Marketing Cloud lets you do this from SSJS with HTTPRequest (in a CloudPage or a Script Activity) or from AMPscript with HTTPPost. The mechanics are easy. The engineering judgment — where the call belongs and how it fails — is the whole job.
This page is the pattern Cleon uses, with the gotchas (6 through 9) built in rather than bolted on.
The one decision that matters: ahead of time, not at render time
The most important choice is when the call runs.
RENDER TIME (avoid) AHEAD OF TIME (prefer)
CloudPage / email renders Automation / Script Activity runs
→ calls the model synchronously → calls the model
→ visitor waits on the model → writes the result to a DE
→ model slow/down = page broken → render-time just reads the DEAn external call at render time makes the model's latency your page's latency and the model's outage your outage (gotchas 6 and 7). Generating ahead of time, into a Data Extension, turns the render into a plain DE lookup — fast, deterministic, and yours to retry when the model fails. Default to ahead-of-time. Render-time calls are for genuinely interactive surfaces (a chat-style CloudPage) where you've accepted the latency and built the failure path.
The pattern (ahead of time)
A Script Activity in an Automation, iterating the audience, calling the model, writing the result back:
<script runat="server">
Platform.Load("Core", "1.1.1");
// Secrets come from a protected DE / Key Management, never hard-coded.
var apiKey = lookupApiKey(); // from a restricted DE or Key Mgmt
var endpoint = "https://api.example-llm.com/v1/messages";
try {
var req = new Script.Util.HttpRequest(endpoint);
req.method = "POST";
req.contentType = "application/json";
req.setHeader("Authorization", "Bearer " + apiKey);
req.postData = Stringify({
model: "the-model",
max_tokens: 300,
messages: [{ role: "user", content: buildPrompt(contactRow) }]
});
var res = req.send();
if (res.statusCode == 200) {
var out = Platform.Function.ParseJSON(String(res.content));
writeResultToDE(contactRow.SubscriberKey, out); // cache for render time
} else {
logModelError(contactRow.SubscriberKey, res.statusCode);
writeFallbackToDE(contactRow.SubscriberKey); // never leave it blank
}
} catch (e) {
// Never let one contact's failure abort the whole run.
logModelError(contactRow.SubscriberKey, e.message);
writeFallbackToDE(contactRow.SubscriberKey);
}
</script>The render-time email or page then reads the cached value with a plain Lookup — no external dependency in the send path at all.
Auth and secrets
- Never hard-code the API key in a CloudPage or Code Resource — CloudPages are URL-addressable and Code Resources get copied. Keep the key in a permission-restricted Data Extension or Key Management, and read it at runtime.
- Treat the key like any production credential: who can see it, how it rotates, who's notified when it leaks.
The three guardrails that keep it from biting
Data — what's allowed to leave
Sending customer data to an external provider is a compliance decision, not a technical one. Before any PII leaves Marketing Cloud, confirm a data-processing agreement covers the provider, and confirm whether they retain or train on what you send. Redact what doesn't need to leave. (Gotcha 6.)
Latency and failure — the call will be slow or down eventually
HTTPRequest is synchronous and can time out. Ahead-of-time placement contains the blast radius, but the Script Activity still needs to: wrap every call in try/catch, check the status code, write a fallback value on failure (never leave the field blank), and let one contact's failure not abort the run. (Gotchas 7 and 9.)
Cost and rate — the bill scales with the audience
One call per contact in a million-send program is a million calls — a per-token bill and a rate limit that both scale with the audience (gotcha 9). The ahead-of-time pattern already helps: generate once, cache in a DE, and re-sends cost nothing. For the generation run itself, throttle to stay under the provider's rate limit (Platform.Function.Sleep between batches) and model the cost against the real audience size before you launch.
The QA problem you can't sample away
A model's output is non-deterministic: the same prompt can return different copy on two calls, and a three-preview spot check won't catch the one bad output (gotcha 8). Constrain and validate before anything ships:
- Prefer a fixed set of options the model chooses among over open-ended generation, when you can.
- Validate the output — length, format, a banned-word / content filter — before writing it to the DE.
- For anything customer-visible and open-ended, a human approves the generated set before the send. The ahead-of-time pattern makes this possible: the copy exists in a DE before the send, so it can be reviewed.
When not to do this at all
If the task is "answer questions over our customer data" or "take an action in the platform," an external LLM is the wrong tool — that's Agentforce, grounded and governed. If the task is "predict engagement," that's Einstein. External AI earns its place for self-contained language tasks where you don't want to hand a platform agent your data and your actions. (See the AI Style Guide.)
Related
- Marketing Cloud AI gotchas — gotchas 6–9, built into this pattern
- Agentforce and Marketing Cloud — the governed alternative for data + actions
- Einstein for Marketing Cloud — the in-platform prediction layer
- Debugging AI personalization — when the generated value lands wrong or blank
- AI Style Guide — Agentforce vs. external AI, the decision