Project · Iterable · Salesforce MC · Marketo
One Block Written for Three ESPs
The iD Tech Pathway progress block, written three times: Iterable Handlebars, SFMC AMPscript and Marketo Velocity. The data and the rendered email are the same on every platform, so the only thing that changes is the platform itself. One parent, Priya, has two Pathway students: Maya at level 4 and Theo at level 10.
- Iterable
- Salesforce MC
- Marketo
- Handlebars
- AMPscript
- Velocity
- Personalization
Switch platforms and the number of students on file. The rendered email, the data as each platform stores it, and the code all change together.
{{#each profile.students}}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="student">
<h2>{{studentName}} · Level {{pathwayLevel}}</h2>
{{#if (eq pathwayLevel 10)}}
<p>Level 10. {{studentName}}'s letter of recommendation is ready.</p>
<a href="{{letterUrl}}">Download the letter</a>
{{else}}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td width="{{progressPct}}%" height="8" bgcolor="#AB9353"></td>
<td height="8" bgcolor="#E9E5DC"></td>
</tr>
</table>
<p>{{numberFormat pathwayXP groupingUsed=true}} XP ·
{{math nextLevelXP '-' pathwayXP}} to level {{math pathwayLevel '+' 1}}</p>
<p>Next on the path: <a href="{{nextCourseUrl}}">{{nextCourse}}</a></p>
{{/if}}
</td>
</tr>
</table>
{{/each}}
%%[
SET @students = LookupOrderedRows("Pathway_Students", 0, "StudentName ASC", "ParentKey", _subscriberkey)
SET @count = RowCount(@students)
IF @count == 0 THEN
RaiseError("No Pathway students for this parent", true)
ENDIF
FOR @i = 1 TO @count DO
SET @row = Row(@students, @i)
SET @name = Field(@row, "StudentName")
SET @level = Field(@row, "PathwayLevel")
SET @xp = Field(@row, "PathwayXP")
SET @next = Field(@row, "NextLevelXP")
SET @width = Concat(Field(@row, "ProgressPct"), "%")
]%%
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="student">
<h2>%%=v(@name)=%% · Level %%=v(@level)=%%</h2>
%%[ IF @level == 10 THEN ]%%
<p>Level 10. %%=v(@name)=%%'s letter of recommendation is ready.</p>
<a href="%%=RedirectTo(Field(@row, "LetterUrl"))=%%">Download the letter</a>
%%[ ELSE ]%%
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td width="%%=v(@width)=%%" height="8" bgcolor="#AB9353"></td>
<td height="8" bgcolor="#E9E5DC"></td>
</tr>
</table>
<p>%%=FormatNumber(@xp, "N0")=%% XP ·
%%=Subtract(@next, @xp)=%% to level %%=Add(@level, 1)=%%</p>
<p>Next on the path: <a href="%%=RedirectTo(Field(@row, "NextCourseUrl"))=%%">%%=v(Field(@row, "NextCourse"))=%%</a></p>
%%[ ENDIF ]%%
</td>
</tr>
</table>
%%[ NEXT @i ]%%
## Email script token {{my.PathwayBlock}} · student_c checked in the object tree
#if( $student_cList.isEmpty() )
## nothing to print; the campaign's smart list already requires a student
#else
#foreach( $s in $sorter.sort($student_cList, "studentName") )
#set( $level = $math.toInteger($s.pathwayLevel) )
#set( $xp = $math.toInteger($s.pathwayXP) )
#set( $next = $math.toInteger($s.nextLevelXP) )
#set( $pct = $math.toInteger($s.progressPct) )
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="student">
<h2>${s.studentName} · Level ${level}</h2>
#if( $level == 10 )
<p>Level 10. ${s.studentName}'s letter of recommendation is ready.</p>
<a href="https://${s.letterPath}">Download the letter</a>
#else
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td width="${pct}%" height="8" bgcolor="#AB9353"></td>
<td height="8" bgcolor="#E9E5DC"></td>
</tr>
</table>
<p>$number.format("#,##0", $xp) XP ·
$math.sub($next, $xp) to level $math.add($level, 1)</p>
<p>Next on the path: <a href="https://${s.nextCoursePath}">${s.nextCourse}</a></p>
#end
</td>
</tr>
</table>
#end
#end
The block
One card per student. Each card shows the student's level, a progress bar toward the next one, the XP still to earn and the next course on their path. At level 10 the bar gives way to the letter of recommendation, which is where the Pathway ends.
The block goes in the level-up email and in the parent newsletter, so it has to handle a family with one student, several, or none.
Progress is sent, not calculated. The Pathway XP service owns the level thresholds, so it writes progressPct alongside the XP. The email, the app and the camp badge always agree, and each template only does the small math: XP to go and the next level number.
| Field | Maya | Theo |
|---|---|---|
| Level | 4 | 10 |
| XP | 3,650 | 9,850 |
| Next level at | 4,000 | null |
| Progress | 65% | 100% |
| Shows | Bar + next course | Letter |
The same values sit on all three platforms. Names, URLs and numbers are examples.
Where the data lives
| Platform | Shape | What to know |
|---|---|---|
| Iterable | An array of objects on the parent's profile, profile.students | Set the field to the nested data type so a segment can match level and pathway on the same student. The XP service writes the whole array each time, since arrays replace rather than merge. |
| Salesforce MC | A child Data Extension, Pathway_Students, one row per student | ParentKey holds the parent's Subscriber Key. The template reads it by Data Extension name with LookupOrderedRows. |
| Marketo | A custom object, student_c, linked to the parent's lead record | Velocity sees it as $student_cList, holding the ten most recently updated records, and only the fields checked in the script token's object tree. |
Iterable · Handlebars
Handlebars is built for display. The loop and the level check read straight off the profile, and the documented math and numberFormat helpers cover the small arithmetic.
Rather than leaning on a template error to stop a send, the "no students" case is handled upstream, with a Journey filter before the send step.
SFMC · AMPscript
AMPscript fetches its own data. One LookupOrderedRows call pulls every student row by Data Extension name, and the FOR loop prints a card per row.
RaiseError with its second argument set to true skips this one subscriber and lets the rest of the send continue. RedirectTo keeps click tracking on links built at send time.
Marketo · Velocity
Velocity in Marketo can hand field values over as strings (booleans arrive as "1" or empty), so the script converts XP and level to numbers before comparing or subtracting.
Links are written with https:// outside the variable and the whole tag output in one piece, which is what lets Marketo track them. The smart list, not the script, decides who gets the email.
| Task | Iterable | Salesforce MC | Marketo |
|---|---|---|---|
| Print a field | {{studentName}} | %%=v(@name)=%% | ${s.studentName} |
| Fallback when empty | {{defaultIfEmpty firstName "there"}} | IIF(Empty(@firstName), "there", @firstName) | #if( $lead.FirstName.isEmpty() )there#else${lead.FirstName}#end |
| Condition | {{#if (eq pathwayLevel 10)}} | IF @level == 10 THEN | #if( $level == 10 ) |
| Loop | {{#each profile.students}} | FOR @i = 1 TO @count DO | #foreach( $s in $student_cList ) |
| Arithmetic | {{math nextLevelXP '-' pathwayXP}} | Subtract(@next, @xp) | $math.sub($next, $xp) |
| Format a number | {{numberFormat pathwayXP groupingUsed=true}} | FormatNumber(@xp, "N0") | $number.format("#,##0", $xp) |
| Related records | An array on the profile, or a Catalog | LookupRows, LookupOrderedRows | A custom object list |
| Skip a send | Journey filter before the send | RaiseError("why", true) | Smart list filter |
| Link built at send time | Merge field in the href | RedirectTo(@url) | Literal https:// plus the variable |
First name with a fallback, five platforms
The token I rewrite most in a migration. Three migrations (Mailchimp to Iterable twice, Marketo to Salesforce MC once) means a lot of these.
| Iterable | {{defaultIfEmpty firstName "there"}} |
| Salesforce MC | %%=IIF(Empty(AttributeValue("FirstName")), "there", AttributeValue("FirstName"))=%% |
| Marketo | {{lead.First Name:default=there}} |
| Mailchimp | *|IF:FNAME|**|FNAME|**|ELSE:|*there*|END:IF|* |
| HubSpot | {{ personalization_token('contact.firstname', 'there') }} |
Marketo's form is the email token; inside a Velocity script the same field is $lead.FirstName.

