Module 1 — AMPscript fundamentals
AMPscript is SFMC's proprietary scripting language for email, SMS, and Cloud Pages. It runs server-side at send time, letting you personalise content dynamically for each subscriber. It's not a general-purpose language — it's purpose-built for SFMC personalisation.
AMPscript syntax basics
AMPscript is wrapped in %%[ ]%% blocks (for logic) or %%variable%% (for output). It's case-insensitive and whitespace-tolerant.
%%[ /* Declare and set a variable */ ]%%
%%[
VAR @firstName, @greeting
SET @firstName = AttributeValue("FirstName")
IF Empty(@firstName) THEN
SET @firstName = "there"
ENDIF
SET @greeting = Concat("Hi ", @firstName, "!")
]%%
<!-- Output the variable -->
<p>%%=v(@greeting)=%%</p>
Use %%=v(@variable)=%% to output a variable inline. Use %%[SET @x = ...]%% for logic. The v() function means "value of" — it's how you print variables into HTML.
Reading subscriber data with AttributeValue
%%[
/* AttributeValue reads from the sending Data Extension */
VAR @email, @firstName, @country, @loyaltyTier
SET @email = AttributeValue("EmailAddress")
SET @firstName = AttributeValue("FirstName")
SET @country = AttributeValue("Country")
SET @loyaltyTier = AttributeValue("LoyaltyTier")
]%%
<p>Hello %%=v(@firstName)=%%, your loyalty tier is %%=v(@loyaltyTier)=%%.</p>
Module 2 — AMPscript for dynamic content
AMPscript shines when you need to look up data from a second table, loop through arrays, or build conditional content blocks that go beyond what Content Builder's rules engine can do.
Lookup — fetching data from another DE
%%[
/* Look up a single value from another DE */
/* Lookup(DE_name, return_field, match_field, match_value) */
VAR @subKey, @tier, @discountCode
SET @subKey = AttributeValue("SubscriberKey")
/* Fetch loyalty tier from a separate DE */
SET @tier = Lookup("LoyaltyDE", "Tier", "SubscriberKey", @subKey)
/* Lookup a discount code based on the tier */
SET @discountCode = Lookup("DiscountCodesDE", "Code", "Tier", @tier)
]%%
<p>Your exclusive code: <strong>%%=v(@discountCode)=%%</strong></p>
LookupRows — returning multiple rows
%%[
VAR @subKey, @rows, @row, @rowCount, @i
VAR @productName, @productPrice, @productURL
SET @subKey = AttributeValue("SubscriberKey")
SET @rows = LookupRows("RecommendationsDE", "SubscriberKey", @subKey)
SET @rowCount = RowCount(@rows)
]%%
%%[ IF @rowCount > 0 THEN ]%%
<h2>Your recommendations</h2>
<table>
%%[
FOR @i = 1 TO @rowCount DO
SET @row = Row(@rows, @i)
SET @productName = Field(@row, "ProductName")
SET @productPrice = Field(@row, "Price")
SET @productURL = Field(@row, "URL")
]%%
<tr>
<td><a href="%%=v(@productURL)=%%">%%=v(@productName)=%%</a></td>
<td>£%%=v(@productPrice)=%%</td>
</tr>
%%[ NEXT @i ]%%
</table>
%%[ ELSE ]%%
<p>Check out our latest collection below.</p>
%%[ ENDIF ]%%
Date functions
%%[
VAR @today, @expiryDate, @daysLeft, @formattedExpiry
SET @today = Now()
SET @expiryDate = AttributeValue("OfferExpiryDate")
SET @daysLeft = DateDiff(@today, @expiryDate, "D")
SET @formattedExpiry = FormatDate(@expiryDate, "MMMM d, yyyy")
]%%
<p>Your offer expires on <strong>%%=v(@formattedExpiry)=%%</strong>
— that's only %%=v(@daysLeft)=%% days away!</p>
See the full AMPscript guide for a complete function reference.
Module 3 — Server-Side JavaScript (SSJS)
SSJS is JavaScript that executes server-side in SFMC. Unlike AMPscript, it can make HTTP requests, work with complex data structures, and interact with the SFMC platform API directly from within an email or Cloud Page.
Use AMPscript for email personalisation — it's faster and purpose-built for DE lookups and content rendering. Use SSJS for Cloud Pages, complex logic, external HTTP calls, or when you need JavaScript's data structures and methods.
SSJS basics
<script runat="server">
Platform.Load("Core", "1");
// Read a subscriber attribute
var firstName = Platform.Variable.GetValue("@firstName");
// Write to a variable (usable by AMPscript)
Platform.Variable.SetValue("@ssjs_output", "Hello from SSJS!");
// Write to the page output
Write("<p>Processed by SSJS</p>");
</script>
Writing to a Data Extension from SSJS
<script runat="server">
Platform.Load("Core", "1");
var de = DataExtension.Init("EventRegistrations");
// Upsert: update if key exists, insert if not
var result = de.Rows.Add({
SubscriberKey: "12345",
EmailAddress: "kwame@example.com",
EventName: "SFMC Summit 2026",
RegistrationDate: new Date().toISOString()
});
if (result) {
Write("Registration saved successfully.");
} else {
Write("Error saving registration.");
}
</script>
HTTP call from SSJS (Cloud Page)
<script runat="server">
Platform.Load("Core", "1");
var url = "https://api.example.com/products?category=shoes";
var headers = { "Authorization": "Bearer YOUR_TOKEN" };
var req = new Script.Util.HttpRequest(url);
req.emptyContentHandling = 0;
req.retryCount = 2;
req.setHeader("Authorization", "Bearer YOUR_TOKEN");
req.method = "GET";
var resp = req.send();
var content = String(resp.content);
var data = Platform.Function.ParseJSON(content);
if (data && data.products) {
for (var i = 0; i < data.products.length; i++) {
Write("<p>" + data.products[i].name + "</p>");
}
}
</script>
Module 4 — SQL deep dive
SQL in SFMC uses T-SQL and is executed in Automation Studio query activities. It's the most powerful segmentation and data manipulation tool in SFMC. See the full SQL reference guide for a comprehensive library of queries.
Working with System Data Views
-- Score subscribers by engagement over 90 days
SELECT
s.SubscriberKey,
s.EmailAddress,
COUNT(DISTINCT o.JobID) AS TotalOpens,
COUNT(DISTINCT c.JobID) AS TotalClicks,
CASE
WHEN COUNT(DISTINCT c.JobID) >= 3 THEN 'Highly Engaged'
WHEN COUNT(DISTINCT o.JobID) >= 2 THEN 'Engaged'
WHEN COUNT(DISTINCT o.JobID) = 1 THEN 'Low Engagement'
ELSE 'Unengaged'
END AS EngagementTier
FROM [MasterSubscriberDE] s
LEFT JOIN [_Open] o
ON s.SubscriberKey = o.SubscriberKey
AND o.EventDate >= DATEADD(day, -90, GETDATE())
LEFT JOIN [_Click] c
ON s.SubscriberKey = c.SubscriberKey
AND c.EventDate >= DATEADD(day, -90, GETDATE())
WHERE s.OptInStatus = 'Subscribed'
GROUP BY s.SubscriberKey, s.EmailAddress
Module 5 — REST API
The SFMC REST API lets you interact with SFMC programmatically — creating contacts, triggering sends, managing journeys, and more. It's the modern API for most SFMC integrations.
Authentication — getting an access token
const response = await fetch(
"https://YOUR_SUBDOMAIN.auth.marketingcloudapis.com/v2/token",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: "YOUR_CLIENT_ID",
client_secret: "YOUR_CLIENT_SECRET",
account_id: "YOUR_MID" // Optional: target BU
})
}
);
const { access_token, rest_instance_url } = await response.json();
// Store these — tokens expire after ~20 minutes
Triggering a transactional send via REST
async function sendTransactionalEmail(accessToken, restUrl, recipient) {
const payload = {
definitionKey: "welcome-email-trigger", // Triggered Send key
recipients: [
{
contactKey: recipient.subscriberKey,
to: recipient.emailAddress,
attributes: {
FirstName: recipient.firstName,
ProductName: recipient.productName
}
}
]
};
const res = await fetch(
`${restUrl}messaging/v1/email/messages/`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
}
);
return res.json();
}
Upserting contacts via REST
async function upsertContact(accessToken, restUrl, contactData) {
const payload = {
items: [
{
keys: { SubscriberKey: contactData.subscriberKey },
values: {
EmailAddress: contactData.email,
FirstName: contactData.firstName,
LastName: contactData.lastName,
Country: contactData.country,
CreatedDate: new Date().toISOString()
}
}
]
};
const res = await fetch(
`${restUrl}hub/v1/dataevents/key:YourDE_ExternalKey/rowset`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
}
);
return res.json();
}
Module 6 — SOAP API
The SOAP API is SFMC's older API — it's more verbose than REST but covers some operations that REST doesn't yet support (particularly around email send management and subscriber data).
Use REST for new integrations wherever possible — it's simpler and more modern. Use SOAP when you need operations not available in REST, such as complex subscriber management, send monitoring, or working with older SFMC components.
SOAP request structure
<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:u="http://docs.oasis-open.org/wss/...">
<s:Header>
<fueloauth xmlns="http://exacttarget.com">YOUR_ACCESS_TOKEN</fueloauth>
</s:Header>
<s:Body>
<RetrieveRequestMsg xmlns="http://exacttarget.com/wsdl/partnerAPI">
<RetrieveRequest>
<ObjectType>Subscriber</ObjectType>
<Properties>SubscriberKey</Properties>
<Properties>EmailAddress</Properties>
<Properties>Status</Properties>
<Filter xsi:type="SimpleFilterPart">
<Property>SubscriberKey</Property>
<SimpleOperator>equals</SimpleOperator>
<Value>customer_12345</Value>
</Filter>
</RetrieveRequest>
</RetrieveRequestMsg>
</s:Body>
</s:Envelope>
Module 7 — Cloud Pages & microsites
Cloud Pages are web pages hosted by SFMC. You can build landing pages, preference centres, unsubscribe pages, registration forms, and full microsites — all with access to SFMC data via AMPscript or SSJS.
Cloud Page types
| Type | Common use |
|---|---|
| Landing page | Post-click pages from email campaigns |
| Microsite | Multi-page web experiences |
| Smart capture form | Data capture forms that write directly to a DE |
| Reference page | Shareable content referenced in emails (e.g. view in browser) |
Form that writes to a Data Extension
%%[
VAR @submitted, @email, @firstName, @result
SET @submitted = RequestParameter("submitted")
IF @submitted == "true" THEN
SET @email = RequestParameter("email")
SET @firstName = RequestParameter("firstname")
/* Upsert to Data Extension */
SET @result = UpsertDE(
"EventRegistrations",
1,
"EmailAddress", @email,
"FirstName", @firstName,
"RegistrationDate", NOW()
)
ENDIF
]%%
<!DOCTYPE html>
<html><body>
%%[ IF @submitted == "true" THEN ]%%
<h1>You're registered!</h1>
<p>We'll be in touch, %%=v(@firstName)=%%.</p>
%%[ ELSE ]%%
<form method="post">
<input type="hidden" name="submitted" value="true" />
<input type="text" name="firstname" placeholder="First name" />
<input type="email" name="email" placeholder="Email address" />
<button type="submit">Register</button>
</form>
%%[ ENDIF ]%%
</body></html>
Module 8 — Custom Journey Builder activities
Custom activities extend Journey Builder with your own logic. You build a small web app that SFMC calls at each execution, enabling you to integrate any external system directly into a journey.
Custom activity flow
A custom activity is a web app hosted externally (or on a Cloud Page) that implements three endpoints SFMC calls at different stages:
| Endpoint | When called | Purpose |
|---|---|---|
/config.json | On load in JB canvas | Defines the activity's UI, name, and argument schema |
/execute (POST) | When a contact reaches the activity | Your business logic — update CRM, fire webhook, etc. |
/publish (POST) | On journey activation | Optional validation before go-live |
Execute endpoint example
// POST /execute — called for each contact
app.post("/execute", async (req, res) => {
const { inArguments, keyValue } = req.body;
// inArguments contains journey data for this contact
const subscriberKey = keyValue;
const email = inArguments[0]?.emailAddress;
const eventType = inArguments[0]?.eventType;
try {
// Your logic here — e.g. update Salesforce CRM
await updateCRM({ subscriberKey, email, eventType });
// Must return 200 for JB to continue the journey
res.status(200).json({ status: "ok" });
} catch (err) {
console.error(err);
// Return 200 even on failure to avoid blocking the journey
res.status(200).json({ status: "error", message: err.message });
}
});
Module 9 — Advanced data modelling
At the advanced level, your data model in SFMC should be a deliberate architectural decision, not an accident. A well-designed model is the difference between an SFMC instance that scales gracefully and one that becomes a maintenance nightmare.
Core data model principles
- One master subscriber DE — a single source of truth for all opt-in subscribers with consistent Subscriber Keys
- Separate transactional data — orders, events, interactions in their own DEs, joined to subscribers via Subscriber Key
- Preference DE — track communication preferences (email types opted in, channel preferences) separately from demographic data
- Segment DEs refreshed on a schedule — don't build audience logic into sends; build it in SQL and refresh via Automation Studio
- Shared DEs at parent BU level — for data that needs to be visible across all business units
Contact Builder relationships
Contact Builder lets you define relationships between your DEs — like a visual ER diagram for SFMC. Link your DEs to the Contact record via Subscriber Key. This powers:
- Journey Builder entry conditions based on cross-DE data
- Unified contact profile view
- AI-powered features (Einstein) that require linked data
Module 10 — Performance & debugging
Debugging SFMC is a skill in itself. Here are the most common issues and how to diagnose them.
AMPscript debugging
%%[
VAR @debugMode
/* Set to 1 in test, 0 in production */
SET @debugMode = 1
]%%
%%[ IF @debugMode == 1 THEN ]%%
<div style="background:#fffbe6;padding:10px;font-family:monospace;font-size:12px;">
<strong>DEBUG:</strong><br/>
SubscriberKey: %%=v(AttributeValue("SubscriberKey"))=%%<br/>
EmailAddress: %%=v(AttributeValue("EmailAddress"))=%%<br/>
Country: %%=v(AttributeValue("Country"))=%%<br/>
LoyaltyTier: %%=v(AttributeValue("LoyaltyTier"))=%%
</div>
%%[ ENDIF ]%%
Common issues and fixes
| Issue | Likely cause | Fix |
|---|---|---|
| AMPscript outputs blank | Field name mismatch (case sensitive in some contexts) | Check DE field name exactly matches AttributeValue() call |
| Lookup returns nothing | No matching row, or wrong DE name | Add RaiseError() to surface issues; verify key values |
| SQL query produces no rows | Date range too narrow, wrong join field | Run manually in Automation Studio with broader dates first |
| Journey not picking up contacts | Entry source DE not refreshed or wrong filter | Check automation is running before journey entry window |
| REST API 401 error | Access token expired (20 min TTL) | Implement token refresh logic; cache with expiry check |
| Emails going to spam | IP not warmed, authentication missing | Check SAP setup, DMARC alignment, list hygiene |