Marketing Cloud: principles from production
Thirteen principles we apply to every Marketing Cloud implementation, with concrete code and the kinds of mistakes that bite at scale. Operational, not aspirational.
Thirteen principles we apply when we open Email Studio, write AMPscript, configure Journeys, or sign off on a Send. Each one comes with a concrete code example or a decision — anchored in implementation work, not in best-practice slides. Where Cleon's experience departs from generic best-practice, we say so.
The list below is what survives when production sends touch millions of contacts and a single bad assumption turns into a 2am incident review.
The principles
1. Beautiful is better than ugly.
The SFMC code that survives the next dev's read is the one that already looked clean to you. Beauty here is operational, not aesthetic — it lets the next person ship a small change without breaking the rest.
%%[
/* BAD — fast to type, slow to read */
var @x = AttributeValue("foo")
IF EMPTY(@x) OR @x == "0" OR LOWER(@x) == "n" THEN
SET @x = ""
ENDIF
]%%
%%[
/* CLEAN — same logic, names that mean something */
/* Normalize the loyalty flag into a boolean usable downstream. */
VAR @loyaltyRaw, @hasLoyalty
SET @loyaltyRaw = AttributeValue("loyalty_status")
SET @hasLoyalty = IIF(
EMPTY(@loyaltyRaw) OR @loyaltyRaw == "0" OR LOWER(@loyaltyRaw) == "n",
"false",
"true"
)
]%%The first version is faster to type. The second version is faster to debug at 11pm. AMPscript and SSJS get written quickly under deadline and stay in production for years — write for the version of you that has slept three hours and needs to find the bug now.
2. Explicit is better than implicit.
Name the data extension. Name the field. Name the journey version. SFMC has too many implicit defaults — every one is a future incident waiting for a junior dev to tab into.
DE_Mkt_LoyaltySends_v3 ← prefix, owner, purpose, version
Subscribers_v2_final ← unspecified prefix, no owner, unfinished versionThe first survives a hand-off. The second is a hostage note from the past.
3. Simple is better than complex.
The fewer Journey decision splits, the fewer 11pm investigations. Every conditional you add is a test path that has to survive the next data-model change. If a Journey cannot be drawn on a single sheet of paper, it is already a maintenance hostage.
KISS — Keep It Simple, Stupid
A Journey with one entry, one Send, and one wait is easier to diagnose than a Journey with one entry, one Send, three Splits, two Engagement Splits, and a wait. If both deliver the same business outcome today, the first one still delivers the same business outcome in six months.
YAGNI — You Aren't Gonna Need It
The "framework for future use cases" is the most expensive code Cleon has ever inherited. The use case you are imagining for next quarter rarely shows up; the abstraction you wrote to support it always does.
4. Complex is better than complicated.
There are problems that are genuinely complex — multi-channel orchestration, real-time consent, deduplication across data sources. For those, embrace the complexity but bound it with clear interfaces.
SOC — Separation of concerns
GOOD BAD
Automation: Build_Daily_Audience Automation: Daily_Stuff
├ SQL Query: rebuild_master_de (SQL + filter + send + cleanup
└ Verify row count in one untyped 800-line query)
Automation: Send_Daily_Audience
├ Trigger Send Definition: TS_Daily
└ Log to DE: de_log_daily_sendsTwo flat, named Automations beat one orchestrator that mixes data prep, sending, and logging. The next dev who needs to pause sends without pausing the data build will thank you.
DRY — Don't Repeat Yourself
Code Resources exist for a reason. The day you have the same 40-line consent check pasted into four CloudPages is the day you owe yourself a refactor.
5. Flat is better than nested. Sparse is better than dense.
Three flat Automations beat one nested orchestrator. Three SQL queries with descriptive names beat one 200-line query with sub-selects.
-- Flat: three steps, each one a question with a name
-- 1) eligibility
INSERT INTO eligible_subs
SELECT s.SubscriberKey
FROM master s
WHERE s.Status = 'Active' AND s.LoyaltyTier IN ('gold','silver');
-- 2) suppression
INSERT INTO send_audience
SELECT e.*
FROM eligible_subs e
LEFT JOIN suppression x ON e.SubscriberKey = x.SubscriberKey
WHERE x.SubscriberKey IS NULL;
-- 3) deduplication on email
INSERT INTO send_audience_dedup
SELECT MAX(SubscriberKey) AS SubscriberKey
FROM send_audience
GROUP BY EmailAddress;Each step is debuggable on its own. Each row count tells you which step lost subscribers. Three flat queries with named outputs beat one query with three layers of WHERE and a CTE no one tested.
6. Readability counts.
Code without comments where the WHY is non-obvious is half a code. The HOW is in the code itself; the WHY is in why this branch exists, why this throttle, why this fallback.
// BAD
if (n > 50000) Platform.Function.Sleep(2000);
// GOOD
// Throttle: SFMC's transactional send API drops requests above ~25k/min
// under our current MID's quota. We sleep 2s every 50k rows to stay
// under that — measured 2026-03 against production tenant after a
// soft-bounce spike. Revisit when SAP changes.
if (n > 50000) Platform.Function.Sleep(2000);The first version is one line. The second version is the same one line plus the receipt that lets the next dev change it safely.
7. Naming conventions count twice.
de_master_subscriber tells the next dev what the row means. Subscribers_v2_final_USE_THIS is a migration from somewhere unspecified to somewhere unfinished.
A Cleon implementation has a written naming convention before any DE is created:
| Prefix | What it holds |
|---|---|
| DE_ | Data Extension (master, owned by the implementation) |
| de_stg_ | Staging DE — rebuilt on every run |
| de_log_ | Run log — append-only, indexed by date |
| TS_ | Triggered Send Definition |
| J_ | Journey |
| Auto_ | Automation |
| CR_ | Code Resource |
Decide once, before the first DE exists. Renaming 200 of them later is the alternative.
8. Special cases aren't special enough to break the rules. Although practicality beats purity.
The deliverability emergency means you'll bypass your own QA rule once. The acquisition campaign needs a Send out by Thursday and the test cycle takes four days. Yes, you'll cut a corner.
9. Errors should never pass silently. Unless explicitly silenced.
// BAD — error is gone, log is silent, you find out from the client
try {
Platform.Function.UpsertData("master_subs",
["SubscriberKey"], [key],
["Status","UpdatedAt"], [status, Now()]);
} catch (e) { /* ignore */ }
// GOOD — silent only after logging the reason
try {
Platform.Function.UpsertData("master_subs",
["SubscriberKey"], [key],
["Status","UpdatedAt"], [status, Now()]);
} catch (e) {
// Known case: stale SubscriberKey from a deleted DE.
// Logged, not raised, because the daily reconciliation
// Automation will catch it.
Platform.Function.UpsertData("de_log_errors",
["RunId","Step","Msg","Ts"],
[runId, "upsert-master", e.message, Now()]);
}The cost of the second version is one DE plus four lines of code. The savings is the next post-mortem you don't have.
10. In the face of ambiguity, refuse the temptation to guess. Even when certain, test.
Salesforce's official documentation is right most of the time and wrong about field-length truncation, throttling behavior under load, and exact Journey re-entry rules in specific edge cases.
The discipline:
- Test against your actual data, not a happy-path sample.
- Send to a single seed before you send to a million.
- Read the Send Log, not just the dashboard summary.
- When the doc disagrees with the system, write down which one was right and the date.
11. Now is better than never. Although never is often better than right now.
Ship the small piece this week, not the perfect platform next quarter. AMPscript that works for one Email Send today is more useful than a "framework" that solves every future case but never gets reviewed.
But: don't ship a "temporary" SSJS that becomes the foundation. Mark temporary code with an expiration date and a calendar reminder.
// TEMPORARY — replace by 2026-06-15 (calendar reminder set 2026-05-15)
// Reason: hard-coded sender ID until the new SAP package finishes verification.
var senderId = 1234567;The reminder is what makes "temporary" honest.
12. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea.
If you cannot explain your Journey to a non-technical stakeholder in 30 seconds — what triggers it, who enters, what they receive, when they leave — simplify before launch.
The stakeholder who cannot follow your Journey today is the same one who will not be able to defend its budget next quarter. The 30-second test is non-negotiable on every Cleon Journey before go-live.
13. Documentation is one honking great idea — let's do more of it!
Every implementation Cleon ships has a one-page operational doc the client team can read on a Monday morning. The template is:
- What this Journey does — one paragraph in business terms
- Who enters — Data Extension name and entry criteria
- What they receive — Email Send Definitions in send order
- When they leave — exit criteria and expected duration
- How to pause it — Automation name, button name, screenshot
- How to roll back the latest Send — revert command, recovery time
- Who to call — Cleon point of contact and escalation chain
Code without docs is a hostage situation — the dev holds operational knowledge the client paid for. Cleon writes the docs because the implementation is not finished until the next person can run it without us.
Closing
These are not rules to memorize. They are the muscle you build when the same kinds of mistakes keep biting at scale. Cleon's MC implementations are written with these in mind — and so are the ones that go to production at our clients.
If you spot a violation of any of them in our work, write to hello@wearecleon.com — we fix it and we say so.