Hi Everyone,
I seemed to have unknowingly stumbled upon Velocity's ability to declare and reference variables across multiple tokens. After removing one debug token from an email, subsequent script tokens stop evaluating and fall back to their default values. Re-adding the debug token “fixes” the issue, but I don’t fully understand why.
Context
I’m working on an auto-renewal email that pulls fields from a related Opportunity with Velocity in a few tokens (Pre-AR notification date, plus 30-/90-day offsets).
I had a temporary “debug” Velocity token in the template that printed out all calculated dates and metadata. That worked perfectly.
Once I removed that debug token (to clean up before final approval), the main script token suddenly rendered its fallback (“no date found”) for every recipient—even though the underlying Opportunity data was unchanged.
If I re-insert the debug token back into the email, everything renders correctly again.
Issue Statement
When multiple Velocity script tokens exist in the same email asset, Marketo appears to stop processing subsequent tokens once the first one is removed—even if those other tokens haven’t changed. I’m looking for insight into how Marketo’s Velocity engine declares or scopes its variables and why the presence (or absence) of an earlier script token affects later ones. Has anyone seen this behavior, and can you explain what’s happening under the hood or suggest a better workaround?
Token Scripts:
Token 1 (Debugging):
## --- Set required Velocity variables for timezone and formatting ---
#set( $defaultTimeZone = $date.getTimeZone().getTimeZone("America/New_York") )
#set( $defaultLocale = $date.getLocale() )
#set( $calNow = $date.getCalendar() )
#set( $ret = $calNow.setTimeZone($defaultTimeZone) )
#set( $calConst = $field.in($calNow) )
#set( $ISO8601DateOnly = "yyyy-MM-dd" )
## --- Format today's date as yyyy-MM-dd string ---
#set( $today = $date.format($ISO8601DateOnly, $calNow, $defaultLocale, $defaultTimeZone) )
## --- Find first matching opportunity ---
#set($matchingOpp = "")
#foreach($opp in $OpportunityList)
#if(
$opp.Pre_Auto_Renewal_Notification_Date__c &&
$opp.Stage != "Closed Lost" &&
!$opp.Auto_Renewal_Opt_Out__c &&
$opp.Type == "Auto Renewal - Pending"
)
#set($matchingOpp = $opp)
#break
#end
#end
## --- Output calculated date values ---
#if($matchingOpp != "" && $matchingOpp.Pre_Auto_Renewal_Notification_Date__c)
## Set Opp Name
#set($oppName = $matchingOpp.Name)
## Parse string to Date
#set($baseDate = $convert.parseDate($matchingOpp.Pre_Auto_Renewal_Notification_Date__c, $ISO8601DateOnly, $defaultLocale, $defaultTimeZone))
## Convert to Calendar and add days
#set($cal30 = $convert.toCalendar($baseDate))
#set($cal90 = $convert.toCalendar($baseDate))
$cal30.add($calConst.DATE, 30)
$cal90.add($calConst.DATE, 90)
## Format and print output
Opp Name: $oppName<br>
Pre AR Date: $date.format($ISO8601DateOnly, $baseDate, $defaultLocale, $defaultTimeZone)<br/>
+30 Days: $date.format($ISO8601DateOnly, $cal30, $defaultLocale, $defaultTimeZone)<br/>
+90 Days: $date.format($ISO8601DateOnly, $cal90, $defaultLocale, $defaultTimeZone)
#else
no date found
#end
Token 2 (30 day offset)
## --- Set required Velocity variables for timezone and formatting ---
#set( $defaultTimeZone = $date.getTimeZone().getTimeZone("America/New_York") )
#set( $defaultLocale = $date.getLocale() )
#set( $calNow = $date.getCalendar() )
#set( $ret = $calNow.setTimeZone($defaultTimeZone) )
#set( $calConst = $field.in($calNow) )
#set( $ISO8601DateOnly = "MM/dd/yyyy" )
## --- Find first matching opportunity ---
#set($matchingOpp = "")
#foreach($opp in $OpportunityList)
#if(
$opp.Pre_Auto_Renewal_Notification_Date__c &&
$opp.Stage != "Closed Lost" &&
!$opp.Auto_Renewal_Opt_Out__c &&
$opp.Type == "Auto Renewal - Pending"
)
#set($matchingOpp = $opp)
#break
#end
#end
## --- Output calculated date values ---
#if($matchingOpp != "" && $matchingOpp.Pre_Auto_Renewal_Notification_Date__c)
## Parse string to Date
#set($baseDate = $convert.parseDate($matchingOpp.Pre_Auto_Renewal_Notification_Date__c, $ISO8601DateOnly, $defaultLocale, $defaultTimeZone))
## Convert to Calendar and add days
#set($cal30 = $convert.toCalendar($baseDate))
#set($cal90 = $convert.toCalendar($baseDate))
$cal30.add($calConst.DATE, 30)
$cal90.add($calConst.DATE, 90)
## Format and print output
$date.format($ISO8601DateOnly, $cal30, $defaultLocale, $defaultTimeZone)#else
no date found#end
Token 3 (90 Offset):
## --- Set required Velocity variables for timezone and formatting ---
#set( $defaultTimeZone = $date.getTimeZone().getTimeZone("America/New_York") )
#set( $defaultLocale = $date.getLocale() )
#set( $calNow = $date.getCalendar() )
#set( $ret = $calNow.setTimeZone($defaultTimeZone) )
#set( $calConst = $field.in($calNow) )
#set( $ISO8601DateOnly = "MM/dd/yyyy" )
## --- Find first matching opportunity ---
#set($matchingOpp = "")
#foreach($opp in $OpportunityList)
#if(
$opp.Pre_Auto_Renewal_Notification_Date__c &&
$opp.Stage != "Closed Lost" &&
!$opp.Auto_Renewal_Opt_Out__c &&
$opp.Type == "Auto Renewal - Pending"
)
#set($matchingOpp = $opp)
#break
#end
#end
## --- Output calculated date values ---
#if($matchingOpp != "" && $matchingOpp.Pre_Auto_Renewal_Notification_Date__c)
## Parse string to Date
#set($baseDate = $convert.parseDate($matchingOpp.Pre_Auto_Renewal_Notification_Date__c, $ISO8601DateOnly, $defaultLocale, $defaultTimeZone))
## Convert to Calendar and add days
#set($cal30 = $convert.toCalendar($baseDate))
#set($cal90 = $convert.toCalendar($baseDate))
$cal30.add($calConst.DATE, 30)
$cal90.add($calConst.DATE, 90)
## Format and print output
$date.format($ISO8601DateOnly, $cal90, $defaultLocale, $defaultTimeZone)#else
no date found#end
Solved! Go to Solution.
Of course!
#set( $CustomDateFormat = "MM/dd/yyyy" )
That’s what you pass to $date.format()
, not to $convert.parseDate()
.
Yep, although I’d use a real Boolean as your check as opposed to an empty String. Plus a little cleanup in there to use .equals()
and avoid unnecessary output.
## --- Set required Velocity variables for timezone and formatting ---
#set( $defaultTimeZone = $date.getTimeZone().getTimeZone("America/New_York") )
#set( $defaultLocale = $date.getLocale() )
#set( $calNow = $date.getCalendar() )
#set( $ret = $calNow.setTimeZone($defaultTimeZone) )
#set( $calConst = $field.in($calNow) )
#set( $ISO8601DateOnly = "yyyy-MM-dd" )
#set( $CustomDateFormat = "MM/dd/yyyy" )
## --- Find first matching opportunity ---
#set($matchingOpp = false)
#foreach($opp in $OpportunityList)
#if(
$opp.Pre_Auto_Renewal_Notification_Date__c &&
!$opp.Stage.equals("Closed Lost") &&
$opp.Auto_Renewal_Opt_Out__c.equals(false) &&
$opp.Type.equals("Auto Renewal - Pending")
)
#set($matchingOpp = $opp)
#break
#end
#end
## --- Output calculated date values ---
#if( $matchingOpp )
## Parse string to Date
#set($baseDate = $convert.parseDate($matchingOpp.Pre_Auto_Renewal_Notification_Date__c, $ISO8601DateOnly, $defaultLocale, $defaultTimeZone))
## Convert to Calendar and add days
#set($cal30 = $convert.toCalendar($baseDate))
#set($cal90 = $convert.toCalendar($baseDate))
#set( $void = $cal30.add($calConst.DATE, 30))
#set( $void = $cal90.add($calConst.DATE, 90))
## Format and print output
$date.format($CustomDateFormat, $cal90, $defaultLocale, $defaultTimeZone)##
#else
no date found##
#end
I seemed to have unknowingly stumbled upon Velocity's ability to declare and reference variables across multiple tokens.
Of course. A Marketo email is one big Velocity template!
All the VTL components share a single scope, just as they would if placed end-to-end in a single {{my.token}}. Splitting across multiple tokens is highly recommended because it allows modular development. Don’t know where I’d be without the ability to use multiple tokens — some inherited, some local.
Issue Statement
When multiple Velocity script tokens exist in the same email asset, Marketo appears to stop processing subsequent tokens once the first one is removed
No, that’s not true.
Most likely cause is you have certain fields checked off in the tree in Script Editor in Token 1, but you didn’t check them in Token 2/3. (This a vital feature as only one token needs to have the fields checked. But you need to include that one!)
Hi Sanford,
Thank you so much for the quick reply! That was totally it - the initial debugging token had all of the field checked off in the template but the production tokens didn't.
However, that brings me to my next question. I'm now seeing the following values that I didn't see before.
$cal30.add($calConst.DATE, 30) $cal90.add($calConst.DATE, 90) $date.format($ISO8601DateOnly, $cal30, $defaultLocale, $defaultTimeZone)
When you see raw Velocity code, that’s because there was an error calling a method (this is how Velocity avoids throwing fatal errors all the time, although it’s still possible to have fatal errors in lots of cases).
To help further I’d need to see the raw output of
#foreach( $oppty in $OpportunityList)
#foreach( $kv in $oppty.entrySet() )
$kv.getValue().class $kv.getKey() $kv.getValue()
#end
#end
Hi Sandy,
Here's the raw ouput from the script provided:
class java.lang.String
Type
Renewal
class java.lang.String
Stage
Stage 1: Unengaged Renewal
class java.lang.String
Auto_Renewal_Opt_Out__c
1
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2025-06-23
class java.lang.String
Name
[REDACTED]
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
Renewal
class java.lang.String
Stage
Stage 1: Unengaged Renewal
class java.lang.String
Auto_Renewal_Opt_Out__c
1
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2025-06-23
class java.lang.String
Name
Ryse Renewal Q3 2025
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
Renewal
class java.lang.String
Stage
Stage 1: Unengaged Renewal
class java.lang.String
Auto_Renewal_Opt_Out__c
1
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2025-06-23
class java.lang.String
Name
[REDACTED]
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
Renewal
class java.lang.String
Stage
Stage 1: Unengaged Renewal
class java.lang.String
Auto_Renewal_Opt_Out__c
1
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2025-06-23
class java.lang.String
Name
[REDACTED]
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
Renewal
class java.lang.String
Stage
Stage 1: Unengaged Renewal
class java.lang.String
Auto_Renewal_Opt_Out__c
1
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2025-06-23
class java.lang.String
Name
[REDACTED]
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
Auto Renewal - Paying
class java.lang.String
Stage
Closed Won
$kv.getValue().class
Auto_Renewal_Opt_Out__c
$kv.getValue()
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2024-06-23
class java.lang.String
Name
Ryse Renewal Q3 2024
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
Auto Renewal - Paying
class java.lang.String
Stage
Closed Won
$kv.getValue().class
Auto_Renewal_Opt_Out__c
$kv.getValue()
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2024-06-23
class java.lang.String
Name
[REDACTED]
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
Auto Renewal - Paying
class java.lang.String
Stage
Closed Won
$kv.getValue().class
Auto_Renewal_Opt_Out__c
$kv.getValue()
class java.lang.String
Pre_Auto_Renewal_Notification_Date__c
2024-06-23
class java.lang.String
Name
[REDACTED]
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
class java.lang.String
Type
New Business
class java.lang.String
Stage
Closed Won
$kv.getValue().class
Auto_Renewal_Opt_Out__c
$kv.getValue()
$kv.getValue().class
Pre_Auto_Renewal_Notification_Date__c
$kv.getValue()
class java.lang.String
Name
[REDACTED]
$kv.getValue().class
Opportunity_Status_StageName__c
$kv.getValue()
You changed the value of $ISO8601DateOnly
to something that’s definitely not ISO 8601. It must be
#set( $ISO8601DateOnly = "yyyy-MM-dd" )
Thank you, Sandy!
Is there a way to format the date to MM/DD/YYYY?
I think I understand. So is this then the correct iteration of script?
## --- Set required Velocity variables for timezone and formatting ---
#set( $defaultTimeZone = $date.getTimeZone().getTimeZone("America/New_York") )
#set( $defaultLocale = $date.getLocale() )
#set( $calNow = $date.getCalendar() )
#set( $ret = $calNow.setTimeZone($defaultTimeZone) )
#set( $calConst = $field.in($calNow) )
#set( $ISO8601DateOnly = "yyyy-MM-dd" )
#set( $CustomDateFormat = "MM/dd/yyyy" )
## --- Find first matching opportunity ---
#set($matchingOpp = "")
#foreach($opp in $OpportunityList)
#if(
$opp.Pre_Auto_Renewal_Notification_Date__c &&
$opp.Stage != "Closed Lost" &&
!$opp.Auto_Renewal_Opt_Out__c &&
$opp.Type == "Auto Renewal - Pending"
)
#set($matchingOpp = $opp)
#break
#end
#end
## --- Output calculated date values ---
#if($matchingOpp != "" && $matchingOpp.Pre_Auto_Renewal_Notification_Date__c)
## Parse string to Date
#set($baseDate = $convert.parseDate($matchingOpp.Pre_Auto_Renewal_Notification_Date__c, $ISO8601DateOnly, $defaultLocale, $defaultTimeZone))
## Convert to Calendar and add days
#set($cal30 = $convert.toCalendar($baseDate))
#set($cal90 = $convert.toCalendar($baseDate))
$cal30.add($calConst.DATE, 30)
$cal90.add($calConst.DATE, 90)
## Format and print output
$date.format($CustomDateFormat, $cal90, $defaultLocale, $defaultTimeZone)#else
no date found#end
I initialized $CustomDateFormat at the top of the template and passed the variable to $date.format at the bottom.
Yep, although I’d use a real Boolean as your check as opposed to an empty String. Plus a little cleanup in there to use .equals()
and avoid unnecessary output.
## --- Set required Velocity variables for timezone and formatting ---
#set( $defaultTimeZone = $date.getTimeZone().getTimeZone("America/New_York") )
#set( $defaultLocale = $date.getLocale() )
#set( $calNow = $date.getCalendar() )
#set( $ret = $calNow.setTimeZone($defaultTimeZone) )
#set( $calConst = $field.in($calNow) )
#set( $ISO8601DateOnly = "yyyy-MM-dd" )
#set( $CustomDateFormat = "MM/dd/yyyy" )
## --- Find first matching opportunity ---
#set($matchingOpp = false)
#foreach($opp in $OpportunityList)
#if(
$opp.Pre_Auto_Renewal_Notification_Date__c &&
!$opp.Stage.equals("Closed Lost") &&
$opp.Auto_Renewal_Opt_Out__c.equals(false) &&
$opp.Type.equals("Auto Renewal - Pending")
)
#set($matchingOpp = $opp)
#break
#end
#end
## --- Output calculated date values ---
#if( $matchingOpp )
## Parse string to Date
#set($baseDate = $convert.parseDate($matchingOpp.Pre_Auto_Renewal_Notification_Date__c, $ISO8601DateOnly, $defaultLocale, $defaultTimeZone))
## Convert to Calendar and add days
#set($cal30 = $convert.toCalendar($baseDate))
#set($cal90 = $convert.toCalendar($baseDate))
#set( $void = $cal30.add($calConst.DATE, 30))
#set( $void = $cal90.add($calConst.DATE, 90))
## Format and print output
$date.format($CustomDateFormat, $cal90, $defaultLocale, $defaultTimeZone)##
#else
no date found##
#end