SFMC Practical UI Playbookone section at a time Β· ← β†’ move Β· Space=pause Β· M=mark for review
Left β€” Goal h

P00 β€” Start Here: The Practical UI Playbook

🎯 Why this exists: your last interviews failed on "where / how would you click", not on theory. This playbook fixes exactly that β€” every common task is a screen wireframe + click-recipe + the spoken answer. Learn to narrate the clicks, and the practical questions become easy.


🧠 The one rule that fixes the practical gap

When they ask "how would you do X?", answer in this shape:

"I'd go to  <App>  β–Έ  <menu>  β–Έ  <button> ,
 set <the key field>, then <validate / test>, and verify in <where>."

If you can name the path and the button, you sound like you've done it 100 times. That's the whole game.


πŸ—ΊοΈ "Where does everything live?" β€” the cheat table (memorize this)

Task they ask about Exact location
Create / load a Data Extension Audience Builder β–Έ Contact Builder β–Έ Data Extensions β–Έ Create
Write SQL (incl. Data Views) Automation Studio β–Έ Activities β–Έ SQL Query β–Έ Create (or Email Studio β–Έ Interactions β–Έ Query)
Build an email Email Studio β–Έ Content Builder β–Έ Create β–Έ Email
Preview / Test / proof open the email β–Έ β–Ύ β–Έ Preview and Test
Build a journey Journey Builder β–Έ Create β–Έ Multi-Step
Build an automation Automation Studio β–Έ New Automation
Contact Deletion Contact Builder β–Έ Contacts β–Έ Contact Configuration (enable) β†’ Contacts β–Έ Contact Deletion (run)
Reports / broader engagement Analytics Builder β–Έ Reports (+ Email Studio β–Έ Tracking)
CloudPage / form Web Studio β–Έ CloudPages
Sender auth / IP / warming Setup β–Έ Sender Authentication Package; Email Studio β–Έ Admin β–Έ Send Classifications/Profiles
Switch Business Unit top-right account/BU dropdown

πŸ“š The modules (each = wireframe + click-recipe + spoken answer)

# Screen / scenario Interview question it kills
P01 Navigation & app map "What studios have you used?"
P02 Create & load a DE append vs dedup; prevent dup subscribers; data for builds
P03 SQL Query Activity "Where do you type the SQL for Data Views?"
P04 Build & send an email "How do you take data for builds?"
P05 Preview / Test / Validate campaign validation & testing
P06 Journey Builder JourneyData vs ContactData
P07 Automation Studio automations / scheduling
P08 Contact Deletion "How do you do Contact Deletion?"
P09 Reports & engagement broader engagement report + 730 days
P10 CloudPage form → DE → email InsertDE vs InsertData; form→email
P11 Deliverability & IP warming "How do you do IP warming?"
P12 Troubleshooting & ops Q&A "didn't receive mail / new address in DE"; Lookup family; RaiseError; historical data; talk about projects
P13 Write-it-live drills the SQL join diagram + dedup + anti-join + LookupRows + RaiseError
P14 Deloitte round: CRM writes, suppression, lists, i18n AMPscript→CRM (UpdateSingleSalesforceObject); Auto-Suppression Config; File-Drop→List; All Subscribers vs All Contacts; email→CloudPage; switch/IIF/10-language

πŸ§ͺ How to get REAL hands-on (do this β€” reading isn't enough)

The wireframes here are faithful labeled maps of the screens (not screenshots β€” I can't fabricate those). To build true muscle memory:

  1. Ask your GAP lead for sandbox/QA-BU access and actually click through P02β†’P05 once each. Nothing beats doing it.
  2. Trailhead (free): search the "Marketing Cloud Engagement" trailmixes β€” Email Studio, Contact Builder, Journey Builder, Automation Studio, Query Data with SQL. Several have hands-on challenges.
  3. Narrate while you click: say the breadcrumb out loud each time ("Automation Studio, Activities, SQL Query, Create…"). That's what you'll repeat in the interview.
  4. After each module here, close your eyes and re-draw the screen from memory. If you can sketch it, you can describe it.

πŸ› οΈ How to use this reader

  • Dark by default (πŸŒ“ toggles light). One subsection at a time; ← / β†’ or Prev/Next.
  • / searches everything. ⏱ paces you (default ~2h). Space pauses. β˜† / M marks a screen for review.
  • Bold = key term, green bold (in quotes) = say-this, cyan italic = nuance.

➑️ Next: P01_Navigation_and_App_Map.md

P01 β€” SFMC Navigation & App Map

🎯 Interview questions this answers: - "Walk me through the SFMC interface β€” how do you move around the tool?" - "What Studios and Builders have you worked with?" - "Where do you go to build an email? A journey? An automation? To manage contacts?" - "How do you switch between business units?" - "Where is Setup, and what do you do there?" - "Classic nav vs the newer nav β€” what's the difference?"

You know the theory. This module makes the screen feel familiar so that when they ask "where would you click to do X," you narrate it like someone who logs in every day. Read this with the picture in your head: one horizontal bar across the top, every tool hangs off it.


🧭 Where in the UI

Everything starts at the top navigation bar (the dark horizontal strip across the top of every SFMC page). You hover an app name to drop its menu, then click an item. The literal breadcrumbs you'll be expected to say out loud:

  • Email β†’ Top nav > Email Studio > Email (lands in Email Studio Overview; Content Builder, Subscribers, Tracking, Admin live here)
  • Content β†’ Top nav > Email Studio > Email > Content Builder (shared content library)
  • Journeys β†’ Top nav > Journey Builder > All Journeys
  • Automations β†’ Top nav > Journey Builder > Automation Studio > Overview
  • Contacts/Data β†’ Top nav > Audience Builder > Contact Builder > Data Extensions
  • SMS / Push β†’ Top nav > Mobile Studio > MobileConnect (SMS) or MobilePush
  • Landing pages β†’ Top nav > Web Studio > CloudPages
  • Reports β†’ Top nav > Analytics Builder > Reports
  • Admin β†’ Top-right account name (☰/β–Ύ) > Setup
  • Switch BU β†’ Top-right account/BU dropdown > pick the business unit

πŸ—οΈ Mental model: Studios = channels & content (Email, Mobile, Web). Builders = data & orchestration (Journey, Contact, Analytics, Automation). (Automation Studio sits visually under the Journey Builder menu.)*


πŸ–ΌοΈ Screen map

⊞ SFMC Email Studio β–Ύ Mobile Studio Journey Builder Automation Studio Contact Builder Analytics GAP β–Ύ βš™ EMAIL STUDIO Overview Email Content Builder Subscribers Tracking Β· Admin Hover = open Β· Click = go Each app drops its own menu of tools (Email Β· Content Β· Subscribers…). Automation Studio lives under Journey Builder's menu β€” not its own top-level app. 1 2 3 4 1 App-name dropdown β€” the active app; hover to open its menu. 2 Studio / builder in the menu β€” click the tool you actually want. 3 Business-Unit switcher β€” reloads the app scoped to that BU. 4 Setup (gear) β€” users, roles, BUs, sender auth, API packages.

Wireframe (not a screenshot) β€” layout & labels reflect the live SFMC UI.


πŸ‘‰ Click recipe

  1. Log in at your tenant URL (e.g. https://mcXXXXXX.marketingcloudapps.com) β†’ you land on the dashboard with the top nav bar across the top.
  2. Open a tool: hover the app name in the top nav β†’ its dropdown appears β†’ click the item. - Build/send email: hover Email Studio β†’ click Email β†’ use the inner tabs Overview / Content Builder / Subscribers / Interactions / Tracking / Admin. - Build a journey: hover Journey Builder β†’ click All Journeys (or Journey Builder to open the canvas). - Build an automation: hover Journey Builder β†’ click Automation Studio. - Manage contacts/DEs: hover Audience Builder β†’ click Contact Builder β†’ tab to Data Extensions / All Contacts / Data Designer. - Send SMS/push: hover Mobile Studio β†’ click MobileConnect or MobilePush. - Landing pages/forms: hover Web Studio β†’ click CloudPages. - Reports: hover Analytics Builder β†’ click Reports (or Discover / Dashboards).
  3. Switch business unit: click the account/BU dropdown at the top-right β†’ choose the BU. The whole app reloads scoped to that BU.
  4. Reach Setup: click the account name at the top-right β†’ choose Setup. (Newer nav puts it behind a gear/βš™ icon, same place.) Setup is where Users, Roles, Business Units, Sender Authentication, Installed Packages (API), Data Retention live.

πŸ—£οΈ Say this in the interview

"SFMC is one horizontal top-nav bar β€” every tool hangs off it, and you hover an app to drop its menu. Day to day at GAP I live in Email Studio β€” that's Content Builder for building emails, plus Subscribers, Tracking, and Admin. For data I'm in Audience Builder β†’ Contact Builder, working with Data Extensions and the Data Designer. For batch work β€” imports, SQL query activities, file transfers, scheduled sends β€” I go to Automation Studio, which sits under the Journey Builder menu. For orchestration I use Journey Builder itself off All Journeys. I've also touched Mobile Studio (MobileConnect for SMS, MobilePush), CloudPages under Web Studio for landing pages and code resources, and Analytics Builder for tracking and reports. Admin and config β€” users, roles, sender authentication, API packages β€” is under Setup, which you reach from the account name at the top-right, same dropdown you use to switch business units."

That single paragraph answers "what have you worked with" and "how do you navigate" at once β€” it's the most efficient thing you can memorize.


⚠️ Gotchas / what they probe

  • BU switching loses unsaved work. Changing the BU from the top-right dropdown reloads the whole app scoped to that BU β€” anything unsaved (a half-built email, an unsaved query) is gone. Always save first. Good senior tell: "I confirm which BU I'm in before I send β€” content, DEs, and sends are BU-scoped."
  • Automation Studio is NOT its own top-level app. It lives under the Journey Builder menu. Interviewers like to see if you know that. (Email Studio also has an "Interactions" area for triggered/automated sends β€” don't confuse the two.)
  • Setup is per-account, role-gated. What you see in Setup depends on your role/permissions and which BU/parent account you're in. Some config (e.g., Sender Authentication Package, account-wide settings) is only at the top/parent BU, not child BUs.
  • Classic nav vs new nav. Older tenants show full text labels in the bar; newer ones may collapse some apps behind an app launcher / waffle icon and move Setup behind a βš™ gear β€” the paths and tool names are the same, only the chrome moved. Say: "Whether it's the classic text nav or the newer condensed nav, the tools and breadcrumbs are identical."
  • Audience Builder vs Contact Builder. Contact Builder (data model, DEs, Data Designer) is the everyday one and isn't going anywhere; the separate Audience Builder segmentation product is being retired β€” so if asked, lean on Contact Builder + SQL/Data Filters for segmentation, not legacy Audience Builder.
  • Content Builder is shared, Email Studio "Email" is the sender. Content Builder is the cross-channel content library; the Email app is where classifications, send definitions, and tracking live. Knowing they're distinct-but-linked reads as hands-on.

➑️ Next: P02_Create_Data_Extension.md

P02 β€” Create & Load a Data Extension

🎯 Interview questions this answers: - "Walk me through creating a Data Extension in the UI." - "How do you take the data for the email builds?" - "Explain Append and deduplication in a Data Extension." - "How do you PREVENT deduplication of subscribers?" - "What's the difference between Overwrite, Add & Update, and Append on import?"


🧭 Where in the UI

Create a DE: App Switcher (top-left waffle) β–Έ Audience Builder β–Έ Contact Builder β–Έ Data Extensions β–Έ Create

You can also create DEs from Email Studio β–Έ Email β–Έ Subscribers β–Έ Data Extensions β–Έ Create. Same object, two doors. Contact Builder is the modern path and the one to name in an interview.

On Create, you pick a creation method: - Standard Data Extension β€” empty DE you define field-by-field. - Filtered Data Extension β€” a live subset of a source DE driven by filter rules (no SQL). - From a Template β€” pre-built field sets (e.g. the Salesforce Master / behavioral templates).

Load data into it: - Manual / one-off: open the DE β–Έ Records tab (view rows) and the Import button (push a file). - Automated / production: Automation Studio β–Έ Activities β–Έ Import File Activity (reads a file dropped on the Enhanced FTP / Safehouse, writes to the DE on a schedule).


πŸ–ΌοΈ Screen map

Contact Builder β–Έ Data Extensions β–Έ Create + Create 1 Properties Name Master_Audience External Key auto-generated (or custom) Location /Data Extensions/Audiences β–Ύ Is Sendable ON 2 Is Testable off (needs Sendable) Data Retention Off β–Ύ (Off / 6 months / …) ⚠ Retention DELETES rows on schedule β€” keep Off for a persistent master audience. Fields Name Data Type Length Primary Key Null SubscriberKey Text 50 No 3 EmailAddress EmailAddress 254 No FirstName Text 50 Yes β–² Primary Key = the row-level dedup key. Tick it on SubscriberKey so one shopper = one row. EmailAddress field is required for a Sendable DE (it carries the send address). Send Relationship (Sendable DE only) SubscriberKey Subscriber Key Import File β–Έ Update Type Overwrite β€” truncate DE, load fresh Add and Update β€” upsert by Primary Key 4 Append β€” insert new rows only PK present + Add and Update = no duplicate rows. *Wireframe (not a screenshot).*

πŸ‘‰ Click recipe

A. Create a sendable DE with a Primary Key

  1. App switcher β–Έ Audience Builder β–Έ Contact Builder β–Έ Data Extensions.
  2. Click Create β–Έ choose Standard Data Extension β–Έ Next.
  3. Properties: type a Name (e.g. Master_Audience); leave External Key blank to auto-generate (or set your own for API/AMPscript); pick a Location (folder); tick Is Sendable, then tick Is Testable. Next.
  4. Data Retention Policy: leave Off for a persistent audience, or set a period (e.g. delete records after 6 months). Next.
  5. Fields: add rows. For SubscriberKey set Type = Text, Length = 50, tick Primary Key, untick Nullable. Add EmailAddress (Type = EmailAddress) and content fields (e.g. FirstName, Nullable on). Next.
  6. Send Relationship: in the dropdown choose the field that maps to Subscriber Key β€” pick SubscriberKey so each row is keyed to one contact. Click Complete.

B. Import a file with Add & Update

  1. Open the DE β–Έ Records tab β–Έ click Import.
  2. Choose source = FTP (file on Enhanced FTP) or From File (manual upload). Pick the file + delimiter.
  3. Update Type = Add and Update β–Έ map CSV columns to DE fields β–Έ Next β–Έ Finish. Rows matching the PK update in place; new PKs insert; nothing is deleted.

C. Production version: in Automation Studio, drop the same as an Import File Activity (FTP location + Update Type = Add and Update) inside a scheduled Automation.


πŸ—£οΈ Say this in the interview

"How do you take the data for the email builds?"

"Three patterns. The clean audience usually lives in a sendable Data Extension keyed on SubscriberKey. If marketing wants a slice of that β€” say UK customers who opted in β€” I build a Filtered DE off it, no SQL needed. For anything that needs joins or business logic I write a SQL Query Activity that selects into a target sendable DE, then I send off that DE. The send always points at a sendable DE so every row resolves to a contact."

"Explain append and deduplication."

"On import you pick an Update Type. Add Only / Append just inserts new rows β€” if you append the same person twice with no key, you get duplicate rows. Add & Update is an upsert: it matches on the Primary Key, updates the existing row, and only inserts genuinely new keys β€” that's how I dedupe at the row level. Overwrite truncates the DE and reloads it from scratch."

"How do you PREVENT deduplication of subscribers?" (the trick question)

"Two different levers, and the interviewer is testing whether I conflate them. Row-level dedup inside a DE is controlled by the Primary Key plus Add & Update β€” same PK, one row. But preventing a subscriber from being split or merged incorrectly across sends is about the send relationship: I map a stable, unique SubscriberKey as the relationship field β€” not EmailAddress. Because SubscriberKey is the contact's identity, the same person stays a single subscriber across every send and isn't duplicated or fragmented. So: PK for row dedup, SubscriberKey as the send relationship for subscriber identity."

(GAP voice: "On GAP brand sends our Master_Audience DE is keyed on the customer ID as SubscriberKey, loaded nightly via an Import File Activity on Add & Update, so the loyalty file refreshes in place without ever duplicating a shopper.")


πŸ’» Code (optional)

A SQL Query Activity that upserts into a sendable DE (set the activity's update type to Update, which behaves as Add & Update on the PK):

SELECT
    c.CustomerID  AS SubscriberKey,
    c.Email       AS EmailAddress,
    c.FirstName,
    c.LoyaltyTier
FROM Customers_Raw  AS c
WHERE c.EmailOptIn = 'True'
  AND c.Email IS NOT NULL

πŸ” Line by line: - c.CustomerID AS SubscriberKey β€” alias the stable customer ID to the DE's PK / send-relationship field, so each shopper is one keyed row. - c.Email AS EmailAddress β€” aliases must match the target DE field names exactly or the write fails. - FROM Customers_Raw β€” source DE (a non-sendable staging/audience DE). - WHERE EmailOptIn = 'True' AND Email IS NOT NULL β€” guards against sending to opted-out or null-email rows (null email would block the send for that row). - Target DE + Update Type Update β‡’ matches on SubscriberKey PK, updates existing rows, inserts new ones β€” no duplicates.


⚠️ Gotchas / what they probe

  • Overwrite wipes the DE first. If a job fails mid-load after the truncate, the DE can be left empty β€” never use Overwrite on a live send audience without a backup. Add & Update is the safe default.
  • No Primary Key = silent duplicate rows. Without a PK, every import inserts; running the same file twice doubles your rows and the send goes to the same person twice. The PK is what makes Add & Update actually dedupe.
  • SubscriberKey β‰  EmailAddress. Using EmailAddress as the subscriber key means a person who changes email becomes a new subscriber (and one who shares an email collides). Always key on a stable ID.
  • Retention deletes permanently. A Data Retention Policy silently drops rows (or the whole DE) on schedule β€” great for compliance, dangerous on a master audience. Confirm it's Off for persistent DEs.
  • Is Testable depends on Is Sendable. You can't tick Testable until Sendable is on β€” and a non-sendable DE can't be the target of a send or a test send.
  • External Key is the stable handle for API / AMPscript / SQL β€” once integrations reference it, renaming the DE is fine but changing the key breaks them.

➑️ Next: P03_SQL_Query_Activity.md

P03 β€” Writing SQL: the Query Activity

🎯 Interview questions this answers: - "Where would you type the SQL query for extracting the Data Views?" - "How do you save historical data of customers?" - "What's the difference between Overwrite, Update, and Append on a Query Activity?" - "Can you SELECT and see results on screen in SFMC?" (No β€” results must land in a Data Extension.)


🧭 Where in the UI

You do not type SQL into a free console that prints results. In SFMC you type SQL inside a SQL Query Activity, and the output is written into a target Data Extension (DE). There are two places to build that activity.

Primary path β€” Automation Studio (this is the answer to memorize):

App Switcher (waffle, top-left)
  β†’ Automation Studio
    β†’ Activities  (left-hand tab / Overview > Activities)
      β†’ Create Activity  (the "New Automation Activity" wizard)
        β†’ SQL Query
          β†’ Create
            β†’ ⬛ SQL Query Activity editor  ← YOU TYPE THE SQL HERE

In the editor you fill in: Name β†’ the big SQL text box (the multi-line query editor) β†’ Target Data Extension (chosen from a dropdown; you must have pre-created it) β†’ Data Action = Overwrite / Update / Append β†’ click Validate β†’ Save.

Alternate / legacy path β€” Email Studio:

Email Studio
  β†’ Email  (top nav)
    β†’ Interactions
      β†’ Query
        β†’ Create
          β†’ ⬛ same Query Activity editor (SQL text box + target DE)

Both editors are functionally the same Query Activity. Modern work lives in Automation Studio because that's where you schedule it; Email Studio > Interactions > Query is the older entry point you'll still see referenced.


πŸ–ΌοΈ Screen map

Automation Studio β€Ί Activities β€Ί SQL Query 1 New SQL Query activity Name Daily_Openers_Rollup SQL Query 1 2 3 SELECT SubscriberKey, EmailAddress FROM Master WHERE OptIn = 'Y' 2 β‘‘ Type your SQL here β€” Data Views (_Sent, _Open …) are just table names. Target Data Extension Reporting_Openers β–Ύ 3 β‘’ Where results land (must pre-exist) Data Action Overwrite Update Append 4 β‘£ Overwrite wipes Β· Append keeps history Validate 5 Save β‘€ Validate parses the SQL + maps each column to the target DE *Wireframe (not a screenshot).*

Call-outs: β‘  the SQL text box is the literal answer to "where do you type the query" β€” including queries against Data Views. β‘‘ Target Data Extension is where results land (you can't print to screen). β‘’ Data Action decides whether the target is wiped (Overwrite), matched-and-merged (Update), or added-to (Append).


πŸ‘‰ Click recipe

  1. Pre-create the target DE. Email Studio β†’ Subscribers β†’ Data Extensions β†’ Create. Add columns whose names and data types match the columns your SELECT returns (an aliased SELECT s.SubscriberKey AS SubscriberKey must have a SubscriberKey column waiting in the DE). Mark a Primary Key if you plan to use Update.
  2. Open Automation Studio β†’ Activities tab β†’ Create Activity β†’ SQL Query β†’ Create.
  3. Name the activity (e.g., Daily_Openers_Rollup).
  4. Paste your SQL into the big SQL text box (call-out β‘ ). Data Views are just table names here β€” _Sent, _Open, etc. β€” you don't "import" them.
  5. Choose the Target Data Extension from the dropdown (call-out β‘‘).
  6. Pick the Data Action (call-out β‘’): Overwrite, Update, or Append.
  7. Click Validate. SFMC parses the SQL and confirms every returned column maps to a column in the target DE. Fix mismatches until it passes.
  8. Save the activity.
  9. Add it to an Automation: Automation Studio β†’ New Automation β†’ drag a SQL Query step onto the canvas β†’ select your saved activity (or build it inline). Add a Schedule (e.g., nightly) or a File Drop starting source.
  10. Run Once to test, then Schedule it. Output appears in the target DE; check the Automation's run-history Activity log for row counts / errors.

πŸ’» Code

Example 1 β€” Last-30-day openers (join _Sent + _Open)

SELECT
    s.SubscriberKey,
    s.JobID,
    j.EmailName,
    o.EventDate AS OpenDate
FROM _Open o
INNER JOIN _Sent s
    ON o.SubscriberKey = s.SubscriberKey
   AND o.JobID         = s.JobID
INNER JOIN _Job j
    ON s.JobID = j.JobID
WHERE o.EventDate >= DATEADD(DAY, -30, GETDATE())

πŸ” Line by line: - SELECT s.SubscriberKey, s.JobID, j.EmailName, o.EventDate AS OpenDate β€” the columns we keep; every one of these must exist (by name) in the target DE, which is why o.EventDate is aliased to OpenDate. - FROM _Open o β€” the _Open Data View (system table of open events); o is its alias. Data Views are queried exactly like any DE β€” no import step. - INNER JOIN _Sent s ON o.SubscriberKey = s.SubscriberKey AND o.JobID = s.JobID β€” join opens to the corresponding send. SubscriberKey + JobID is the standard pairing that ties an engagement event back to the exact send job; INNER JOIN keeps only subscribers who actually opened. - INNER JOIN _Job j ON s.JobID = j.JobID β€” _Job holds one row per send job, so this brings in human-readable send metadata like EmailName. - WHERE o.EventDate >= DATEADD(DAY, -30, GETDATE()) β€” limits to opens in the trailing 30 days. SFMC SQL is T-SQL flavored, so DATEADD/GETDATE are valid.

Example 2 β€” Nightly rollup of Data Views into a retained Reporting DE

SELECT
    s.SubscriberKey,
    s.JobID,
    s.EventDate AS SentDate,
    CASE WHEN o.SubscriberKey IS NOT NULL THEN 1 ELSE 0 END AS Opened,
    CASE WHEN c.SubscriberKey IS NOT NULL THEN 1 ELSE 0 END AS Clicked,
    CASE WHEN b.SubscriberKey IS NOT NULL THEN 1 ELSE 0 END AS Bounced
FROM _Sent s
LEFT JOIN _Open o
    ON s.SubscriberKey = o.SubscriberKey AND s.JobID = o.JobID
LEFT JOIN _Click c
    ON s.SubscriberKey = c.SubscriberKey AND s.JobID = c.JobID
LEFT JOIN _Bounce b
    ON s.SubscriberKey = b.SubscriberKey AND s.JobID = b.JobID
WHERE s.EventDate >= DATEADD(DAY, -1, GETDATE())

Run this on a nightly schedule with Data Action = Append into a permanent Reporting_EmailEngagement DE.

πŸ” Line by line: - SELECT s.SubscriberKey, s.JobID, s.EventDate AS SentDate, ... β€” one row per send, flattened with engagement flags; these columns define the schema of the retained Reporting DE. - CASE WHEN o.SubscriberKey IS NOT NULL THEN 1 ELSE 0 END AS Opened β€” turns "did a matching open exist?" into a 1/0 flag. Same pattern for Clicked (_Click) and Bounced (_Bounce). - FROM _Sent s β€” start from every send, so the row exists even when there was no engagement. - LEFT JOIN _Open o ... / _Click c ... / _Bounce b ... β€” LEFT JOIN (not INNER) so a send with zero opens still returns a row with Opened = 0. Each join keys on SubscriberKey + JobID. - WHERE s.EventDate >= DATEADD(DAY, -1, GETDATE()) β€” only yesterday's sends. Because the activity runs nightly with Append, the Reporting DE accumulates history beyond the 6-month Data View window β€” this is the historical-data answer.


πŸ—£οΈ Say this in the interview

"In SFMC you don't get a free SQL console that prints rows. You type the query inside a SQL Query Activity β€” Automation Studio β†’ Activities β†’ Create Activity β†’ SQL Query β†’ Create β€” and the SQL goes in the big SQL text box in that editor. That's exactly where I'd write a query against the Data Views: _Sent, _Open, _Click, _Bounce, _Job are system tables I just SELECT FROM, even though they never show up in the Data Extension list. I pick a pre-built target Data Extension whose columns match my SELECT, set the Data Action to Overwrite, Update, or Append, hit Validate, save, then drop the activity into an Automation and schedule it. The older entry point is Email Studio β†’ Interactions β†’ Query, but I keep it in Automation Studio because that's where I schedule it.

For saving historical customer data: Data Views only keep about 6 months, so at GAP I'd run a nightly Query Activity that reads the Data Views and Appends the flattened engagement into a permanent Reporting DE. That DE never expires, so we keep multi-year send/open/click history for reporting long after the Data Views have rolled off."


⚠️ Gotchas / what they probe

  • Overwrite wipes the target first. Overwrite truncates the entire target DE before writing β€” great for a "current snapshot," catastrophic if you meant to keep history. For historical rollups use Append (or Update with a Primary Key to upsert).
  • Columns must match the target DE. The DE is not auto-created from your SELECT. Returned column names/types must already exist in the target, which is what Validate checks β€” alias everything (AS …) so names line up.
  • Data Views are ~6-month rolling. _Sent, _Open, _Click, _Bounce, _Job, _Unsubscribe, etc. retain roughly 6 months (_Subscribers, _ListSubscribers, _BusinessUnitUnsubscribes are unlimited). If you need history, persist it to a retained DE β€” the Data View itself silently drops old rows.
  • No INSERT / UPDATE / DELETE on the source. Query Activity SQL is SELECT-only. You can't mutate Data Views or any DE in the query body; the only "write" is the Data Action against the target.
  • You can't SELECT to screen. There's no result grid in the activity β€” results must land in a DE. (Query Studio is a separate AppExchange-style tool some orgs add for ad-hoc previews; the native activity always writes to a DE.)
  • Runtime cap (~30 min). A Query Activity that runs longer than roughly 30 minutes is cancelled. Probe answer: narrow the date WHERE, index via Primary Keys, avoid full-table Update checks on huge targets, and split heavy jobs into smaller scheduled passes.
  • Child BU prefix. From a child Business Unit, prefix shared Data Views with Ent. (e.g., Ent._Open) β€” except _Job, which stays BU-specific.

➑️ Next: P04_Build_and_Send_Email.md

P04 β€” Build & Send an Email

🎯 Interview questions this answers: - "Walk me through building an email and sending it end-to-end." - "How do you take / pull the data for your email builds?" (the audience DE) - "In the send flow, where do you pick the audience vs. the exclusions?" - "What is a Send Classification, and when is it Commercial vs. Transactional?" - "What's a Sender Profile vs. a Delivery Profile?" - "Template vs. HTML vs. Text β€” when do you pick each in Content Builder?"


🧭 Where in the UI

Two distinct things happen here, in two distinct tools:

A) Build the email β€” Content Builder (inside Email Studio):

App Switcher (waffle, top-left)
  β†’ Email Studio
    β†’ Content  (top nav)  ← this is Content Builder
      β†’ + Create  (top-right button)
        β†’ Email          ← the message type
          β†’ choose creation mode:
              β€’ Template      (drag-and-drop into a layout)
              β€’ HTML          (paste/write your own HTML + AMPscript)
              β€’ Text Only     (plain-text fallback)
              β€’ Existing Email (clone a previous build)
            β†’ ⬛ Email content editor  ← YOU EDIT BLOCKS / CODE HERE

B) Send the email β€” the Guided Send wizard: From the open email (or from the email's row), click Send. The wizard runs four numbered steps:

1  Define Properties   β†’ subject, preheader, From (Sender Profile),
                          Send Classification (Commercial / Transactional)
2  Select Audience     β†’ Targeted = sendable DE / list / filtered
                          Excluded & Suppressed = exclusions/suppression
3  Configure Delivery  β†’ send now or Schedule, throttling, tracking
4  Review and Send     β†’ preview, fix errors, confirm, Send

Memorize the two-part split: Content Builder builds the asset; the Send wizard delivers it. The interviewer's "where do you pick the audience" lives in Step 2 (Select Audience), and "Commercial vs. Transactional" lives in Step 1 (Define Properties β†’ Send Classification).


πŸ–ΌοΈ Screen map

Content Builder Β· Email Editor Email > Welcome_Series > Order_Confirmation Send β–Έ Content Blocks </> HTML paste raw markup ΒΆ Free form rich-text + AMPscript Image from Content area Button styled CTA + link Templates Saved Content drag β†’ drop onto canvas β†’ Email canvas β€” drop & edit blocks IMAGE BLOCK hero / header banner FREE-FORM BLOCK Hi %%FirstName%%, your order %%OrderID%% has shipped. %%[ AMPscript personalization ]%% BUTTON BLOCK Track my order β–Έ Block settings align padding link / alt 1 2 *Wireframe (not a screenshot) β€” layout reflects the live SFMC UI.*
Guided Send wizard β€” 4 steps 1 Define Properties Email + Name Subject / Preheader Folder location 2 Select Audience = your sendable Data Extension Targeted DE / list Excluded & Suppressed 3 Configure Delivery Send now / Schedule Throttling Sender Profile + Send Classification 4 Review & Send Preview Fix errors Send Step 2 targets a SENDABLE Data Extension built earlier via SQL Query, Import, or Data Filter. Step 3 binds the Sender Profile (from-name/address) and Send Classification (CAN-SPAM footer + delivery profile). 3 4 5 *Wireframe (not a screenshot) β€” layout reflects the live SFMC UI.*

Call-outs: β‘  the left content tree holds your reusable Content Blocks (Free Form, HTML, Image, Button) and saved Templates β€” drag these onto β‘‘ the canvas, where you edit copy, personalization strings, and AMPscript. β‘’ the Send button launches the Guided Send wizard. In the wizard, Step 2 Select Audience is where "where do you pick the audience" is answered β€” Targeted vs. Excluded & Suppressed β€” and Step 1 Define Properties holds the Send Classification (Commercial vs. Transactional). The dotted bottom band shows the three ways the sendable DE gets populated before you ever open the wizard.


πŸ‘‰ Click recipe

Part A β€” Build the email

  1. Open Content Builder: App Switcher β†’ Email Studio β†’ Content (top nav).
  2. Click + Create (top-right) β†’ Email.
  3. Pick the creation mode on the "Create Email" screen: - Template β€” start from a drag-and-drop layout; best for marketers / repeatable brand emails. - HTML β€” paste or write your own responsive HTML; this is where I drop hand-built markup + AMPscript for full control. - Text Only β€” plain-text version (also auto-generated as the text fallback of any HTML email). - Existing Email β€” clone a prior build to save time.
  4. Fill Define Email Properties: Name (internal), Subject, Preheader, and the folder to save into. Click Next.
  5. Edit content on the canvas: - Drag Content Blocks from the left tree onto the layout (Image, Free Form/Text, Button, HTML). - Double-click a block to edit. For an HTML block, switch to the code view and write markup. - Add personalization: type %%FirstName%% for a profile/DE attribute, or open an AMPscript block: %%[ ... ]%% for logic (lookups, conditionals).
  6. Save. Use Preview and Test (covered in P05) before you ever send.

Part B β€” Send the email (Guided Send wizard)

  1. With the email open, click Send. The wizard opens at Step 1.
  2. Step 1 β€” Define Properties: confirm Subject + Preheader, set the From information by choosing a Sender Profile, and choose the Send Classification (which bundles Sender Profile + Delivery Profile and sets Commercial vs Transactional). Click Next.
  3. Step 2 β€” Select Audience: - From the All Audience Types dropdown, find your sendable Data Extension (or list / filtered DE) and drag it into the Targeted area. Watch Total Targeted populate with the row count. - Drag any audiences you want gone into Excluded and Suppressed (and/or apply a suppression list). Add CC/BCC if needed. Click Next.
  4. Step 3 β€” Configure Delivery: choose Send Now or Schedule (date/time). Set Send Throttling if it's a large send, and confirm tracking options (Track Clicks, Retain Send Log Data). Click Next.
  5. Step 4 β€” Review and Send: preview the rendered email, review every setting, fix any flagged errors, tick the confirmation checkbox, and click Send.

πŸ—£οΈ Say this in the interview

"There are two halves: build in Content Builder, then send through the Guided Send wizard.

To build, I go Email Studio β†’ Content β†’ + Create β†’ Email, and pick the mode: Template for drag-and-drop brand emails, HTML when I want full control β€” that's my default at GAP because I hand-write responsive HTML and drop in AMPscript for personalization and conditional content β€” or Text Only for the plain-text fallback. On the canvas I drag content blocks, edit copy, and add personalization strings like %%FirstName%% or AMPscript %%[ ... ]%% lookups. I always Preview and Test before sending.

To send, I hit Send and the wizard walks four steps. Step 1, Define Properties β€” subject, preheader, the From via a Sender Profile, and the Send Classification, which is the bundle of Sender Profile plus Delivery Profile and decides Commercial vs Transactional. Step 2, Select Audience β€” this is where I pick the audience: I drag my sendable Data Extension into Targeted and drop anything I want removed into Excluded and Suppressed. Step 3, Configure Delivery β€” send now or schedule, throttling, tracking. Step 4, Review and Send β€” preview, fix errors, confirm, send.

On how I take the data for the build: the audience is always a sendable Data Extension, and I build it one of three ways. Most often a SQL Query Activity in Automation Studio that joins or filters my source data into the audience DE β€” for example, customers who purchased in the last 30 days. Sometimes an import / File Drop of a CSV into a DE for a one-off list. And for simple segments, a Data Filter that produces a filtered DE. Whichever way, it lands in a DE that has a subscriber-key / email field and is marked sendable, so it shows up in Step 2 of the wizard."


⚠️ Gotchas / what they probe

  • Audience vs. exclusions are two different boxes in one step. Both live in Step 2 (Select Audience): Targeted = who gets it, Excluded and Suppressed = who's pulled back out. Exclusions are applied on top of the targeted set at send time β€” saying "I filter them out in SQL" is fine, but they want to hear you also know the wizard's Excluded and Suppressed box and suppression lists.
  • Send Classification = Commercial vs. Transactional, and it changes the rules. A Commercial classification enforces the unsubscribe / CAN-SPAM footer and respects the master unsubscribe list. Transactional (order confirmations, password resets) can skip the unsubscribe requirement and isn't subject to the same opt-out β€” abusing Transactional for marketing is a compliance trap. Classification bundles the Sender Profile (the friendly From name/address) and the Delivery Profile (IP/authentication, footer/header settings).
  • Sender Profile β‰  Delivery Profile. Sender Profile = who it's from (display name + reply address). Delivery Profile = how it leaves (sending IP, SAP/authentication, header & footer). The Send Classification stitches them together so you pick one named thing.
  • "Sendable" is a DE property, not a guess. A Data Extension only appears in Step 2 if it's marked Is Sendable with a relationship mapping its key (e.g., EmailAddress/SubscriberKey) to the Subscriber relationship. A correct, populated DE that isn't sendable simply won't show up β€” a common "why can't I select it" trap.
  • Always Preview and Test before Send (P05). They probe for discipline: send a test send to a seed/test DE, validate AMPscript renders, check personalization didn't blank out, and confirm links. Test sends don't count against the audience and don't fire on the real list.
  • Throttling & timing on big sends. Large sends should be throttled or scheduled off-peak; an unthrottled blast can hurt deliverability and trip the per-hour ceiling.
  • Sender Authentication / footer. If the From domain isn't authenticated (SPF/DKIM via a Sender Authentication Package), deliverability tanks β€” a frequent follow-up to "what could go wrong on send."

➑️ Next: P05_Preview_Test_Validate.md

P05 β€” Preview, Test & Validate

🎯 Interview questions this answers: - "How do you test an email before you send it?" - "How do you confirm your AMPscript / dynamic content actually renders for a real subscriber?" - "It worked in preview but the live send went out blank / wrong β€” what happened?" - "How do you check links and tracking before a send?" - "How do you make sure it renders in Outlook and dark mode?" - "How do you verify the audience is right before sending?" - "How do you promote a tested email from QA to production?"


🧭 Where in the UI

There is no separate "test" app. Testing lives on the email itself, inside Content Builder.

App Switcher (waffle, top-left)
  β†’ Content Builder
    β†’ find your email in the folder tree
      β†’ hover the row β†’ click the β–Ύ down-arrow (or open the email, top-right β–Ύ)
        β†’ Preview and Test          ← THE TESTING SCREEN
            β”œβ”€ Subscribers   tab   β†’ Subscriber Preview (render vs a real DE row)
            β”œβ”€ Test Send     tab   β†’ Send Preview / Proof (up to 5 inboxes or a test DE)
            └─ Validate            β†’ link / content checks (Content Detective, link validation)
            └─ Litmus icon         β†’ cross-client render previews (if provisioned)

Where "Send Proof" is: it's the Test Send tab inside Preview and Test (older accounts call the button Send Proof; newer UI calls it Send Test β†’ Confirm and Send). You enter up to five email addresses or point it at a test Data Extension, then choose whose data to merge in via Change (selected subscriber / entire list or DE / recipient test DE).

The same Preview and Test screen is reachable from inside a Journey Builder email activity and from Email Studio, but Content Builder is the canonical place to memorize.


πŸ–ΌοΈ Screen map

Preview and Test β€” Welcome_Email_v3 Content Builder Subscriber Preview Test Send Preview as: row from Send DE SubscriberKey 7741 β€” Akash Β· SC-20 β–Ύ Pick a real subscriber to merge actual data (not a placeholder). 1 Rendered preview β€” AMPscript resolved Hi Akash, your code: SC-20 [ dynamic hero block ] Shop now β–Έ %%FirstName%% β†’ Akash 2 Test Send β€” send a proof To address (up to 5): me@gap.com; qa@gap.com Send β†’ Confirm and Send 3 Validate links Β· subject Content Detective 4 *Wireframe (not a screenshot).*

Call-outs: β‘  Subscriber selector β€” pick a real row from the sendable Data Extension so personalization/AMPscript resolve against actual data. β‘‘ Rendered preview pane β€” the email as that subscriber sees it, with %%FirstName%%, AMPscript, and dynamic blocks already merged. β‘’ Render testing (Litmus / Email on Acid) for Outlook, Gmail, Apple Mail, mobile, and dark mode. β‘£ Validate runs link/subject checks and Content Detective (spam-trigger words). β‘€ Send Preview / Proof to up to five inboxes or a test DE, then Confirm and Send.


πŸ‘‰ Click recipe

Do these in order, before any production send.

  1. Open Preview and Test. Content Builder β†’ email row β†’ β–Ύ β†’ Preview and Test.
  2. Subscriber Preview against a real row. On the Subscribers tab, select a subscriber from the sendable DE (call-out β‘ ). Confirm the preview pane shows the resolved values β€” real first name, real order number, dynamic blocks populated β€” not the raw %%…%% tags or AMPscript.
  3. Preview several edge-case rows. Swap the subscriber: a row with empty optional fields, a long name, a different locale/segment. This is how you catch AMPscript that only works for "happy path" data.
  4. Check links and UTMs. Hover/click each link in the preview; confirm destination URLs are correct and that UTM parameters (utm_source, utm_medium, utm_campaign) are present and consistent. Run Validate so SFMC flags broken/empty links and Content Detective surfaces spam-trigger words.
  5. Run render tests. Open the Litmus / Email on Acid panel and review Outlook (desktop), Gmail, Apple Mail, mobile, and dark mode. Outlook (Word rendering engine) and dark-mode color inversion are the usual breakers.
  6. Verify audience row count. Before sending, check the target audience/DE record count matches the expected segment size (Email Studio β†’ the DE β†’ Records, or the send wizard's audience count). A count of 0 or a count 10x too big = stop.
  7. Confirm the View As Web Page (VAWP) link works and that the email has a valid Subject, preheader, From name/address, and unsubscribe / physical-address footer (CAN-SPAM).
  8. Send a Proof. Test Send tab β†’ enter up to five addresses (yourself + QA) or a test DE β†’ Change to merge real subscriber data (not generic placeholders) β†’ Send Test β†’ Confirm and Send.
  9. Inspect the proof in a live inbox on desktop and phone β€” this catches things preview never will (image blocking, clipping in Gmail, footer truncation).
  10. Only then send / activate. Promote from the QA Business Unit to production (see Gotchas β†’ deployment).

βœ… QA checklist

Memorize this as P-R-A-V-D: Preview, Render, Audience, Validate links, Deploy.

  • [ ] Subscriber Preview run against a real sendable-DE row β€” personalization + AMPscript resolve (no raw %%…%%).
  • [ ] Previewed edge-case rows (empty field, long value, other segment).
  • [ ] Links + UTMs correct; Validate passed (no broken links, Content Detective clean).
  • [ ] Render tests pass: Outlook, Gmail, Apple, mobile, dark mode.
  • [ ] Audience row count matches expected segment (not 0, not wildly off).
  • [ ] Subject, preheader, From, VAWP, unsubscribe + physical address all present.
  • [ ] Proof sent to inboxes/test DE and opened on real desktop + mobile.
  • [ ] Promoted QA BU β†’ production via Deployment/Package Manager with correct naming + folder.

πŸ—£οΈ Say this in the interview

"Before any send I open the email in Content Builder, click the β–Ύ and go to Preview and Test. The first thing I do is Subscriber Preview against a real row from the sendable Data Extension β€” that's the only way to confirm my AMPscript and dynamic content actually resolve, because the preview merges that subscriber's real data, not a placeholder. I deliberately preview a few edge-case rows too β€” someone with a blank optional field, a long name β€” because that's where AMPscript quietly breaks.

Then I Validate to catch broken links and check my UTMs are tagged consistently, and I run render tests in Litmus / Email on Acid across Outlook, Gmail, Apple Mail, mobile and dark mode β€” Outlook and dark-mode inversion are the usual culprits. I confirm the audience row count matches the segment we expect, then I Send a Proof to myself and QA β€” pulling real subscriber data, not generic placeholders β€” and I open it on an actual phone and desktop.

The gotcha I always watch for is 'it worked in preview but the send went out blank.' That almost always means the AMPscript referenced an attribute that exists for the preview row but is empty or missing on other subscribers, or it read from a DE the send context couldn't actually resolve. Preview shows you one row looking perfect; the live send hits every row. So I never trust a single preview β€” I check multiple rows and always confirm with a real proof.

At GAP, once QA signs off I promote from our QA / staging Business Unit to production using Deployment Manager (or Package Manager) so the exact tested asset moves over β€” never hand-rebuilt in prod β€” and everything follows our naming and folder governance so it's auditable."


⚠️ Gotchas / what they probe

  • Preview uses ONE real row β€” the send hits all of them. Subscriber Preview renders against the single subscriber you picked. Looking perfect there proves nothing about the other 2 million rows. Always preview multiple/edge-case rows and send a real proof.
  • "It worked in preview but the send went out blank." The classic AMPscript trap. Causes: (1) the attribute is populated for the preview row but NULL/empty for most subscribers; (2) AMPscript Lookup/AttributeValue points at a field or DE that resolves in preview context but not in send context (sendable-DE relationship / subscriber relationship missing); (3) a fallback default was never set (%%[ if empty(@fn) then ... ]%%). Fix: defensive AMPscript with defaults, and validate against rows that are missing data β€” not just complete ones.
  • Proof β‰  real audience data unless you tell it so. On Test Send, if you don't use Change to merge real subscriber data, the proof can render with placeholders and mask exactly the personalization bug you're hunting. Always proof with a real row / test DE.
  • Outlook and dark mode are the renderers that break. Outlook uses Word's engine (background images, padding, rounded corners fail); dark mode inverts colors and can hide logos/text. Subscriber Preview alone won't show these β€” that's what Litmus / Email on Acid is for.
  • Audience count is a guardrail. A row count of 0 (broken filter / empty DE) or 10x expected (un-deduped join, wrong DE) is the cheapest catch before a send. Verify it every time.
  • VAWP / unsubscribe / physical address. A missing unsubscribe link or physical mailing address is a CAN-SPAM violation; a broken View As Web Page link is an embarrassing but common miss. Validate confirms these.
  • There is no true "sandbox" in SFMC. Marketing Cloud has no sandbox the way core Salesforce does β€” every Business Unit is production. Teams designate a QA / staging BU for testing, then promote to the production BU. Don't say "I deploy to a sandbox"; say "I test in our QA Business Unit and promote to prod."
  • Deploy the tested asset β€” don't rebuild it. Promote QA β†’ prod with Deployment Manager (creates a JSON snapshot of journeys/DEs/automations to import into the target BU) or Package Manager (bundles journeys/automations/campaigns). Rebuilding by hand in prod reintroduces the exact errors QA just caught. Follow naming + folder governance so the promoted asset is traceable and the right version is the one that ships.

➑️ Next: P06_Journey_Builder_UI.md

One-line summary: Validate every email by previewing AMPscript/dynamic content against a real sendable-DE row, checking links/UTMs and Outlook/dark-mode renders, confirming the audience count, sending a proof with real data, then promoting the tested asset from a QA Business Unit to production via Deployment/Package Manager β€” because "it worked in preview" only proves one row worked.

P06 β€” Journey Builder (build a journey)

🎯 Interview questions this answers: - "Walk me through building a multi-step journey in the UI β€” from entry source to activation." - "What's the difference between Journey Data and Contact Data?" (the frozen entry-time snapshot vs the live contact record) - "How do you reference each in a message?" ({{Event."DEkey".Field}} vs {{Contact.Attribute."Set"."Field"}}) - "Can you edit a journey that's already running?" (No β€” you create a new version.) - "How does a Decision Split decide the path?" (Top-to-bottom on contact attributes; first match wins.)


🧭 Where in the UI

App Switcher (waffle, top-left)
  β†’ Journey Builder
    β†’ Journeys  (list of all journeys)
      β†’ Create New Journey  (top-right button)
        β†’ Multi-Step Journey   ← pick this (not Single Send / Transactional)
          β†’ ⬛ Journey Canvas   ← Entry Source box + drag-and-drop activities

The canvas is the workspace. The Entry Source box sits at the top-left of the flow; the activity palette runs down the left edge; you drag activities onto the canvas to the right of the entry source. Validate and Activate live in the top-right toolbar.


πŸ–ΌοΈ Screen map

Advisor_Onboarding_Welcome Version 1 Validate Activate 4 5 ACTIVITIES Email SMS Wait Decision Split Engagement Split Join Update Contact 2 Entry Source Data Extension 1 Email Welcome Wait 2 days Decision Split Tier = Gold? 3 Yes Email 2 VIP offer No Exit leaves journey Drag activities from the palette onto the flow, then connect them.

Wireframe (not a screenshot).

Call-outs: 1 Entry Source feeds contacts in (here a Data Extension). 2 Decision Split branches on a contact attribute, evaluated top-to-bottom. 3 Update Contact writes a value back to the contact record at that step. 4 The activity palette you drag from. 5 Validate, then Activate.


πŸ‘‰ Click recipe β€” build & activate an advisor-onboarding journey

  1. Journey Builder β†’ Journeys β†’ Create New Journey β†’ Multi-Step Journey. Rename it top-left, e.g. Advisor_Onboarding_Welcome.
  2. Click the Entry Source box β†’ pick the source: - Data Extension β€” contacts enter when they land in the chosen DE (batch / scheduled). You set the DE, a filter (e.g. Status = New), and an entry schedule (how often Journey Builder checks the DE for new rows). - API Event β€” a real-time POST from an external system fires entries (good for event-driven onboarding). You define an event definition key. - Salesforce Data β€” entries driven by an SFDC object via Marketing Cloud Connect; unlocks related-object data in splits. - CloudPage β€” a form submission on a CloudPage injects the contact.
  3. Set Entry Settings β€” re-entry mode (No re-entry / Re-entry anytime / Re-entry only after exiting), and a contact evaluation filter so only qualifying contacts proceed.
  4. Drag activities from the left palette onto the canvas, in order: - Email β€” pick the welcome email; map it to use Journey or Contact data (see below). - Wait β€” e.g. wait 2 days. - Decision Split β€” branch on AdvisorTier (Yes = Gold β†’ VIP email, No = standard track). Configure the attribute (next section). - Engagement Split β€” branch on did-they-open/click the prior email. - Join β€” merge the branches back into one path so downstream steps run once. - Update Contact β€” stamp a value back (e.g. Onboarded = true).
  5. Validate (top-right). Fix any flagged steps (unconfigured email, missing wait duration, dangling branch).
  6. (Optional) Test Mode β€” run a small set of test contacts through the flow without sending live; confirm path logic and personalization before going live.
  7. Activate. The journey goes live and contacts start entering per the entry source.

🧠 JourneyData vs ContactData

When Journey Builder injects a contact, it takes a snapshot of the entry Data Extension row β€” that snapshot is Journey Data and it is frozen for the contact's entire trip through the journey. Contact Data is the live contact record from the Contact Builder data model, re-read at each step.

Journey Data (Event) Contact Data (Contact)
Source Entry DE row, captured at injection Live attributes in the Contact model
When evaluated Frozen at entry time Live β€” read fresh at each step
Changes mid-journey? No, never Yes, reflects current values
Reference syntax {{Event."DEkey".Field}} {{Contact.Attribute."Set"."Field"}}
Example {{Event."DEAudience-…"."FirstName"}} {{Contact.Attribute."Loyalty"."Tier"}}

Reference syntax in a message:

Journey Data (frozen snapshot of the entry DE):
  {{Event."DEAudience-a33c4f76-f98c-1210-8205-df62800d6778"."FirstName"}}

Contact Data (live attribute set from the contact model):
  {{Contact.Attribute."Loyalty"."Tier"}}

Contact Key shortcut:
  {{Contact.Key}}

When to use which: - Journey Data β€” when you want the value as it was at entry. The advisor's region, the order they placed, the tier that qualified them. Even if it later changes, the message should reflect entry-time state. - Contact Data β€” when the value can change mid-journey and you want the latest. A Decision Split that re-checks Tier after a Wait, or an email that should show today's points balance. - Decision Splits can read either β€” pick Contact Data when the branch must reflect changes that happened during the journey (e.g. after an Update Contact step), Journey Data when the branch should lock to entry-time conditions.


πŸ—£οΈ Say this in the interview

"At GAP I build advisor-onboarding journeys in Journey Builder. I start with Create New Journey β†’ Multi-Step, click the Entry Source and usually pick a Data Extension with an entry schedule β€” or an API Event when onboarding is event-driven. Then I drag activities onto the canvas: a welcome Email, a Wait, a Decision Split on advisor tier, sometimes an Engagement Split on open/click, a Join to merge branches, and an Update Contact to stamp the contact record. Then Validate, run a quick Test Mode pass, and Activate."

"The one I always call out is Journey Data versus Contact Data. Journey Data is a frozen snapshot of the entry DE row taken when the contact is injected β€” I reference it as {{Event."DEkey"."Field"}}. Contact Data is the live contact record, re-read at each step, referenced as {{Contact.Attribute."Set"."Field"}}. So if I want the tier that qualified them at entry, I use Journey Data; if I want the current tier after they might have upgraded mid-journey, I use Contact Data."

"And you can't edit a running version β€” if the journey is live I create a new version, build my changes there, then activate it. The old version moves to Finishing so existing members complete it, then Stopped, and new entrants pick up the new version."


⚠️ Gotchas / what they probe

  • You cannot edit a running journey. The only way to change a live journey is to create a new version. Existing members keep flowing through the old version while you build; once you activate the new one, the old goes Finishing β†’ Stopped.
  • Re-entry modes matter. No re-entry (a contact can be in once), Re-entry anytime (can re-qualify while still in), Re-entry only after exiting. Pick wrong and contacts either get stuck out or double-messaged. You also can't push current members through newly added steps unless your settings allow re-entry.
  • Entry DE schedule. A Data Extension entry source only injects on its schedule β€” contacts don't enter the instant a row appears unless the schedule runs. If "no one's entering," check the entry schedule and the entry filter first.
  • Frozen vs live is a classic trap. If a personalization value looks stale, you probably used Journey Data ({{Event…}}) when you needed Contact Data ({{Contact.Attribute…}}), or vice-versa. Journey Data never updates after entry.
  • Decision Split order. Paths evaluate top-to-bottom; first match wins, and a contact who matches multiple criteria takes the first one. Put the most-specific / highest-priority path on top. Up to 20 paths.
  • Validate before Activate. Validation catches unconfigured emails, empty waits, and dangling branches β€” fix these before activation, and use Test Mode to confirm the path logic with sample contacts.

➑️ Next: P07_Automation_Studio_UI.md

P07 β€” Automation Studio (build an automation)

🎯 Interview questions this answers: - "Walk me through building a nightly automation in Automation Studio." - "What's the difference between a Schedule and a File Drop starting source?" - "How do you know an automation failed? Where do you watch runs?" - "What activities can you chain together, and in what order for a campaign send?" - "How does a Verification activity protect a send?" - "Run Once vs Schedule β€” when do you use each?" - "Automation Studio vs Journey Builder β€” when do you reach for which?"


🧭 Where in the UI

App Switcher (waffle, top-left)
  β†’ Automation Studio
    β†’ Overview  (lists all automations + their status)
      β†’ New Automation  (top-right button)
        β†’ ⬛ Automation canvas
             β€’ Starting Source box  (top of canvas)  ← Schedule OR File Drop
             β€’ Steps row            (Step 1, Step 2, … left-to-right)
             β€’ Activities palette   (left rail β€” drag onto a step)

The canvas has three zones: a Starting Source box at the top (what kicks the run off), a left-to-right row of Steps (each step runs to completion before the next starts), and the Activities palette on the left that you drag activities from onto a step. A step can hold multiple activities, which run in parallel within that step β€” so put things that must happen in order in separate steps.


πŸ–ΌοΈ Screen map

Automation Canvas β€” Nightly_Campaign_Prep Starting Source Schedule βœ“ Daily @ 02:00 File Drop on FTP arrival 1 Steps (run left β†’ right, in sequence) Step 1 Import File β†’ Staging DE Step 2 SQL Query β†’ Audience DE Step 3 Verification row-count guard 3 Step 4 Send Email 2 Activities palette β–Έ File Transfer β–Έ Import β–Έ SQL Query β–Έ Filter β–Έ Script β–Έ Send Email β–Έ Data Extract β–Έ Verification β–Έ Wait drag any item onto a step ↑ 2 Run Once Schedule 4 Run Once = dry-run Β· Schedule = activate recurring run Activity Monitor Last run: Complete βœ“ Running β–Έ Β· Error βœ• Β· Skipped β€Ό click a run β†’ drill into the failing step 5 *Wireframe (not a screenshot).*

Call-outs: β‘  the Starting Source box β€” pick Schedule (time-based) or File Drop (fires when a file lands on Enhanced FTP). β‘‘ a Step holds one or more activities; steps run left-to-right in sequence. β‘’ the Verification activity sits between SQL and Send as a guard β€” if the audience is empty/too small/too large it halts before the email goes out. β‘£ Run Once tests the whole chain immediately; Schedule activates the recurring run. β‘€ the Activities palette you drag from. β‘₯ the Activity Monitor tab β€” where you watch live runs and drill into Complete / Error / Skipped status.


πŸ‘‰ Click recipe

Goal: a nightly file-drop β†’ import β†’ SQL β†’ verify β†’ send automation (the classic campaign-prep pattern).

  1. Pre-build the pieces you'll reference. A Staging DE (matches the CSV layout), an Audience DE (matches your SQL SELECT), and the email + send definition. Activities reference existing objects β€” the automation just orchestrates them.
  2. Automation Studio β†’ Overview β†’ New Automation. Name it Nightly_Campaign_Prep.
  3. Set the Starting Source (call-out β‘ ). Click the box at the top: - Schedule β†’ set Repeats = Daily, Time = 02:00, time zone, start/end dates. Use this when the vendor file lands on a reliable clock. - File Drop β†’ choose the Enhanced FTP location and a filename pattern (e.g. nightly_*.csv). The automation fires the moment a matching file arrives β€” no clock. Use this when the upstream system's timing is unpredictable.
  4. Step 1 β€” drag Import File from the palette (call-out β‘€). Point it at the dropped file β†’ target Staging DE β†’ action Overwrite (fresh load each night).
  5. Step 2 β€” drag SQL Query. Select your saved query that reads the Staging DE (joins/segments) and writes the Audience DE. New step = runs after import finishes.
  6. Step 3 β€” drag Verification (call-out β‘’). Set a rule on the Audience DE, e.g. Stop the automation if row count is less than 1 (or greater than your safety ceiling). This guards the send.
  7. Step 4 β€” drag Send Email. Pick the email + the Audience DE as the sendable target. Because it's the last step, it only runs if Verification passed.
  8. Turn on notifications. Automation settings β†’ Notifications: add your email for Run Complete, Error, and Skip. (Skip fires when a scheduled run is missed β€” e.g. the previous run was still going.)
  9. Save, then click Run Once (call-out β‘£) to dry-run the whole chain. Watch it in Activity Monitor.
  10. Activate the Schedule once the test run is clean. From now on it runs nightly (or on every file drop).
  11. Watch it in the Activity Monitor tab (call-out β‘₯): each run shows Running β†’ Complete / Error / Skipped; click a failed run to see which step / activity errored and the message (e.g. import column mismatch, SQL timeout).

πŸ—£οΈ Say this in the interview

"For a nightly campaign-prep job I build it in Automation Studio β†’ New Automation. First I set the Starting Source. If the vendor drops a file on a dependable clock I use a Schedule β€” say daily at 2 AM. If their timing is unreliable I use File Drop, which points at an Enhanced FTP folder and fires the instant a matching file lands, so I'm not guessing when to run.

Then I lay out Steps left to right β€” they run in sequence. Step 1 Import File loads the CSV into a staging DE with Overwrite. Step 2 a SQL Query segments that into my Audience DE. Step 3 β€” and this is the part I always include β€” a Verification activity that halts the automation if the audience is empty or unexpectedly huge, so a bad data night can never trigger a bad send. Step 4 is the Send Email, which only runs because Verification passed. I'll add File Transfer up front if I need to decrypt/move the file, or a Data Extract plus Script** at the end if I'm exporting results.

I Run Once to dry-run it, watch it in the Activity Monitor β€” that's where I see Running, Complete, Error, or Skipped per run and can click into the exact step that failed β€” and I turn on run / error / skip notifications so I'm emailed the moment anything breaks. Then I activate the schedule.

Automation Studio vs Journey Builder in one line: Automation Studio is batch / data-and-ops orchestration on a schedule (ETL, imports, SQL, scheduled sends); Journey Builder is per-contact, event-triggered, 1:1 lifecycle messaging that waits and branches on individual behavior. At GAP I use Automation Studio to prepare and feed the audience, and Journey Builder to engage each customer once they're in it."


⚠️ Gotchas / what they probe

  • File Drop vs Schedule is the classic question. Schedule = time-based (cron-like; runs even if no new data). File Drop = event-based on Enhanced FTP (runs only when a matching file arrives; ignores the clock). File Drop avoids the race where your 2 AM run beats a late file. Mention Enhanced FTP and the filename pattern.
  • Steps are sequential; activities inside one step are parallel. Put anything order-dependent (import then query then send) in separate steps. Two activities dropped on the same step do not guarantee order.
  • Verification guards a bad send. Without it, an empty or broken import can still flow into a Send. A Verification rule (e.g. "stop if Audience DE rows < 1" or "> ceiling") halts the automation before Step 4 β€” this is the safety story interviewers want.
  • Import Overwrite wipes the DE first. Overwrite truncates the staging DE before loading. If the new file is short or corrupt, you've lost the prior data and loaded garbage β€” pair it with Verification downstream, or use Append/Update when you need to retain.
  • Skipped runs are silent killers. If a scheduled run is still going when the next is due, the next is Skipped (and a Wait activity mid-run causes the overlapping run to skip too). Turn on the Skip notification so you catch overlap instead of finding a missing send the next morning.
  • Run Once β‰  Scheduled. Run Once executes the chain immediately for testing but does not activate the schedule. You must explicitly activate the schedule, or it silently never recurs.
  • Activity Monitor is the answer to "how do you know it failed." It lists every run with Complete / Error / Skipped, lets you drill into the failing step + activity and read the error, and is where you'd pause, skip the next occurrence, or stop a running automation. Pair it with run / error / skip email notifications β€” don't rely on logging in to check.
  • ~30-min activity / runtime caps still apply. A SQL Query or import that runs too long is cancelled and surfaces as an Error in the monitor β€” narrow date ranges, index with primary keys, split heavy jobs.

➑️ Next: P08_Contact_Deletion.md

P08 β€” Contact Deletion (GDPR erasure)

🎯 Interview questions this answers: - "How do you do a Contact Deletion in Marketing Cloud?" - "How do you handle a GDPR 'right to be forgotten' / right-to-erasure request?" - "What's the difference between unsubscribing someone, deleting a row in a Data Extension, and a Contact Delete?" - "What is the suppression period and why does it matter?" - "Does a Contact Delete affect one Business Unit or all of them?" - "What is a Data Retention Policy on a Data Extension?"


🧭 Where in the UI

There are two breadcrumbs β€” one to turn the feature on (once, by an admin), one to actually run a deletion.

1. Enable it once (admin, at the parent/top Business Unit):

Audience Builder β–Έ Contact Builder β–Έ Contacts Configuration (top menu)
   └─ "Contact Delete" section β–Έ enable β–Έ "Manage Settings"
        └─ set the Suppression Period (default = 2 days; set 0 for immediate)

2. Run a deletion (any time, after it's enabled):

Audience Builder β–Έ Contact Builder β–Έ All Contacts (top menu)
   └─ trash πŸ—‘οΈ icon β–Έ choose the source of contacts to delete β–Έ confirm "Delete"

Heads-up: Contacts Configuration is where you flip the toggle and set the delay. All Contacts is where the trash-can deletion flow lives. People mix these up in interviews β€” enable lives in Configuration, run lives in All Contacts.


πŸ–ΌοΈ Screen map

Contact Builder β€Ί Contacts All Activities Data Designer Contact Configuration Contact Configuration Contact Deletion Delete Contacts ON 1 Suppression period Delay: 2 days Held this long before hard delete (0 = immediate). Save Contact Deletion βœ• 2 Source of contacts to delete List Data Extension Filtered set 3 Selected Data Extension DE_GDPR_DeleteKeys β–Ύ ⚠ Suppresses, then deletes account-wide (all BUs). Runs asynchronously β€” irreversible once it completes. Cancel Delete 4 1 Enable Delete Contacts 2 Open Contact Deletion 3 Pick the source 4 Click Delete

Wireframe (not a screenshot).

Call-outs: 1. Delete Contacts toggle β€” in Contacts Configuration; an admin turns this ON once before anyone can delete. 2. Suppression Period β€” the delete delay. Default 2 days; set 0 to make the request eligible immediately. 3. Trash icon β€” opens the Delete Contacts dialog from the All Contacts screen. 4. Source selector β€” pick the audience to erase: a List, a Data Extension of contact keys, or a Filtered set. 5. Delete β€” confirm; the contacts go into the suppress β†’ queue β†’ hard-delete pipeline.


πŸ‘‰ Click recipe

A. Enable once (admin, do this on the parent / top BU):

  1. Audience Builder β–Έ Contact Builder β–Έ Contacts Configuration.
  2. Find the Contact Delete section and turn Delete Contacts ON.
  3. Click Manage Settings and set the Suppression Period β€” leave the 2-day default, or set 0 days if you want erasure to start immediately.
  4. Save. (This is a one-time setup; you don't repeat it per deletion.)

B. Run a deletion (every time you need to erase someone):

  1. Audience Builder β–Έ Contact Builder β–Έ All Contacts.
  2. Click the trash πŸ—‘οΈ icon without selecting individual rows β†’ choose Delete contacts from list / data extension (or select specific contacts and choose Delete selected contacts).
  3. Pick the source: a List, a Data Extension holding the contact keys to delete, or a Filtered set. (A DE of keys is the usual pattern for a batch GDPR request β€” up to ~1 million per operation.)
  4. Optionally choose whether the source DE itself is kept or removed after the run.
  5. Click Delete Contacts, then review and confirm by clicking Delete.

C. What happens next (you don't click anything β€” it's automatic):

  1. Suppress β€” the contacts are immediately hidden and blocked everywhere (Contact Builder, Email Studio, Journey Builder, Mobile Studio, Einstein) for the suppression window.
  2. Queue (async) β€” the request enters a queue that processes one deletion at a time; a run can take several hours.
  3. Hard delete β€” once the window passes and all related data is identified, the contact and its related data are permanently erased account-wide across every Business Unit that shares the account's ClientID.

πŸ—£οΈ Say this in the interview

"Contact Deletion is the GDPR right-to-erasure tool, and it's a two-part thing.

First, enable it once. As an admin at the parent BU I go to Contact Builder β–Έ Contacts Configuration, turn on Delete Contacts, and set the Suppression Period β€” it defaults to 2 days, and I usually set it to 0 if I want the erasure to start right away.

Then, to run it, I go to Contact Builder β–Έ All Contacts, click the trash icon, and choose my source β€” typically a Data Extension that holds the contact keys I need erased, but it can also be a List or a filtered set. I confirm with Delete.

What happens then is suppress-then-delete: the contacts are immediately suppressed β€” hidden and blocked across Email, Journey Builder, Mobile, and Einstein β€” and the actual erase request goes into an asynchronous queue that processes one at a time and can take hours. After the suppression window it hard-deletes the contact and all its related data. And critically it's account-wide β€” it erases across every Business Unit under the account, not just the one I'm in.

I always contrast the three kinds of 'remove': an unsubscribe only changes a status β€” the data's still there; deleting a row in a Data Extension removes them from that audience only; and a Contact Delete is true account-wide erasure. For GDPR you need the third one."


⚠️ Gotchas / what they probe

  • Irreversible. Once it hard-deletes, the contact and related data are gone permanently β€” there's no undo. Make sure the key list is correct before confirming.
  • Suppression delay is a window, not the deletion. During the suppression period the contact is only hidden/blocked. The default is 2 days; set 0 to make the request immediately eligible. Until the window passes, nothing is actually erased.
  • It affects ALL Business Units. In Enterprise 2.0, the delete runs from the parent and applies account-wide to every BU sharing the ClientID. You cannot selectively delete a contact from just one BU with this tool β€” it's all or nothing across the account.
  • Asynchronous / queued. Requests process one at a time and can take several hours; don't expect instant removal. Don't fire dozens of separate requests expecting parallel processing.
  • Watch BU-specific leftovers. Triggered sends, synchronized data extensions, and query/filter outputs in child BUs can still hold copies β€” verify those separately if you need a truly clean account.
  • Difference vs unsubscribe / row delete (they love this one):
  • Unsubscribe = changes status only (still in the system; suppressed from sends).
  • Delete a DE row = removes from that audience/table only; contact record survives elsewhere.
  • Contact Delete = account-wide erasure of the contact and related data. Only this one satisfies GDPR right-to-erasure.
  • Per-DE Data Retention Policy is a different lever. Set on a Data Extension (often on step 2 when creating it), it auto-cleans data on a schedule: delete individual records older than N days, delete all records (keep the DE shell), or delete the entire Data Extension. Max period is 730 days (2 years), and anything that ages out is permanently deleted β€” there's no recovery. With no policy set, data stays forever. This is housekeeping/compliance hygiene; it is not a substitute for a Contact Delete, because a DE policy only touches that one table, not the account-wide contact record.

➑️ Next: P09_Reports_and_Broader_Engagement.md

P09 β€” Reports & Broader Engagement

🎯 Interview questions this answers: - "What are the broader engagement reports in Marketing Cloud and where do you find them?" - "How do you access and run an email engagement report for a date range, then export it?" - "How do you verify that the platform stores 730 days of engagement data β€” and what do you do when you need more history?"


🧭 Where in the UI

You answer "broader engagement reports" by pointing at three distinct places β€” name them in order from most-packaged to most-custom:

  1. Analytics Builder β–Έ Reports β€” the standard report catalog. Pre-built email engagement reports you run, filter by date range, schedule, and export. This is the literal answer to "broader engagement reports." - Breadcrumb: App Switcher (top-left waffle) β–Έ Analytics Builder β–Έ Reports
  2. Email Studio β–Έ Tracking β€” per-send and aggregate metrics (Sent / Opens / Clicks / Bounces / Unsubs) for an individual job or rolled up across the account. - Breadcrumb: App Switcher β–Έ Email Studio β–Έ Email β–Έ Tracking
  3. Analytics Builder β–Έ Discover β€” drag-and-drop custom reports when the standard catalog isn't enough. And below that, Data Views via SQL (in Automation Studio Query Activities) for fully custom engagement history. - Breadcrumb: App Switcher β–Έ Analytics Builder β–Έ Discover (custom) / Automation Studio β–Έ SQL Query Activity (Data Views)

The single most common miss: candidates say "Email Studio Tracking" and stop. "Broader engagement reports" specifically means the Analytics Builder β–Έ Reports catalog β€” reports that span many sends, not one job.


πŸ–ΌοΈ Screen map

Analytics Builder β–Έ Reports Report Catalog Email Performance Over Time Account Send Summary Email Sends Engagement Bounce Mailing + more standard reports… 1 Email Performance Over Time Date Range 2024-06-22 β†’ 2026-06-22 max 730 days 2 Results preview Sends Opens Clicks 48,210 19,884 5,107 52,640 21,302 5,613 44,975 18,440 4,902 Opens / Clicks trend over selected range Run Schedule Export 3 4 Earliest selectable date β‰ˆ today βˆ’ 730 days ← data older than ~2 yrs is not queryable here

Wireframe (not a screenshot).

Call-outs: 1. Report catalog (left rail) β€” pick a standard engagement report; Email Performance Over Time is the workhorse "broader" one. 2. Date-range control β€” the filter that scopes the report; how far back it lets you go is your live proof of the retention window. 3. Run / Schedule / Export β€” run on demand, schedule a recurring emailed run, or export to Excel / CSV / PDF. 4. 730-day boundary β€” the earliest date you can select lands ~2 years ago; data older than that is no longer queryable here.


πŸ‘‰ Click recipe

A. Run a broader engagement report for a date range and export it

  1. Top-left App Switcher (waffle) β–Έ Analytics Builder β–Έ Reports.
  2. In the Report Catalog (left), select Email Performance Over Time (or Account Send Summary for account-level totals).
  3. Set the Date Range control to your window (e.g., last 90 days, or a custom From/To).
  4. Optionally filter by send/email or business unit, then click Run.
  5. Click Export and choose a format (Excel / CSV / PDF), or click Schedule to have it emailed on a recurring cadence.

B. Confirm the 730-day retention window (the "verify" move)

  1. In any engagement report, open the Date Range picker and drag the From date as far back as it allows.
  2. The earliest selectable / returnable date lands at roughly today βˆ’ 730 days. Anything older returns no rows β€” that is the retention limit, observable in the UI.
  3. Cross-check the same boundary in Email Studio β–Έ Tracking (older jobs drop off the same 730-day cliff) and confirm against the Salesforce policy effective June 16, 2025.

πŸ—£οΈ Say this in the interview

"The broader engagement reports live in Analytics Builder β–Έ Reports β€” that's the standard catalog. The one I reach for is Email Performance Over Time, plus Account Send Summary and Email Performance by Attribute / Domain. I select the report, set a date range, click Run, then Export to Excel/CSV/PDF or Schedule it to email out on a cadence. For a single send I use Email Studio β–Έ Tracking for sent/open/click/bounce/unsub, and for anything custom I go to Discover or query the Data Views with SQL in Automation Studio.

On retention: since the June 16, 2025 policy, Tracking and the Analytics Builder engagement reports keep 730 days β€” two years of subscriber engagement data. Data Views are only ~6 months. I verify the 730-day window by walking the date-range picker back: the earliest date it'll return is about today minus 730 days, and older data just isn't accessible. At GAP, because campaign-level history outlives two years, the senior move is to persist engagement to a Reporting Data Extension β€” a scheduled SQL Query Activity copying _Open, _Click, _Sent, _Bounce, _Unsubscribe data views into a DE β€” so we keep history well past the 730-day and 6-month limits and own our long-range reporting."


⚠️ Gotchas / what they probe

  • 730 days, not 6 months, not 180 days. The modern correct answer for Tracking + Analytics Builder Reports engagement data is 730 days (2 years), effective June 16, 2025. (The earlier-announced 180-day change was superseded.) Saying "6 months" here is the trap.
  • Data Views are the ~6-month figure β€” that's _Open, _Click, _Sent, _Bounce, _Job, etc. via SQL. Don't conflate the two retention numbers: Reports/Tracking = 730 days, Data Views β‰ˆ 6 months.
  • Intelligence Reports for Engagement already retain 2 years and Data Extensions are admin-managed β€” both are excluded from the June 2025 change. Mention this if probed on scope.
  • Opens are unreliable. Apple Mail Privacy Protection (MPP) pre-fetches images, inflating open rates. Lead with clicks / click-to-open as the trustworthy engagement signal.
  • Persist for longer history. The expected senior answer: data past 730 days (or 6 months for Data Views) is gone unless you export it out β€” a scheduled Query Activity into a Reporting DE, or an Automation Studio export to SFTP. Owning your own history is the GAP pattern.
  • "Verify" means show, not recite. They want you to demonstrate it in the UI (walk the date picker to the ~730-day floor) and cite the Salesforce policy β€” not just quote a number.

➑️ Next: P10_CloudPage_Form_to_Email.md

P10 β€” CloudPage Form β†’ DE β†’ Email

🎯 Interview questions this answers: - "How will you take information from a CloudPage form into an email? Which AMPscript/SSJS function inserts the data into SFMC?" - "InsertDE vs InsertData β€” what's the difference and when do you use each?"

One-line answer: A CloudPage form self-posts β†’ you read the fields with RequestParameter("field") β†’ you write them to a Data Extension with UpsertData (the CloudPage-context function that returns rows affected) β†’ a later email reads them back with Lookup / LookupRows keyed on SubscriberKey/email. On a CloudPage you use the *Data family; in an email you'd use the *DE family.


🧭 Where in the UI

Breadcrumb: App Switcher (waffle) β–Έ Web Studio β–Έ CloudPages

Web Studio
 └─ CloudPages
     └─ [+ Create]  β†’  Collection            ← a "Collection" is just a folder/campaign bucket
          └─ Open the Collection
               └─ [Create] β–Έ Landing Page    ← your form page (HTTPS, public URL)
                    └─ Content tab
                         β”œβ”€ HTML Block        ← paste your <form> + AMPscript here (full control)
                         └─ Smart Capture Block ← drag-drop form bound to a DE (no-code option)
  • The form, the RequestParameter reads, and the UpsertData write all live in ONE Landing Page β€” an HTML Content Block that posts to itself.
  • Two ways to build the form:
  • Smart Capture block β€” drag it on, point it at a target Data Extension, map DE columns β†’ input fields. SFMC writes the rows for you. Fast, but limited logic.
  • HTML block + AMPscript β€” you write the <form>, read fields, and call UpsertData yourself. This is the version interviewers want to see because you control validation, keys, and de-dupe.
  • The Data Extension itself is created over in Email Studio β–Έ Subscribers β–Έ Data Extensions (or Contact Builder). The CloudPage just references it by name/key.
  • The email that reads the data back is built in Email Studio / Content Builder β€” its Lookup runs at send time.

πŸ–ΌοΈ Screen map

Web Studio › CloudPages CloudPage form First name Akash Email akash@gap.com Submit AMPscript UpsertData reads fields, writes the submission ↳ returns rows affected Data Extension SubKey FirstName Email akash Akash akash@ gap.com ... ... ... key = EmailAddress Email Lookup at send time Hi Akash (read back) POST write read 1 2 3 4 ① CloudPage form (First name, Email, Submit) self-posts. ② AMPscript UpsertData writes the submission. ③ Data Extension stores SubscriberKey | FirstName | Email, keyed on EmailAddress. ④ A later email reads it back with Lookup at send time → renders “Hi Akash”.

Wireframe (not a screenshot).


πŸ‘‰ Click recipe

  1. Create the DE first. Email Studio β–Έ Subscribers β–Έ Data Extensions β–Έ Create. Name it Form_Submissions. Add columns: EmailAddress (Email, Primary Key), FirstName, Product, SubmittedDate. Set the primary key to EmailAddress so upserts de-dupe on it.
  2. Create the page. Web Studio β–Έ CloudPages β–Έ Create β–Έ Collection (e.g. Acquisition) β–Έ open it β–Έ Create β–Έ Landing Page.
  3. Set properties. Name it, pick your domain from the URL dropdown, tick HTTPS only.
  4. Build the form. Drag an HTML Block onto the canvas and paste the form + AMPscript (code below). (Or drag a Smart Capture Block, point it at Form_Submissions, and map fields β€” no-code alternative.)
  5. Add the AMPscript write-back. At the top of the same block, detect a POST (RequestParameter), validate, and call UpsertData.
  6. Publish. Click Publish (top right) β€” a draft page does NOT serve a live URL. Copy the published URL.
  7. Test. Open the URL, submit the form, then check Form_Submissions in Email Studio β€” the row should appear. Resubmitting the same email should update, not duplicate (proves the key works).
  8. Read it back in an email. Content Builder β–Έ create/open an email β–Έ in an HTML block add the Lookup (code below) keyed on the subscriber's email β–Έ preview against a test subscriber whose email exists in the DE β–Έ send.

πŸ’» Code

(a) CloudPage β€” form + RequestParameter + UpsertData write-back

%%[
  /* Run only when the page was POSTed to itself */
  SET @submitted = RequestParameter("submitted")

  IF @submitted == "1" THEN
    /* 1. Read the posted fields */
    SET @email     = RequestParameter("email")
    SET @firstName = RequestParameter("firstName")
    SET @product   = RequestParameter("product")

    /* 2. Validate + encode before trusting anything */
    IF NOT EMPTY(@email) AND IndexOf(@email, "@") > 0 THEN
      SET @firstName = HTMLEncode(@firstName)
      SET @product   = HTMLEncode(@product)

      /* 3. Write to the DE. UpsertData RETURNS the rows affected. */
      SET @rows = UpsertData(
        "Form_Submissions", 1,
          "EmailAddress",  @email,        /* 1 key column β†’ de-dupes on email */
          "FirstName",     @firstName,    /* update/insert columns follow */
          "Product",       @product,
          "SubmittedDate", Now()
      )
      SET @thanks = Concat("Thanks ", @firstName, " β€” saved (", @rows, " row).")
    ELSE
      SET @error = "Please enter a valid email."
    ENDIF
  ENDIF
]%%

%%[ IF NOT EMPTY(@thanks) THEN ]%%
  <p>%%=v(@thanks)=%%</p>
%%[ ELSE ]%%
  %%[ IF NOT EMPTY(@error) THEN ]%%<p style="color:red">%%=v(@error)=%%</p>%%[ ENDIF ]%%
  <form method="post" action="%%=RequestParameter('PAGEURL')=%%">
    <input type="hidden" name="submitted" value="1">
    <label>Email <input type="email" name="email" required></label>
    <label>First name <input type="text" name="firstName"></label>
    <label>Product <input type="text" name="product"></label>
    <button type="submit">Sign up</button>
  </form>
%%[ ENDIF ]%%

πŸ” Line by line: - SET @submitted = RequestParameter("submitted") β€” reads the hidden field. On first GET it's empty (show the form); after POST it's "1" (process the write). This is how one page handles both display and submit. - RequestParameter("email" / "firstName" / "product") β€” pulls each posted form field by its name attribute. RequestParameter reads POSTed form fields (and also CloudPagesURL-encrypted params); for plain ?key=value GET strings you'd use QueryParameter. - IF NOT EMPTY(@email) AND IndexOf(@email, "@") > 0 β€” minimal server-side validation. Never trust the browser; always re-check on the page. - HTMLEncode(@firstName) β€” output/encoding defense. Stops a submitted <script> from being reflected back into the page (XSS). Encode anything that came from the user before you store or echo it. - UpsertData("Form_Submissions", 1, "EmailAddress", @email, ...) β€” the write. Arg shape: DE name, then number of key columns (1), then the key column/value pair(s), then the update column/value pairs. If a row with that EmailAddress exists it's updated; otherwise a new row is inserted β€” that's the "upsert." - SET @rows = UpsertData(...) β€” the whole point of the *Data family: it returns the number of rows added/updated. You can branch on it, log it, or show a confirmation. (InsertData similarly returns 1 on success.) - Now() β€” server timestamp for SubmittedDate. - action="%%=RequestParameter('PAGEURL')=%%" β€” posts the form back to this same page, so the AMPscript above handles it. PAGEURL is the current published page URL. - %%=v(@thanks)=%% β€” renders the confirmation string. The IF @thanks / ELSE structure shows the thank-you after a successful write, otherwise re-renders the form (with an error if validation failed).

(b) Email β€” Lookup read-back at send time

%%[
  /* In an email, the recipient's address is in the system var. */
  SET @sk = AttributeValue("_subscriberkey")   /* or emailaddr */

  /* Single value: */
  SET @product = Lookup("Form_Submissions", "Product", "EmailAddress", emailaddr)

  /* Multiple columns / rows: */
  SET @rows = LookupRows("Form_Submissions", "EmailAddress", emailaddr)
  IF RowCount(@rows) > 0 THEN
    SET @row  = Row(@rows, 1)
    SET @fname = Field(@row, "FirstName")
  ENDIF
]%%

<p>Hi %%=v(@fname)=%%, here's more on %%=v(@product)=%% you asked about.</p>

πŸ” Line by line: - AttributeValue("_subscriberkey") / emailaddr β€” system values available in the email send context, identifying the recipient. You key your lookup off the recipient, never off a value the user could tamper with. - Lookup("Form_Submissions", "Product", "EmailAddress", emailaddr) β€” returns one column value from the first matching row: "give me Product where EmailAddress = this recipient." Perfect for a single field. - LookupRows("Form_Submissions", "EmailAddress", emailaddr) β€” returns the whole rowset matching the WHERE. Use when you need several columns or multiple rows. - RowCount(@rows) > 0 β€” guard so you don't read from an empty set (a recipient who never submitted). - Row(@rows, 1) then Field(@row, "FirstName") β€” pull row #1, then extract a named column from it. - Net effect: the email is personalized from what the subscriber typed on the CloudPage, because both sides share the Form_Submissions DE keyed on email.

*Data (CloudPage) vs *DE (email) β€” the function families

CloudPage / landing page / SMS Email What it does Returns a value?
InsertData InsertDE Insert a new row *Data yes (rows, 1); *DE no
UpsertData UpsertDE Update if key matches, else insert *Data yes (rows affected); *DE no
RetrieveData / Lookup / LookupRows Lookup / LookupRows Read rows back from a DE yes (the data)

Lookup/LookupRows work in both contexts; the insert/upsert split is the one that matters.


πŸ—£οΈ Say this in the interview

"It's a round-trip through a Data Extension. On the CloudPage β€” built under Web Studio β–Έ CloudPages as a Landing Page β€” I have an HTML form that posts to itself. On submit I read each field with RequestParameter, validate and HTMLEncode them, then write the row with UpsertData β€” keyed on email so it de-dupes instead of creating duplicates. I use UpsertData specifically because it's the CloudPage-context function and it returns the rows affected, so I can confirm the write and show a thank-you. Then the next email personalizes off that same DE: at send time I use Lookup or LookupRows keyed on the recipient's SubscriberKey or email to pull back exactly what they submitted. So the data the subscriber typed on the page flows form β†’ UpsertData β†’ DE β†’ Lookup β†’ email."

On InsertDE vs InsertData: "Same operation, different context. InsertDE/UpsertDE are the email-context functions β€” they run at the completion of the send and don't return a usable value. InsertData/UpsertData are the CloudPage / landing-page / SMS functions and they return the number of rows affected, which is why on a CloudPage I always reach for the *Data family β€” I get a return value I can branch on, and it runs inline when the page is hit rather than deferred to send completion."

(GAP context: I've used exactly this pattern for preference-center and acquisition pages β€” capture on a CloudPage, write to a DE, then drive the welcome/journey email off Lookup.)


⚠️ Gotchas / what they probe

  • *Data returns, *DE doesn't. This is the #1 distinction they're testing. UpsertData/InsertData give you a rows-affected count you can act on; UpsertDE/InsertDE return nothing usable and the insert is processed at send completion (and is skipped if a RaiseError aborts the send). Use *Data on CloudPages, *DE in emails.
  • RequestParameter vs QueryParameter. RequestParameter reads POSTed form fields and CloudPagesURL-encrypted params; QueryParameter reads plain GET ?key=value strings. A self-posting form uses POST β†’ RequestParameter.
  • IDOR / don't trust a raw key. Never accept a subscriber key or email straight from a query string and use it to write or read someone else's record. Validate, and when you link from an email to a CloudPage pass identifiers through CloudPagesURL so they're delivered as encrypted/signed params (then read with RequestParameter) β€” not as plaintext the user can edit.
  • Always validate + encode. Server-side check required fields and HTMLEncode user input before storing/echoing it to block reflected XSS. Browser-side required is not security.
  • Publish vs draft. A draft Landing Page has no live URL β€” the form 404s until you click Publish. Re-publish after edits.
  • Primary key drives upsert. If EmailAddress isn't the DE's primary key, UpsertData can't match and you'll get duplicate rows instead of updates.
  • Smart Capture vs HTML+AMPscript. Smart Capture is no-code but limited; for custom validation, de-dupe logic, and a confirmation off the return value, write the HTML form + UpsertData yourself.

➑️ Next: P11_Deliverability_and_IP_Warming.md

P11 β€” Deliverability & IP Warming

🎯 Interview questions this answers: - "How do you do IP warming?" - "A client just moved to a dedicated IP β€” what's your plan to protect deliverability?" - "Where do sender, authentication, and delivery settings live in SFMC, and how do they tie into reputation?"

One-line answer: IP warming = you gradually build sender reputation on a cold/new dedicated IP by starting with low volume to your most engaged subscribers, then ramping daily over ~2–4+ weeks on a fixed schedule while watching bounces, complaints, and Postmaster/SNDS β€” and backing off the moment reputation dips. ISPs distrust a brand-new IP that suddenly blasts millions; a slow, consistent, engagement-first ramp teaches them your mail is wanted.


🧭 Where in the UI

The auth + sender plumbing (one-time setup):

Setup (gear, top-right)
 └─ Settings
     └─ Company Settings
          └─ Sender Authentication Package (SAP)   ← your dedicated IP(s), private/branded domain,
              β”œβ”€ Dedicated IP address(es)               SPF, DKIM, Reply Mail Management, link wrapping
              β”œβ”€ Authenticated sending domain (DKIM/SPF)
              └─ Reply / bounce domain
Email Studio  β–Έ  Admin
 β”œβ”€ Send Classifications     ← bundles a Sender Profile + Delivery Profile + CAN-SPAM/commercial-vs-transactional
 β”œβ”€ Sender Profiles          ← the From name / From address subscribers see
 └─ Delivery Profiles        ← which IP/IP-pool + header/footer the send goes out on
  • The Sender Authentication Package (SAP) is the paid add-on that gives you a dedicated IP plus a branded/private domain with SPF + DKIM aligned to your domain (not the shared *.mcsv.net space). This is what you warm. Without a SAP you're on a shared IP β€” reputation is pooled across many senders and there's nothing for you to warm.
  • A Send Classification is the dropdown you pick at send time. It ties together a Sender Profile (the visible From) and a Delivery Profile (the technical "how/where it leaves"), and flags the send as Commercial vs Transactional for CAN-SPAM (transactional skips the unsubscribe/commercial footer).
  • The Delivery Profile is where the dedicated IP / IP pool is bound to the send. During warming you route through the IP you're warming; if you have multiple IPs you can split traffic across a pool here.
  • Authentication β‰  warming. SPF/DKIM/DMARC + branded domain prove who you are (anti-spoofing). Warming builds trust in your sending pattern. You need both β€” auth is table stakes; a perfectly-authenticated cold IP that blasts will still get throttled.

πŸ–ΌοΈ Screen map

IP Warming Β· daily volume ramp daily volume β†’ back off if complaints / bounces rise Day 1 Day 3 Week 1 Week 2 Week 3 Week 4+ most-engaged 30-day openers full list 1 2 3 4 β‘  start low to engaged openers β‘‘ ramp gradually steady daily steps β‘’ monitor Postmaster / bounces β‘£ full volume whole list, ~2–4 wks Setup β–Έ Sender Authentication Package Dedicated IP 198.51.100.24 Authenticated domain mail.brand.com Authentication SPF DKIM DMARC Monitor daily: Google Postmaster Tools Β· SNDS Β· Sender Score Β· bounces Β· spam complaints

Wireframe (not a screenshot).


πŸ“ˆ The warm-up ramp

A sample schedule β€” always tune the numbers to your list size, engagement, and how each ISP responds. The shape matters more than the exact figures: small start, steady daily increases, engaged-first, widen the audience window as reputation builds.

Day / Week Daily volume (illustrative) Audience to target
Day 1 ~500–1,000 Most-engaged: opened/clicked in last 30 days
Day 2 ~1,000–2,000 Same 30-day engaged segment
Day 3 ~3,000–5,000 30-day engaged
Day 4 ~7,500–10,000 30-day engaged
Day 5 ~15,000–25,000 30-day engaged
Week 2 ~50,000 β†’ 100,000 (step up daily) Widen to engaged in last 60 days
Week 3 ~250,000 β†’ 500,000 (step up daily) Widen to engaged in last 90 days
Week 4+ ~1M β†’ full list (or +40–100%/day) Full active/engaged list, then re-introduce older segments carefully

Rules of thumb that go with the table: - Roughly double (or +40–100%) per step, but only if yesterday's bounces/complaints stayed clean. - Keep volume consistent and rising β€” don't do 500 one day then nothing for a week; erratic patterns read as suspicious. - Cap a single IP at ~2–2.5M/day even once warm; add another dedicated IP per additional ~2M/day rather than overloading one. - A typical warm takes ~30 days of desirable sending history before ISPs fully trust the IP; larger/colder lists take longer.


πŸ‘‰ In practice

The step-by-step I'd actually run:

  1. Confirm the foundation is live. Setup β–Έ Company Settings β–Έ Sender Authentication Package provisioned: dedicated IP assigned, branded sending domain with SPF + DKIM validated, DMARC published on the domain, link/reply domains aligned. Verify with a seed send + header check before any warming. (No SAP = shared IP = nothing for you to warm.)
  2. Wire up the send config. Create a Sender Profile (From name/address on the branded domain), a Delivery Profile pointing at the dedicated IP/pool you're warming, and a Send Classification that bundles them and marks the stream Commercial.
  3. Build the engaged-first segments. Query/segment your most-engaged subscribers β€” opened or clicked in the last 30 days β€” as your Day-1 audience. Prepare wider tiers (60-day, 90-day) for later weeks. Suppress hard bounces, unsubscribes, role addresses, and anything stale.
  4. Build the ramp calendar. Lock the daily volumes (table above) into a sending calendar. Pick high-engagement, relevant content for the warm-up sends β€” these need strong open/click rates to signal "wanted mail."
  5. Send Day 1 low and watch. Send the smallest volume to the hottest segment. Don't move to Day 2 numbers until you've seen the results.
  6. Monitor every single day. Check Google Postmaster Tools (Gmail reputation, spam rate, auth pass), Microsoft SNDS / SNDR (Outlook/Hotmail), Sender Score, plus SFMC's own bounce rate, spam-complaint rate, deferrals/421 errors, and open rate.
  7. Ramp only on green. If yesterday looked clean, step up per the calendar. If bounces/complaints/deferrals spike or reputation dips β€” hold or roll the volume back to the last good level, let it stabilize, then resume more slowly. Throttle per-ISP independently (Gmail might be happy while Outlook defers).
  8. Reach steady state, then keep it steady. After ~2–4+ weeks you're at full volume. Maintain a consistent daily cadence β€” a warmed IP that goes quiet for weeks cools off and effectively needs re-warming.

πŸ—£οΈ Say this in the interview

"IP warming is how you build sender reputation on a new dedicated IP before you send at full scale. A brand-new IP has no history, so ISPs treat it with suspicion β€” if you blast millions on day one you get throttled, deferred, or blocked. So I warm it gradually.

First I make sure the foundation's there: in Setup β–Έ Company Settings, the Sender Authentication Package gives us the dedicated IP and a branded domain with SPF and DKIM, and we publish DMARC. Then I set up the Sender Profile, Delivery Profile, and Send Classification in Email Studio Admin so the send routes out on that IP.

The warming itself: I start low β€” a few hundred to a couple thousand a day β€” to our most-engaged subscribers, people who opened or clicked in the last 30 days, because their opens and low complaints tell the ISP this mail is wanted. Then I ramp the daily volume gradually over about two to four weeks, roughly doubling each step, and I widen the engagement window as I go β€” 60-day, then 90-day, then the full list. The whole time I'm monitoring bounces, spam complaints, deferrals, and reputation in Google Postmaster Tools and Microsoft SNDS. If reputation dips or complaints rise, I back off β€” hold or drop volume to the last clean level, let it settle, then resume more slowly. The keys are: engaged-first, gradual and consistent ramp, monitor daily, back off on any dip."

(Financial-services angle): "In financial services this matters even more β€” the mail is sensitive and time-critical, statements, security and account alerts, so deliverability has to be near-perfect and inbox placement consistent. I'd warm conservatively, separate transactional and promotional streams so a marketing complaint never poisons the reputation that delivers a security alert, and keep DMARC enforced to prevent spoofing of a trusted financial brand."


⚠️ Gotchas / what they probe

  • Cold blast = throttling/blocks. The #1 thing they're checking you understand: send big from a fresh IP and ISPs defer (421/4xx), throttle, or hard-block you, tanking the reputation you were trying to build. Warming exists precisely to avoid this.
  • Warm by engagement, not by random list slices. Start with your most-engaged subscribers β€” high opens/clicks and near-zero complaints are the positive signals that build reputation. Warming on a stale or purchased list does the opposite.
  • Authentication β‰  reputation. SPF/DKIM/DMARC and a branded domain prove identity and stop spoofing, but they don't earn inbox placement on their own. A fully-authenticated cold IP still has to be warmed.
  • Domain reputation matters too β€” and it's portable. ISPs track reputation at the sending-domain level as well as the IP. If you change IPs but keep the domain, some domain reputation carries; conversely a damaged domain hurts even a warm IP. Warm and protect both.
  • Consistency beats bursts. Erratic volume (500 today, nothing for a week, then 50k) reads as suspicious and breaks the warm. Keep a steady, rising daily cadence β€” and once warm, don't let the IP go idle or it cools.
  • Shared vs dedicated. On a shared IP you don't warm anything β€” reputation is pooled. Warming only applies once you've got a dedicated IP via the SAP; clarify which the client has before promising a plan.
  • Watch the right signals, per ISP. Don't rely on open rate alone (Apple MPP inflates it). Use Postmaster Tools (Gmail), SNDS (Microsoft), Sender Score, plus bounce/complaint/deferral rates β€” and throttle per-ISP, since Gmail and Outlook can react very differently to the same volume.
  • Separate your streams (esp. financial services). Keep transactional mail (statements, security/account alerts) on a different IP/classification from promotional blasts, so a marketing-complaint spike can't degrade the reputation that delivers critical, sensitive messages.
  • Single-IP ceiling. Even fully warmed, keep a single IP under ~2–2.5M sends/day; add IPs for more volume rather than overloading one.

➑️ Next: P12_Troubleshooting_and_Ops.md

P12 β€” Troubleshooting & Day-to-Day Ops (the questions you got)

🎯 Interview questions this answers: - "A customer says they didn't receive the email, but I can see the new email address IS in the target Data Extension β€” why didn't it go to the new address?" - "Lookup vs LookupRows vs LookupOrderedRows β€” what's the difference and when do you use each?" - "How do you raise an error / stop a send in AMPscript?" - "How do you save historical data of customers in SFMC?" - "Talk about your projects."

One-line answer: Most "they didn't get it" tickets are a subscriber-record problem, not a DE problem β€” the send email is stored on All Subscribers keyed by SubscriberKey, and for a DE send keyed on SubscriberKey, SFMC sends to the email already on that subscriber record (or a Held/Bounced/Unsub status suppresses them) β€” the new email in the DE only seeds brand-new subscribers. The rest is knowing your Lookup family (single value / unordered rowset / ordered+limited rowset, 2,000-row cap), RaiseError to guard a send, and a nightly Query Activity to retain Data View data before it ages out (Data Views ~6 months, Tracking 730 days).


πŸ–ΌοΈ RCA decision tree

START β€” "didn't receive it" but the new email IS in the target DE 1. Check All Subscribers Status Held / Bounced / Unsub? CAUSE: suppressed status blocks the send β€” they were never mailed 2. Compare stored email vs DE email subscriber record email β‰  DE email? MAIN CAUSE: they differ SubscriberKey send uses the subscriber-record email; the DE email only seeds NEW subscribers 3. Check audience sent from old DE / stale segment? CAUSE: wrong source corrected row wasn't in the audience β€” resend from right DE 4. Check _Bounce log sent then bounced? hard vs soft CAUSE: downstream it left SFMC β€” bounce / spam / mailbox issue, not config else ↓ else ↓ else ↓ yes yes yes yes

Wireframe (not a screenshot) β€” layout reflects the live SFMC UI.

Email Studio β–Έ Subscribers β–Έ All Subscribers Search by Subscriber Key: 7741 Search Subscriber Key Email Address Status 7741 old@x.com Active Verify the stored email + status HERE β€” not in the DE.

Wireframe (not a screenshot) β€” layout reflects the live SFMC UI.


πŸ”§ Q&A β€” theory + in practice

Q1. "Customer didn't receive the email, but the new address IS in the target DE β€” why?"

Theory. This is the single most-asked SFMC troubleshooting question, and the trap is assuming the DE is the source of truth for the email address. It is not. SFMC stores subscribers at the All Subscribers level, keyed by SubscriberKey, and the email address lives on that subscriber record (plus the _Subscribers profile data). For a Data Extension send whose send relationship is SubscriberKey, SFMC matches each DE row to an existing All Subscribers record by key and sends to the email already on that record. The email address in the DE is only used to create a brand-new subscriber the first time that key is seen β€” it does not overwrite the address on an existing subscriber. So: new address in the DE, old (or wrong) address on the subscriber record β†’ the mail goes to the old address (or nowhere useful), and the ticket says "didn't receive it."

That's the headline cause. Walk the rest of the RCA tree (above) in order:

  1. Status suppression. Even with a perfect address, if the All Subscribers status is Held, Bounced, or Unsubscribed, SFMC silently suppresses the send. They were never mailed.
  2. Stored email β‰  DE email (the headline cause) β€” send keyed on SubscriberKey used the record's old address.
  3. Wrong / stale audience. The send went out against an older DE or filtered audience that didn't include the corrected row β€” or the EmailAddress field wasn't mapped as the send's email field, so the address was empty/blank.
  4. It actually sent. Check Tracking β€” if it shows in _Sent and then _Bounce, the address left SFMC and the problem is downstream (mailbox full, hard bounce, spam folder).

…and in practice I'd: - Open Email Studio β–Έ Subscribers β–Έ All Subscribers, search by the customer's SubscriberKey (not email). On the record I check two things: the Status (Active vs Held/Bounced/Unsub) and the stored Email Address. If the stored email is the old one, that's the smoking gun β€” the DE's new email never took effect. - If status is Held/Bounced/Unsubscribed, that's the cause β€” they were suppressed. (For Unsub I cannot just flip it back; the contact must re-opt-in / re-subscribe through a legitimate flow.) - Confirm which audience was sent: open the Job in Tracking (or the email's send definition) and verify it used the corrected DE and that the EmailAddress field is mapped as the send email field. - Check Tracking β–Έ the Job β–Έ _Sent / _Bounce (or the _Sent, _Bounce data views via a Query): did it send and bounce? Hard vs soft bounce tells me address-dead vs temporary.

The fix (matched to the cause): - Stored email is stale β†’ update the email on the subscriber record before sending (manual edit in All Subscribers, an import that updates the profile, or a sync process β€” at GAP I built a WSProxy-based tool exactly to reconcile DE values against the subscriber record). Updating the DE alone does not fix it. - Want the DE email to win going forward β†’ send via a Journey (Journey Builder honors the DE email when entering contacts), or send to a list-based / properly configured send where the address is taken from the DE field, or have Salesforce support enable the override that lets a sendable DE's EmailAddress update the record at send time. Be careful: silently changing send-relationship behavior org-wide has side effects. - Held record β†’ clear the Held status (it auto-clears after the held period, or you re-validate); Unsubscribed β†’ legitimate re-opt-in only.

Q2. Lookup vs LookupRows vs LookupOrderedRows

Theory. All three read from a Data Extension, but they differ in what they return and whether you control order:

Function Returns Order Use when
Lookup a single column value from the first matching row unordered (you get a match, not a guaranteed one) you need exactly one field for one record (e.g. a first name)
LookupRows a rowset (multiple rows/columns) unordered you need several columns or several rows and don't care about order
LookupOrderedRows a rowset, sorted by a column you specify, optionally limited to N rows ordered (ASC/DESC) you need "the most recent order," "top 3," or any ordered/limited slice

All three are subject to the 2,000-row cap: LookupRows returns at most 2,000 rows, and LookupOrderedRows returns at most 2,000 when its rowcount argument is < 1 (meaning "all rows, capped at 2,000"). If you pass an explicit number greater than 2,000 you can exceed it, but the practical guidance is: don't lean on AMPscript to page huge sets β€” that's a SQL Query Activity's job.

%%[
  /* Lookup β†’ ONE value from the first matching row (unordered) */
  SET @firstName = Lookup("Customers", "FirstName", "SubscriberKey", _subscriberkey)

  /* LookupRows β†’ a rowset, unordered */
  SET @orders = LookupRows("Orders", "SubscriberKey", _subscriberkey)
  SET @count  = RowCount(@orders)

  /* LookupOrderedRows β†’ ordered + limited (here: the 1 most-recent order) */
  SET @recent = LookupOrderedRows("Orders", 1, "OrderDate DESC", "SubscriberKey", _subscriberkey)
  IF RowCount(@recent) > 0 THEN
    SET @row       = Row(@recent, 1)
    SET @lastOrder = Field(@row, "OrderNumber")
  ENDIF
]%%

πŸ” Line by line: - Lookup("Customers", "FirstName", "SubscriberKey", _subscriberkey) β€” args are DE name, the column you want back, then key/value pair(s) for the WHERE. Returns the FirstName from the first row whose SubscriberKey matches. One value, no order guarantee β€” fine when keys are unique. - LookupRows("Orders", "SubscriberKey", _subscriberkey) β€” args are DE name, then key/value pair(s). Returns the whole rowset that matches, unordered. You then iterate with Row() / Field(). - RowCount(@orders) β€” always guard a rowset before reading it, so you don't index into an empty set. - LookupOrderedRows("Orders", 1, "OrderDate DESC", "SubscriberKey", _subscriberkey) β€” args are DE name, max rows (1), the ORDER BY string ("OrderDate DESC"), then key/value pair(s). This gives me the single most-recent order. Passing 0/negative as the row count means "all, capped at 2,000." - Row(@recent, 1) then Field(@row, "OrderNumber") β€” pull row #1 from the rowset, then read a named column.

…and in practice I'd: reach for Lookup for a simple personalization token (first name, loyalty tier); LookupRows to render a list where order doesn't matter (e.g. all active subscriptions); and LookupOrderedRows whenever the business asks for "latest," "top N," or any sorted slice β€” passing the limit as the 2nd arg so I never accidentally pull a huge set. If I genuinely need thousands of rows joined/aggregated, I do it upstream in a SQL Query Activity into a purpose-built DE and let the email do a cheap Lookup.

Q3. How do you RaiseError in AMPscript?

Theory. RaiseError stops processing for a send and surfaces a message β€” it's how you guard a bad send or exclude a subscriber when a precondition isn't met (missing required field, failed validation, no matching record). The signature:

RaiseError("message", stopForThisSubscriberOnly [, smtpErrorCode] [, errorNumber] [, preserveDE])

The boolean (2nd arg) is the one interviewers probe β€” and it's counterintuitive: - true β†’ skip only the current subscriber and continue the rest of the send. - false (the default if omitted) β†’ stop the entire send job and return the error message.

%%[
  SET @email = AttributeValue("emailaddr")

  /* Guard: no usable email β†’ skip THIS subscriber, keep the job running */
  IF EMPTY(@email) THEN
    RaiseError("Missing email address β€” skipping subscriber", true)
  ENDIF

  /* Guard: a config/data problem so bad the whole send should abort */
  SET @region = Lookup("Config", "Region", "Key", "ACTIVE")
  IF EMPTY(@region) THEN
    RaiseError("Config DE empty β€” aborting entire send", false)
  ENDIF
]%%

πŸ” Line by line: - RaiseError("Missing email address β€” skipping subscriber", true) β€” message first, then true = per-subscriber skip. SFMC drops just this contact from the send and moves to the next one. Use this to quietly exclude bad rows without nuking the campaign. - RaiseError("Config DE empty β€” aborting entire send", false) β€” false (also the default) halts the whole job and bubbles the message up. Use this for "something is so wrong I'd rather send nothing." - Optional later args: an smtp error code / custom error number (your own values for logging), and a preserve-DE boolean that lets data already written by InsertDE/UpsertDE persist even though the error fired.

…and in practice I'd: put a RaiseError(..., true) guard at the top of the email's AMPscript for every must-have field (email present, key record found), so dirty rows get skipped, not sent with broken content β€” and I test it by previewing/sending to a deliberately broken test subscriber and confirming they're excluded while clean ones still send. I reserve false for genuine "abort everything" conditions (a config/feed DE that came back empty), because stopping a live send is a big hammer.

Q4. How do you save historical data of customers?

Theory. SFMC's engagement data ages out: Data Views (the system views like _Sent, _Open, _Click, _Bounce, _Job, _Subscribers) only hold roughly the last 6 months, and the broader Tracking / Email Studio Reports engagement retention is 730 days (2 years). If the business wants reporting beyond those windows, you have to copy the data into your own Data Extension before it expires β€” a DE you control can retain data indefinitely while the account is active. The standard pattern is a nightly SQL Query Activity in Automation Studio that selects from the Data Views and writes into a retained Reporting DE (append/update, not overwrite), scheduled daily so you always grab the freshest rows before they roll off.

-- Nightly: roll _Open data into a retained reporting DE
SELECT
    o.SubscriberKey,
    o.JobID,
    o.EventDate,
    j.EmailName
FROM _Open o
JOIN _Job j
    ON o.JobID = j.JobID
WHERE o.EventDate >= DATEADD(day, -2, GETDATE())   -- small overlap window

πŸ” Line by line: - FROM _Open o JOIN _Job j ON o.JobID = j.JobID β€” read from the Data Views (_Open, _Job); joining _Job gives human-readable context like EmailName alongside the raw open event. - WHERE o.EventDate >= DATEADD(day, -2, GETDATE()) β€” only pull the last couple of days each night (a small overlap window so nothing is missed between runs), instead of re-scanning 6 months every time. - The Query Activity's target DE is configured as Update (or Append) so each run adds to history rather than wiping it β€” the target DE is your long-term store and is set to no/long data retention so it never auto-purges.

…and in practice I'd: in Automation Studio, build a SQL Query Activity per data view I care about (_Sent, _Open, _Click, _Bounce, _Unsubscribe), point each at a Reporting DE with retention off, set the action to Update, drop them into a daily Automation scheduled in the early morning, and verify the row counts grow over time. That way when someone asks for engagement from 18 months ago, it's in my DE even though the Data View only remembers 6 months.


πŸ—£οΈ "Talk about your projects" β€” your template

Use a tight 3-part frame for each project β€” Problem β†’ What I built (with the SFMC primitives named) β†’ Impact β€” and tie the last line back to this role. Lead with the one most relevant to the JD, keep each to ~30–45 seconds, and name the actual tools so it's clearly hands-on, not theoretical.

Framing line: "At GAP I've owned the SFMC build for [brand/program]. Three pieces I'm proud of β€”"

1. DE-lookup WSProxy tool. "Support kept asking which DE holds this customer and what's their current value. I built an internal tool using WSProxy (the lightweight SOAP wrapper) that queries across our Data Extensions and the subscriber record by SubscriberKey, so we could reconcile the email on the All Subscribers record vs what's in the DE β€” which, as you know, is exactly the gap behind most 'they didn't get the email' tickets. It cut that triage from manual hunting to a few seconds."

2. Dynamic barcodes & countdown timers. "For loyalty and promo emails I built AMPscript-driven dynamic content β€” per-subscriber barcodes and live countdown timers generated at send/open time from DE values β€” so each email was personalized and time-aware without a separate template per offer."

3. A/B testing framework. "I set up a reusable A/B test framework so the team could split audiences, swap subject lines/content, and read the winner out of Tracking / Data Views consistently, instead of one-off tests β€” which made our optimization repeatable."

4. VAWP escalations. "I also handled Validation / Account / Workflow Process escalations end to end β€” diagnosing failed sends, automation errors, and data issues under SLA β€” which is where I got fluent in the RCA tree: All Subscribers status, stored-email-vs-DE, audience source, and bounce/tracking."

Tie-back: "That mix β€” building tooling, writing the AMPscript, and owning production troubleshooting β€” maps directly to what this role needs: someone who can both build the campaign and keep it running when something breaks."

How to deliver it: pick 1–2 projects unless they ask for more; always end each with a number or an outcome; and if they push on "how," drop straight into the UI path or the function (e.g. "I'd open All Subscribers, search by key, check status and stored email") β€” that's what separates a builder from a bystander.


⚠️ Gotchas / what they probe

  • The DE is NOT the source of truth for the send email. For a SubscriberKey-based DE send, SFMC uses the email on the All Subscribers / subscriber record; the DE email only seeds new subscribers. Updating the DE alone won't fix a wrong address. This is the #1 trap in the "didn't receive it" question β€” say it explicitly.
  • Search All Subscribers by SubscriberKey, not email. If you search by email you may miss the record entirely (the old/wrong email won't match the customer you're chasing).
  • Held / Bounced / Unsubscribed are silent. No error, no delivery β€” the contact is suppressed. Always check status first.
  • Unsubscribe is one-way. You can't just flip an Unsub back to Active; it needs a legitimate re-opt-in. Saying "I'd re-subscribe them" without that nuance is a red flag.
  • RaiseError boolean is backwards from intuition. true = skip just this subscriber; false (default) = kill the whole send. Mixing these up either nukes a campaign or lets bad rows through.
  • 2,000-row cap on the Lookup family. Don't try to paginate huge sets in AMPscript β€” that's a SQL Query Activity's job. Know that LookupOrderedRows with a rowcount < 1 means "all, capped at 2,000."
  • Retention windows: Data Views ~6 months, Tracking 730 days. If you can't quote these, you can't justify the nightly-Query-Activity archive pattern. The archive must run before data ages out, and the target DE must have retention off.
  • Mapped EmailAddress field. A DE send with no field designated as the email address sends to nothing β€” easy to overlook when a junior built the send definition.
  • "Talk about your projects" tests specificity. Vague ("I worked on emails") loses. Naming WSProxy, AMPscript dynamic content, Data Views, RCA steps proves hands-on depth β€” always be ready to drill from the story down into the exact click or function.

➑️ Next: P13_Write_It_Live_Drills.md

P13 β€” Write-It-Live Drills

🎯 Interview questions this answers: - "Here's a join diagram on the whiteboard β€” write me the SFMC SQL for it." (the D β†’ A β†’ B β†’ C puzzle) - "De-dupe this Data Extension so we keep only the latest row per subscriber. Write it now." - "Build a send audience that excludes hard bounces and unsubscribes. Show me the SQL." - "Loop over a customer's orders in AMPscript and render an HTML table. Go." - "Stop a send to anyone with a bad email β€” how do you throw it out at send time?" - "On a CloudPage, take this form and write it back to a Data Extension. Code it." - "Find everyone we emailed in the last 90 days who never opened. Write the query." - "Walk me through your thinking while you write it." (narration β€” they're scoring this too)

This module is a dojo. Each drill is Prompt β†’ solution β†’ line-by-line. Read the prompt, cover the solution, write your own on paper or in a scratch DE, then compare. The win condition is not "I understand it" β€” it's "I can produce it cold, out loud, in 90 seconds."


πŸ–ΌοΈ Join types at a glance

When they draw a diagram, you need to map shape β†’ join in one second. INNER = overlap only. LEFT = all of the left circle + the overlap. FULL OUTER = everything in both, matched where it can be, NULL-padded where it can't.

INNER JOIN A B only the overlap kept C.E. = Common Elements interview chain: D—C.E.→A—C.E.→B LEFT JOIN A B all of A + the overlap all of the left table keep every left row; NULL-pad where B is empty FULL OUTER JOIN A B both whole circles kept Everything = all rows, both sides interview chain: B—Everything→C

Wireframe.


πŸ₯Š Drills

Conventions used below (retail / financial-services flavored): - Customers β€” master audience DE. PK SubscriberKey, plus EmailAddress, FirstName, Region. - Orders β€” transactional DE. SubscriberKey, OrderID, OrderDate, OrderTotal. - Accounts β€” financial-services DE. SubscriberKey, AccountType, Balance. - System Data Views: _Sent, _Open, _Bounce, _Subscribers β€” joined on SubscriberKey (and JobID for send-level data).


Drill 1 β€” The JOIN-diagram question

Prompt: The interviewer draws this on the board and says "write the SFMC SQL":

D  --C.E.-->  A  --C.E.-->  B  --Everything-->  C

where C.E. = Common Elements (INNER JOIN) and "Everything" = FULL OUTER JOIN. No SELECT *. Explain INNER vs FULL OUTER, and mention the alternate reading.

Solution:

SELECT
      a.SubscriberKey   AS SubscriberKey,
      a.EmailAddress    AS EmailAddress,
      d.Region          AS Region,
      b.AccountType     AS AccountType,
      c.OrderID         AS OrderID,
      c.OrderTotal      AS OrderTotal
FROM      TableD AS d
INNER JOIN TableA AS a
      ON  a.SubscriberKey = d.SubscriberKey
INNER JOIN TableB AS b
      ON  b.SubscriberKey = a.SubscriberKey
FULL OUTER JOIN TableC AS c
      ON  c.SubscriberKey = b.SubscriberKey

πŸ” Line by line: - SELECT a.SubscriberKey AS SubscriberKey, ... β€” explicit column list with aliases, never SELECT *. In SFMC the target DE's columns must match what you return, so naming every column is non-negotiable. - FROM TableD AS d β€” start at D because the diagram starts there; the reading order of the arrows drives the FROM/JOIN order. - INNER JOIN TableA AS a ON a.SubscriberKey = d.SubscriberKey β€” first arrow D --C.E.--> A. C.E. = Common Elements = INNER JOIN: keep only rows where the key exists in both D and A. Anything unmatched on either side is dropped. - INNER JOIN TableB AS b ON b.SubscriberKey = a.SubscriberKey β€” second arrow A --C.E.--> B, same logic: only subscribers present in the D∩A result and in B survive. - FULL OUTER JOIN TableC AS c ON c.SubscriberKey = b.SubscriberKey β€” third arrow B --Everything--> C. "Everything" = FULL OUTER JOIN: every row from the left side (D∩A∩B) and every row from C, matched where keys line up, NULL-padded where they don't. A subscriber in C with no order history on the left still appears; a left-side subscriber with no row in C still appears.

Why INNER vs FULL OUTER (say this out loud): - INNER = intersection. Use it when the relationship is required β€” "a customer must exist in both the audience and the eligibility list." Smaller, safe result. - FULL OUTER = union of both sides with matching. Use it when you must not lose anyone from either table β€” reconciliation, "show me all customers and all orders even if one side is missing." Can produce large, NULL-heavy results, so it's the deliberate choice, not the default.

πŸ” Alternate reading β€” if "Everything" means "all of B": If the diagram-drawer meant "keep everything from B (the left so far) and only matching C" rather than literally both sides, that's a LEFT JOIN, not FULL OUTER:

LEFT JOIN TableC AS c
      ON  c.SubscriberKey = b.SubscriberKey

Call this ambiguity out in the room: "'Everything' usually means FULL OUTER β€” both sides survive. But if you mean 'all of B plus whatever matches in C,' that's a LEFT JOIN. Which did you intend?" Asking that question scores points; guessing silently does not.


Drill 2 β€” Dedup: keep the latest row per SubscriberKey

Prompt: "Orders has multiple rows per customer. Give me one row per SubscriberKey β€” the most recent order. Write it."

Solution:

SELECT
      ranked.SubscriberKey  AS SubscriberKey,
      ranked.OrderID        AS OrderID,
      ranked.OrderDate      AS OrderDate,
      ranked.OrderTotal     AS OrderTotal
FROM (
      SELECT
            o.SubscriberKey  AS SubscriberKey,
            o.OrderID        AS OrderID,
            o.OrderDate      AS OrderDate,
            o.OrderTotal     AS OrderTotal,
            ROW_NUMBER() OVER (
                  PARTITION BY o.SubscriberKey
                  ORDER BY     o.OrderDate DESC
            ) AS rn
      FROM  Orders AS o
) AS ranked
WHERE ranked.rn = 1

πŸ” Line by line: - Inner SELECT ... FROM Orders AS o β€” pull the raw order rows; you can't apply ROW_NUMBER() and filter it in the same SELECT, so it lives in a subquery. - ROW_NUMBER() OVER (...) β€” assigns a sequential number to each row within its window. It produces a value you can then filter on. - PARTITION BY o.SubscriberKey β€” restart the numbering for each subscriber. Every customer's rows get numbered 1, 2, 3... independently. - ORDER BY o.OrderDate DESC β€” within each customer, number them newest-first, so the most recent order gets rn = 1. (Add a tiebreaker like , o.OrderID DESC if two orders share a date and you need determinism.) - ) AS ranked β€” the whole windowed query becomes a derived table named ranked. SFMC requires a subquery alias. - WHERE ranked.rn = 1 β€” keep only the top row per partition. That's exactly one latest order per SubscriberKey. Done.

πŸ’‘ To dedup and keep all columns of the latest row, list those columns in the inner SELECT (as above) β€” don't GROUP BY, because MAX(OrderDate) alone can't tell you which OrderID/OrderTotal belonged to that date.


Drill 3 β€” Suppression anti-join (audience minus bounces + unsubs)

Prompt: "Build the mailable audience: everyone in Customers, minus anyone who hard-bounced or unsubscribed. Use an anti-join. Write it."

Solution:

SELECT
      c.SubscriberKey  AS SubscriberKey,
      c.EmailAddress   AS EmailAddress,
      c.FirstName      AS FirstName,
      c.Region         AS Region
FROM  Customers AS c
WHERE NOT EXISTS (
      SELECT 1
      FROM  _Bounce AS bnc
      WHERE bnc.SubscriberKey = c.SubscriberKey
        AND bnc.BounceCategory = 'Hard bounce'
)
AND   NOT EXISTS (
      SELECT 1
      FROM  _Subscribers AS sub
      WHERE sub.SubscriberKey = c.SubscriberKey
        AND sub.Status = 'Unsubscribed'
)

πŸ” Line by line: - FROM Customers AS c β€” the base audience; everything starts as "in" and we carve people out. - WHERE NOT EXISTS ( SELECT 1 FROM _Bounce ... ) β€” the anti-join. NOT EXISTS keeps a customer only when the subquery returns no rows. If a matching bounce exists, the customer is excluded. - SELECT 1 β€” inside an EXISTS/NOT EXISTS you only care whether a row exists, not its values, so select a constant. It's the idiomatic, fastest form. - WHERE bnc.SubscriberKey = c.SubscriberKey β€” correlate the subquery to the outer row. This is what makes it a per-customer existence check. - AND bnc.BounceCategory = 'Hard bounce' β€” only hard bounces suppress; soft bounces are transient and shouldn't permanently drop someone. - AND NOT EXISTS ( ... _Subscribers ... Status = 'Unsubscribed' ) β€” second anti-join, AND-ed on, removing opt-outs. Both conditions must pass to stay in the audience.

πŸ’‘ Why NOT EXISTS over LEFT JOIN ... WHERE x IS NULL? Both work in SFMC, but NOT EXISTS reads as intent ("not in the suppression list"), short-circuits on first match, and won't multiply rows if the suppression table has duplicates. Say that if they ask.


Drill 4 β€” AMPscript LookupOrderedRows loop β†’ HTML table

Prompt: "On an email or CloudPage, render this customer's recent orders as an HTML table, newest first. Handle the case where they have no orders. Write the AMPscript."

Solution:

%%[
  VAR @sk, @rows, @rowCount, @i, @row, @orderId, @orderDate, @orderTotal

  SET @sk = AttributeValue("SubscriberKey")

  /* DE name, max rows (0 = all, cap 2000), sort, then column/value filter */
  SET @rows = LookupOrderedRows("Orders", 10, "OrderDate DESC", "SubscriberKey", @sk)
  SET @rowCount = RowCount(@rows)
]%%

%%[ IF @rowCount > 0 THEN ]%%
  <table border="1" cellpadding="6">
    <tr><th>Order</th><th>Date</th><th>Total</th></tr>
    %%[ FOR @i = 1 TO @rowCount DO ]%%
      %%[
        SET @row        = Row(@rows, @i)
        SET @orderId    = Field(@row, "OrderID")
        SET @orderDate  = Field(@row, "OrderDate")
        SET @orderTotal = Field(@row, "OrderTotal")
      ]%%
      <tr>
        <td>%%=v(@orderId)=%%</td>
        <td>%%=Format(@orderDate, "MMM d, yyyy")=%%</td>
        <td>%%=FormatCurrency(@orderTotal, "en-US")=%%</td>
      </tr>
    %%[ NEXT @i ]%%
  </table>
%%[ ELSE ]%%
  <p>You have no recent orders.</p>
%%[ ENDIF ]%%

πŸ” Line by line: - VAR @sk, @rows, ... β€” declare every variable up front. AMPscript wants declarations before use. - SET @sk = AttributeValue("SubscriberKey") β€” pull the current subscriber's key from the send context (use RequestParameter instead if this is a CloudPage with a query string). - LookupOrderedRows("Orders", 10, "OrderDate DESC", "SubscriberKey", @sk) β€” returns a rowset: from Orders, at most 10 rows, sorted newest-first, filtered to SubscriberKey = @sk. The trailing pairs are column/value filters (you can append more pairs to AND extra conditions). - SET @rowCount = RowCount(@rows) β€” how many rows came back. This is the loop bound and the empty-state test. - IF @rowCount > 0 THEN β€” the empty fallback guard. Never loop 1 TO 0; branch first so a customer with no orders gets a friendly message, not a broken empty table. - FOR @i = 1 TO @rowCount DO β€” AMPscript rowsets are 1-indexed. Looping from 1 to the count touches every row exactly once. - SET @row = Row(@rows, @i) β€” grab the i-th row object out of the rowset. - SET @orderId = Field(@row, "OrderID") β€” pull a named column off that row. Repeat per column you want to print. - %%=v(@orderId)=%% β€” v() prints a variable inline in HTML. Format(...) and FormatCurrency(...) make the date and money human-readable. - NEXT @i β€” advance the loop. After the loop, </table> closes it. - ELSE ... ENDIF β€” the no-orders branch. Always close every IF with ENDIF and every FOR with NEXT.


Drill 5 β€” RaiseError to exclude a bad/empty email

Prompt: "At send time, throw this subscriber out of the send if their email is missing or malformed. How?"

Solution:

%%[
  VAR @email
  SET @email = AttributeValue("EmailAddress")

  IF Empty(@email) THEN
    RaiseError("Empty email address β€” excluding subscriber", true)
  ELSEIF IndexOf(@email, "@") == 0 THEN
    RaiseError("Malformed email (no @) β€” excluding subscriber", true)
  ENDIF
]%%

πŸ” Line by line: - SET @email = AttributeValue("EmailAddress") β€” read the recipient's email from the send context. - IF Empty(@email) THEN β€” Empty() is true for both NULL and "". Catches the missing-email case. - RaiseError("...", true) β€” this is the exclusion mechanism. RaiseError stops processing for the current subscriber and skips the send to them; the rest of the send continues for everyone else. The first arg is the logged message. - The second arg true β€” skip this subscriber but keep the send running. Passing false (or omitting it) would abort the entire send job, which you almost never want for a per-recipient data problem. Know the difference cold β€” they love this follow-up. - ELSEIF IndexOf(@email, "@") == 0 THEN β€” IndexOf is 1-based in AMPscript and returns 0 when the substring isn't found. So == 0 means "no @ anywhere" β†’ malformed β†’ raise and skip. - ENDIF β€” close the branch. If neither condition fires, the subscriber sails through and gets the send.

πŸ’‘ If asked "why not just filter in SQL upstream?" β€” say: "I do filter in the audience query; RaiseError is the last-line guard for bad data that slips through at send time, and it logs the reason so I can audit exclusions."


Drill 6 β€” CloudPage form write-back (RequestParameter + UpsertData)

Prompt: "A CloudPage has a preference-center form. On submit, write the values back to a Data Extension. Code the AMPscript."

Solution:

%%[
  VAR @submitted, @sk, @firstName, @email, @region

  SET @submitted = RequestParameter("submitted")

  IF @submitted == "true" THEN
    SET @sk        = RequestParameter("SubscriberKey")
    SET @firstName = RequestParameter("FirstName")
    SET @email     = RequestParameter("EmailAddress")
    SET @region    = RequestParameter("Region")

    UpsertData(
      "Customers", 1,
      "SubscriberKey", @sk,
      "FirstName",     @firstName,
      "EmailAddress",  @email,
      "Region",        @region
    )
  ENDIF
]%%

<form method="post" action="%%=RequestParameter('PAGEURL')=%%">
  <input type="hidden" name="submitted" value="true">
  <input type="hidden" name="SubscriberKey" value="%%=v(@sk)=%%">
  <input type="text"   name="FirstName"     placeholder="First name">
  <input type="email"  name="EmailAddress"  placeholder="Email">
  <input type="text"   name="Region"        placeholder="Region">
  <button type="submit">Save preferences</button>
</form>

πŸ” Line by line: - SET @submitted = RequestParameter("submitted") β€” RequestParameter reads a posted/query-string value by its form name. Here it reads a hidden flag so the page knows it's handling a real submission, not the first load. - IF @submitted == "true" THEN β€” only write on submit. Without this guard the page would try to upsert empty values every time it loads. - SET @sk = RequestParameter("SubscriberKey") (and the rest) β€” pull each form field by name into a variable. The name attribute on each <input> must match the string here exactly. - UpsertData("Customers", 1, ...) β€” the write-back. Updates the matching row or inserts a new one if none matches. - "Customers" β€” the target DE. It must have a Primary Key (here SubscriberKey) for the match to work. - 1 β€” the number of match column/value pairs that follow before the update pairs begin. We match on one key (SubscriberKey). The pairs after the match pair are the columns to set. - "SubscriberKey", @sk β€” the match pair: find the row where SubscriberKey = @sk. - "FirstName", @firstName, ... β€” the update pairs: the columns to write. If the row exists they're updated; if not, a new row is inserted with all these values. - action="%%=RequestParameter('PAGEURL')=%%" β€” post the form back to this same CloudPage, so the AMPscript at the top processes it. - <input ... name="FirstName"> β€” the form name is the contract between the HTML and RequestParameter. Mismatch the name and you silently write blanks.

πŸ’‘ UpsertData is the CloudPage/Microsite/SMS function. Its email-send cousin is UpsertDE. If they ask which to use on a landing page, the answer is UpsertData.


Drill 7 β€” Sent but not opened (last 90 days)

Prompt: "Find everyone we emailed in the last 90 days who never opened the email. Join the system data views. Write it."

Solution:

SELECT DISTINCT
      s.SubscriberKey  AS SubscriberKey,
      s.EmailAddress   AS EmailAddress
FROM  _Sent AS s
WHERE s.EventDate >= DATEADD(DAY, -90, GETDATE())
  AND NOT EXISTS (
      SELECT 1
      FROM  _Open AS o
      WHERE o.SubscriberKey = s.SubscriberKey
        AND o.JobID         = s.JobID
)

πŸ” Line by line: - SELECT DISTINCT s.SubscriberKey, s.EmailAddress β€” a subscriber can be sent many emails in 90 days; DISTINCT collapses them to one row per person so the resulting suppression/retarget audience has no duplicates. - FROM _Sent AS s β€” the system Data View of send events. One row per (subscriber, send). - WHERE s.EventDate >= DATEADD(DAY, -90, GETDATE()) β€” the 90-day window. GETDATE() is now; DATEADD(DAY, -90, ...) rolls back 90 days. Keep only sends in that window. - AND NOT EXISTS ( SELECT 1 FROM _Open AS o ... ) β€” the anti-join: keep a send only if there is no matching open. - WHERE o.SubscriberKey = s.SubscriberKey AND o.JobID = s.JobID β€” match on both keys. SubscriberKey alone isn't enough β€” you must check that this specific send (JobID) wasn't opened. Joining on SubscriberKey only would wrongly clear someone who opened a different email. - Net result: subscribers who received an email in the last 90 days and never registered an open for that exact send. That's your "sent, not opened" re-engagement audience.

πŸ’‘ Two follow-ups they may toss: (1) "unique opens vs total?" β€” _Open has a row per open event; DISTINCT plus the JobID match keeps it clean. (2) "why JobID?" β€” answer above; it scopes the open to the same send. Saying "SubscriberKey + JobID" unprompted signals you actually know the data views.


πŸ—£οΈ How to narrate while you code

Silence reads as "stuck." Keep a quiet running commentary β€” it shows your reasoning even when the syntax is still landing.

  • State the plan before the first keystroke. "I'll start from the base audience, then anti-join the suppression lists with NOT EXISTS." Now they know where you're headed even if you fumble a comma.
  • Name the join as you type it. "INNER here because the relationship is required β€” both sides must have the key." / "FULL OUTER because we can't lose rows from either table."
  • Say why, not just what. Not "I'm adding ROW_NUMBER" but "I need the latest row per customer, so I'll rank within each SubscriberKey by OrderDate descending and keep rank one."
  • Flag assumptions out loud. "I'm assuming OrderDate is the recency field β€” is that right?" / "'Everything' β€” do you mean FULL OUTER, or all of B? I'll go FULL OUTER unless you mean the latter." Asking beats guessing.
  • Call your edge cases. "I'll guard the loop with a row-count check so a customer with no orders gets a message, not an empty table." / "RaiseError with the second arg true so I skip just this subscriber, not the whole send."
  • Read it back when you finish. One sentence: "So: base audience, minus hard bounces, minus unsubs β€” that's the mailable list." Confirms intent and gives them a clean stopping point.
  • If you blank on exact syntax, narrate the shape. "It's a windowed function β€” ROW_NUMBER OVER, PARTITION BY the key, ORDER BY date descending β€” let me get the parentheses right." Demonstrating you know the structure recovers most of the credit.

➑️ Next: (final β€” re-run these drills out loud until automatic. You've got this, Akash! πŸš€)

P14 β€” Deloitte Round: CRM writes, Suppression, Lists & i18n

🎯 Interview questions this answers: - "How would you update a CRM object with AMPscript? Which Marketing Cloud Connect functions?" - "UpsertData vs UpsertDE β€” what's the difference and when do you use each?" - "Where is the Auto-Suppression List and who/how keeps it updated?" - "How do you update a List?" - "Walk me through updating a List via a File Drop Automation β€” the steps." - "All Subscribers vs All Contacts β€” what's the difference?" - "How do you carry data from an email into a CloudPage for personalisation?" - "The email supports 10 languages β€” how do you switch? Is there a switch / three-track function in AMPscript?"

One-line answer: CRM writes go through three Marketing Cloud Connect functions (CreateSalesforceObject, RetrieveSalesforceObjects, UpdateSingleSalesforceObject) — fine on CloudPages, never in bulk sends. *Data returns rows (CloudPage context), *DE doesn't (email context). The Auto-Suppression List lives under Email Studio ▸ Admin ▸ Send Management and you keep it fed (usually via an Automation). Lists update by import — File Drop → File Transfer → Import File. All Subscribers is the email roster (SubscriberKey); All Contacts is the cross-channel superset (ContactKey). Email→CloudPage uses CloudPagesURL with an encrypted param. And there is no switch in AMPscript — the "three-track" function is the IIF() ternary, and 10 languages want a lookup-driven pattern, not a 10-branch IF.


🧭 Where in the UI

Auto-Suppression Configuration (the one they probe):

Email Studio
  β†’ Email  (top nav)
    β†’ Admin
      β†’ Send Management
        β†’ Auto-Suppression Configuration   ← the org-level auto-suppression lists

⚠️ This is NOT the same screen as Email Studio β–Έ Subscribers β–Έ Suppression Lists. That second one is a per-send/exclusion list you attach at send time. The Auto-Suppression Configuration under Admin β–Έ Send Management is applied automatically to every send in the Business Unit. Knowing both paths β€” and that they're different β€” is the whole point of the question.

Lists:

Email Studio
  β†’ Subscribers
    β†’ Lists            ← create / open / import into a List
      β†’ [open a List] β†’ Import  (or feed it from an Automation Import File activity)

File Drop Automation (to update a List on file arrival):

Automation Studio
  β†’ New Automation
    β†’ Starting Source = File Drop      ← watches an Enhanced FTP folder
      β†’ File Transfer activity          ← move / decrypt / unzip the dropped file
        β†’ Import File activity          ← destination = the List, with field mapping + update type
          β†’ (optional) Verification activity / SQL / send

πŸ–ΌοΈ Screen map

Email Studio β€Ί Admin β€Ί Send Management β€Ί Auto-Suppression Configuration 1 Admin β–Έ Send Management (NOT Subscribers β–Έ Suppression Lists) Lists below are applied automatically to EVERY send in this Business Unit. + Add List 2 Suppression List Name Source Records DoNotContact_Global Automation 48,210 HardBounce_Suppress Automation 12,764 Compliance_Legal_Holds Manual import 331 … 3 YOU maintain it. Typical feed: SQL β†’ Data Extract β†’ FTP β†’ Import back into this list. β‘  Path: Email Studio β–Έ Admin β–Έ Send Management β–Έ Auto-Suppression Configuration. β‘‘ Add / Edit a list here β€” it then applies to EVERY send automatically (no per-send attach). β‘’ Not auto-populated: you feed it, usually via a scheduled Automation (do-not-send + hard bounces). *Wireframe (not a screenshot).*

Call-outs: β‘  it lives under Admin β–Έ Send Management, distinct from Subscribers β–Έ Suppression Lists. β‘‘ Add / Edit manages the org-level lists that auto-apply to every send. β‘’ SFMC does not keep these fed for you β€” a scheduled Automation typically extracts do-not-send / hard-bounce keys and imports them back in.


πŸ”§ Q&A β€” theory + in practice

Q1 β€” Update a CRM (Salesforce) object with AMPscript

Theory. With Marketing Cloud Connect (the MCC integration that links SFMC to a Salesforce org), AMPscript gets three CRM functions:

Function What it does Returns
CreateSalesforceObject("Object", n, "Field", val, …) Inserts a new CRM record the new record's Id
RetrieveSalesforceObjects("Object", "Fields", "Filter", op, "value") Reads matching CRM records into a rowset rowset
UpdateSingleSalesforceObject("Object", RecordId, "Field", value, …) Updates one existing record by Id 1 success / 0 fail

The catch is latency: roughly ~1 second PER call because each one is a live API round-trip to Salesforce. That's fine on a CloudPage or preference centre (one visitor at a time, low volume). It is catastrophic in a high-volume send β€” 1 million recipients Γ— ~1s = you blow the send window. For bulk CRM writes use Journey Builder Salesforce activities (Update Contact / Object) or the Salesforce API / data loads, not AMPscript in the email.

…and in practice (code): retrieve a Contact by email, then flip a field.

%%[
  /* 1. Read the CRM Contact for this visitor */
  SET @rows = RetrieveSalesforceObjects(
    "Contact",
    "Id,Email,Newsletter_OptIn__c",   /* fields to return */
    "Email", "=", @email              /* filter: Email = @email */
  )

  IF RowCount(@rows) > 0 THEN
    SET @row = Row(@rows, 1)
    SET @cid = Field(@row, "Id")       /* the record Id we'll update */

    /* 2. Update that one Contact β€” returns 1 on success, 0 on fail */
    SET @ok = UpdateSingleSalesforceObject(
      "Contact", @cid,
      "Newsletter_OptIn__c", "true"
    )

    IF @ok == 1 THEN
      SET @msg = "Preferences saved to CRM."
    ELSE
      SET @msg = "Could not update CRM β€” please retry."
    ENDIF
  ENDIF
]%%
<p>%%=v(@msg)=%%</p>

πŸ” Line by line: - RetrieveSalesforceObjects("Contact", "Id,Email,Newsletter_OptIn__c", "Email", "=", @email) β€” a live read into the connected Salesforce org: object = Contact, the fields to return (comma-separated), then the filter triple field / operator / value. You must return Id because that's what the update needs. This call costs ~1s. - IF RowCount(@rows) > 0 β€” guard: only proceed if a matching CRM Contact exists. - SET @cid = Field(@row, "Id") β€” pull the Salesforce record Id out of row #1. UpdateSingleSalesforceObject targets a record by Id, not by a filter. - UpdateSingleSalesforceObject("Contact", @cid, "Newsletter_OptIn__c", "true") β€” updates that one Contact's custom checkbox field. Arg shape: object, RecordId, then field/value pair(s). Another ~1s round-trip. - IF @ok == 1 β€” the function returns 1 on success, 0 on failure, so you branch on it to confirm or show a retry. (CreateSalesforceObject instead returns the new Id.) - Because each CRM call is ~1s, this whole block belongs on a CloudPage / preference centre, never inside a send to thousands of subscribers.


Q2 β€” UpsertData vs UpsertDE (and the whole *Data / *DE split)

Theory. Same operation (update-if-key-matches, else insert) β€” different execution context and different return behaviour:

  • UpsertDE = the email context function. It runs at send completion and does not return a usable value. You can't confirm the row count.
  • UpsertData = the CloudPage / landing-page / SMS context function. It runs inline when the page is hit and returns the number of rows affected, so you can branch on it and show a confirmation.

The same split applies to the whole family: InsertData/InsertDE, UpsertData/UpsertDE. Lookup / LookupRows (the read side) work in both contexts.

CloudPage / landing / SMS Email What it does Returns?
InsertData InsertDE Insert a new row *Data yes; *DE no
UpsertData UpsertDE Update if key matches, else insert *Data yes (rows); *DE no
Lookup / LookupRows Lookup / LookupRows Read rows back yes (the data) β€” both contexts

…and in practice (code):

%%[
  /* CloudPage: UpsertData RETURNS rows affected β†’ confirm the write */
  SET @rows = UpsertData(
    "Preferences", 1,
      "SubscriberKey", @sk,     /* 1 key column β†’ de-dupes on it */
      "Frequency",     @freq,   /* update/insert columns follow */
      "UpdatedDate",   Now()
  )
  SET @msg = Concat("Saved (", @rows, " row).")

  /* Email: UpsertDE does the same write but returns nothing usable */
  UpsertDE("Preferences", 1, "SubscriberKey", @sk, "Frequency", @freq)
]%%

πŸ” Line by line: - SET @rows = UpsertData("Preferences", 1, "SubscriberKey", @sk, …) β€” arg shape is DE name, number of key columns (1), then key col/value, then update col/value pairs. On a CloudPage this returns the rows-affected count into @rows. - SET @msg = Concat("Saved (", @rows, " row).") β€” because we got a return value, we can show a real confirmation. This is the reason to prefer *Data on CloudPages. - UpsertDE("Preferences", 1, "SubscriberKey", @sk, "Frequency", @freq) β€” identical intent in an email, but there's no return to capture (nothing to SET), and the write is deferred to send completion. Use it when the data-write is a side-effect of the send (e.g. logging), not when you need to confirm it.


Q3 β€” Auto-Suppression List: where it lives & who updates it

Theory. The Auto-Suppression Configuration is at Email Studio β–Έ Admin β–Έ Send Management β–Έ Auto-Suppression Configuration. Lists registered here are applied automatically to every send in the Business Unit β€” you don't attach them per-send. This is different from Email Studio β–Έ Subscribers β–Έ Suppression Lists, which are exclusion lists you pick when configuring a specific send/user-initiated send.

Who / how maintains it: YOU do. SFMC does not auto-populate it by default. The standard pattern is a scheduled Automation:

  1. SQL Query Activity β€” select do-not-send / hard-bounce / opt-out SubscriberKeys (e.g. from _Bounce where IsUnique + bounce category = hard, plus your compliance DE) into a staging DE.
  2. Data Extract activity β€” write that DE to a file.
  3. File Transfer activity β€” drop the file on Enhanced FTP.
  4. Import File activity β€” import the file back into the auto-suppression list, so the list stays current.

…and in practice (SQL for step 1):

SELECT DISTINCT b.SubscriberKey
FROM _Bounce b
WHERE b.BounceCategory = 'Hard bounce'
  AND b.EventDate >= DATEADD(DAY, -180, GETDATE())
UNION
SELECT d.SubscriberKey
FROM DoNotContact d          /* your compliance / opt-out DE */

πŸ” Line by line: - SELECT DISTINCT b.SubscriberKey FROM _Bounce b β€” pull unique keys from the bounce Data View; DISTINCT avoids re-adding the same key many times. - WHERE b.BounceCategory = 'Hard bounce' β€” only permanent (hard) bounces belong in suppression; soft bounces can retry. - AND b.EventDate >= DATEADD(DAY, -180, GETDATE()) β€” bounded to the ~6-month Data View window so the query stays fast. - UNION SELECT d.SubscriberKey FROM DoNotContact d β€” merges in your maintained opt-out/compliance DE. UNION de-dupes across both sources. - The result DE is then Data-Extracted β†’ File-Transferred to FTP β†’ Imported into the Auto-Suppression list on a schedule β€” that's the "who maintains it" answer: an Automation you own.


Q4 β€” Update a List

Theory. A List (Email Studio β–Έ Subscribers β–Έ Lists) is the classic email-channel grouping. You update its membership four ways: manual add/edit, Import (upload a file through the UI), an Automation Studio Import File activity (scheduled/File-Drop), or the API (SOAP/REST). Imports let you pick an update type (Add & Update / Overwrite / etc.) and map file columns to subscriber attributes.

…and in practice (UI path):

Email Studio β–Έ Subscribers β–Έ Lists
  β†’ open the target List
    β†’ Import
      β†’ choose file source (FTP / upload) β†’ map columns β†’ pick update type β†’ Import

For anything recurring you promote that same import into an Automation (next question) rather than doing it by hand.


Q5 β€” Update a List via a File Drop Automation (the steps)

Theory. A File Drop starting source makes Automation Studio watch an Enhanced FTP folder; the moment a matching file lands, the automation fires. That's how you turn "someone SFTP's us a file" into a hands-off list refresh.

…and in practice (the ordered steps):

  1. Automation Studio β–Έ New Automation.
  2. Set Starting Source = File Drop. Point it at the Enhanced FTP folder and a filename pattern (e.g. list_update_*.csv) it should watch.
  3. Add a File Transfer activity β€” moves the dropped file into the safe Import location and, if it arrived encrypted/zipped, decrypts / unzips it. (File Drop watches the drop folder; File Transfer stages it for import.)
  4. Add an Import File activity β€” destination = the List, define the field mapping (file columns β†’ subscriber attributes), and choose the update type (Add & Update / Overwrite / Update only).
  5. (Optional) Add a Verification activity (row-count sanity check) and/or a follow-on SQL / send step.
  6. Activate the automation. Now every matching file that lands updates the List automatically.

Memory hook: File Drop β†’ File Transfer β†’ Import File (β†’ Verify). "Watch β†’ stage/decrypt β†’ load β†’ check."


Q6 β€” All Subscribers vs All Contacts

Theory.

All Subscribers All Contacts
Where Email Studio β–Έ Subscribers β–Έ All Subscribers Contact Builder β–Έ All Contacts
Keyed on Subscriber Key (+ subscriber status) Contact Key
Scope The email-channel roster β€” everyone reachable by email, with their opt-in/held/unsub status The cross-channel people record (email, SMS, push, CRM, …) β€” the billing superset
Relationship A subset The universe

Rule to say out loud: every Subscriber is a Contact, but not every Contact is a Subscriber. All Subscribers is the legacy email view (status lives here β€” Active/Held/Unsubscribed/Bounced); All Contacts is the modern Contact Builder view that counts toward your contact billing count and spans every channel. Deleting from All Contacts is what actually removes a person org-wide (see P08).


Q7 β€” Email β†’ CloudPage personalisation (carry data across)

Theory. From an email you link to a CloudPage and pass an identifier through CloudPagesURL(pageId, "sk", _subscriberkey). CloudPagesURL encrypts the appended params, so the identifier travels as ciphertext, not plaintext the user can edit. On the page you read it with RequestParameter("sk"), then Lookup the DE for the details. Never pass raw PII (email, name) in the URL β€” pass the key, then look the rest up server-side.

…and in practice (code):

%%[ /* --- in the EMAIL --- */ ]%%
<a href="%%=CloudPagesURL(1234, 'sk', _subscriberkey)=%%">Manage preferences</a>

%%[ /* --- on the CLOUDPAGE --- */
  SET @sk  = RequestParameter("sk")                 /* encrypted param, auto-decrypted */
  SET @row = LookupRows("Master", "SubscriberKey", @sk)
  IF RowCount(@row) > 0 THEN
    SET @fname = Field(Row(@row, 1), "FirstName")
    SET @freq  = Field(Row(@row, 1), "Frequency")
  ENDIF
]%%
<p>Hi %%=v(@fname)=%%, your current frequency is %%=v(@freq)=%%.</p>

πŸ” Line by line: - CloudPagesURL(1234, 'sk', _subscriberkey) β€” builds a link to CloudPage id 1234 and appends sk = _subscriberkey as an encrypted query param. _subscriberkey is the email-context system var identifying the recipient. - RequestParameter("sk") β€” on the landing page this reads that param and auto-decrypts it. (Because it came via CloudPagesURL, it's signed/encrypted β€” a user can't just swap in someone else's key.) - LookupRows("Master", "SubscriberKey", @sk) β€” key the lookup off the trusted, decrypted key to pull the person's real details server-side. This is why you pass a key, not PII: the sensitive data never rides in the URL. - Field(Row(@row, 1), "FirstName") / "Frequency" β€” extract the columns you need from row #1 to personalise the page. - Net: the email hands off only an encrypted key; the CloudPage rehydrates the rest from the DE.


Q8 β€” 10 languages: switch / the "three-track" function

Theory. There is no native switch statement in AMPscript. (CASE … WHEN exists, but that's SQL β€” Query Activities β€” not AMPscript.) The AMPscript "three-track" function interviewers mean is IIF(condition, trueValue, falseValue) β€” the inline ternary: one condition, one true track, one false track. IIF is great for a 2-way choice.

For 10 languages, do not write a 10-branch IF/ELSEIF. Use a lookup-driven pattern so adding an 11th language is a data row, not a code change:

  • Option A β€” copy DE: a Copy_By_Lang DE keyed on (LangCode, Key); Lookup the localized string by the subscriber's Language attribute, with an English fallback when the row is missing.
  • Option B β€” content blocks: name blocks by language (hero-en, hero-fr, …) and pull with ContentBlockByKey(Concat("hero-", @lang)).

…and in practice (code):

%%[
  /* IIF = the "three-track" ternary: 2-way only */
  SET @lang    = AttributeValue("Language")
  SET @lang    = IIF(EMPTY(@lang), "en", @lang)     /* default to English */
  SET @isRTL   = IIF(@lang == "ar", "rtl", "ltr")   /* one condition, two tracks */

  /* Option A β€” lookup localized copy with English fallback (scales to 10+) */
  SET @hdr = Lookup("Copy_By_Lang", "Text", "LangCode", @lang, "Key", "hero_headline")
  IF EMPTY(@hdr) THEN
    SET @hdr = Lookup("Copy_By_Lang", "Text", "LangCode", "en", "Key", "hero_headline")
  ENDIF
]%%

<div dir="%%=v(@isRTL)=%%">
  <h1>%%=v(@hdr)=%%</h1>

  %%[ /* Option B β€” pull a whole localized content block by key */ ]%%
  %%=ContentBlockByKey(Concat("hero-", @lang))=%%
</div>

πŸ” Line by line: - SET @lang = AttributeValue("Language") β€” read the subscriber's language attribute (the switch key). One attribute drives everything β€” no hard-coded branching. - SET @lang = IIF(EMPTY(@lang), "en", @lang) β€” the three-track IIF: condition EMPTY(@lang) β†’ true track "en" β†’ false track keep @lang. This is AMPscript's stand-in for a switch/ternary, and it's inherently 2-way. - SET @isRTL = IIF(@lang == "ar", "rtl", "ltr") β€” another IIF for text direction. IIF shines for binary decisions like this; it does not scale to 10 branches. - Lookup("Copy_By_Lang", "Text", "LangCode", @lang, "Key", "hero_headline") β€” the scalable pattern: fetch the localized string for this language and content key from a data-driven copy table. Adding a language = adding rows, not editing AMPscript. - IF EMPTY(@hdr) THEN … Lookup(… "LangCode", "en" …) β€” the English fallback: if a translation row is missing, fall back to English instead of rendering blank. - ContentBlockByKey(Concat("hero-", @lang)) β€” Option B: build the block key dynamically (hero-fr, hero-de, …) and pull the whole localized block. Same idea β€” data/naming-driven, not a 10-way IF. - dir="%%=v(@isRTL)=%%" β€” applies the RTL/LTR direction so Arabic renders correctly. Localisation is copy and layout direction.


⚠️ Gotchas / what they probe

  • CRM writes are ~1s each β€” never in bulk sends. Update/Create/RetrieveSalesforceObjects are live API round-trips. Fine on CloudPages/preference centres (one visitor); in a mass send they blow the send window. Bulk = Journey Builder Salesforce activities or the API. Marketing Cloud Connect must be configured for these to work at all.
  • UpdateSingleSalesforceObject returns 1/0, and updates by Id. You need the record Id first (retrieve it), and you should branch on the 1/0 return to confirm. CreateSalesforceObject returns the new Id instead.
  • *Data returns, *DE doesn't. The #1 tested distinction: UpsertData/InsertData (CloudPage) give a rows-affected count and run inline; UpsertDE/InsertDE (email) return nothing usable and run at send completion.
  • Two different "suppression" screens. Admin β–Έ Send Management β–Έ Auto-Suppression Configuration (auto-applies to every send) is NOT Subscribers β–Έ Suppression Lists (attached per send). Mixing them up is the trap.
  • Auto-suppression isn't self-maintaining. You feed it β€” typically SQL β†’ Data Extract β†’ File Transfer (FTP) β†’ Import back into the list, on a schedule. If nobody built that Automation, the list goes stale.
  • File Drop vs File Transfer. File Drop is the starting source that watches the FTP folder; File Transfer is the activity that moves/decrypts the file. You usually need both, in that order, before Import File.
  • Import update type matters. On the Import File activity, "Overwrite" wipes the List first; "Add & Update" merges. Pick deliberately β€” Overwrite on the wrong list drops everyone not in the file.
  • All Subscribers βŠ‚ All Contacts. Subscriber status lives in All Subscribers; billing/cross-channel identity lives in All Contacts. Every subscriber is a contact, not vice-versa β€” and true org-wide deletion happens in Contact Builder (P08).
  • No PII in URLs. Pass the SubscriberKey via CloudPagesURL (encrypted) and Lookup the rest on the page; never put email/name in the querystring. Read encrypted params with RequestParameter, not QueryParameter.
  • No switch in AMPscript; CASE is SQL. The ternary is IIF() (two-way). For 10 languages use a lookup/content-block pattern with an English fallback β€” a 10-branch IF is the wrong answer and they're listening for that.

➑️ Next: (final practical module β€” pair with the Marketing Cloud Next track)

β˜… Marked for Review

Sections you flagged with the β˜† Mark button in the bar above (or the M key) while studying. Click any item to jump straight back to it. This list is saved in your browser and updates automatically.