Author's Posts

Once upon a time there was a neat trick that SFMC Developers could use to quickly write and run code in Marketing Cloud. You could create a Cloud Page Landing Page, write your code, and press Preview – this would quickly run the code without publishing the Cloud Page or consume any Super message credits.

However a recent update to how Cloud Pages handles programmatic code (specifically SSJS) has made this rapid development method inoperable. Marketing Cloud also has a 5 minute publishing window for changes made to Cloud Pages, so simply updating your code and republishing it can be a very time consuming activity.

Luckily, there is still a workaround that you can use to quickly build and test your programmatic code!

Use Cases

Working with large blocks of code or 3rd party APIs can sometimes take some trial and error to get right, so unit testing with text outputs can really speed up development. I’ve also had tasks that you just need to process once – such as separating email addresses from their email domain – and having a quick workbench to write the necessary script and run it once can save you a lot of hassle.

How it works

In Content Builder, create a HTML block and paste the programmatic code you want to test. Save the Content Block and copy the Content Block ID from Content Builder.
Next, create a new Cloud Page, and insert the ContentBlockbyId() AMPscript function, pasting the Content Block ID from your saved HTML Block.
Save and Publish the Cloud Page.

Content Block containing some SSJS code.
Cloud Page using ContentBlockbyId() to reference the SSJS Content Block

Now you can access the Cloud Page via it’s published URL. Each time you access the Cloud Page, it will make a dynamic lookup to the HTML Content Block, meaning you can update the content block and refresh the Cloud Page to see changes immediately!

The drawback to this method is that it does consume a Super message credit each time the Cloud Page is reloaded. Depending on the code you are testing and the messages/details you want to print on the page, you can use one of the Code Resource page types to get around the super message cost, however be carful of which code page type you choose as they each contain different headers which could affect your testing.

Conclusion

There are loads of other ways that members of the SFMC community have found – including using Github repositories – to speed up development in Marketing Cloud. See what works for your development style, but always check if your solution introduces any security or utilization concerns!

Read more

One of my most common use cases for SSJS is making API calls in Automation Studio using the Script Activity. Building an automation that can retrieve or refresh data from an external source can open up so many amazing communication opportunities.

You could get the local weather data feed and use it to advertise weather appropriate products or destinations – I actually did a video on this here: https://www.youtube.com/watch?v=vCSCnoiR5ww
You could get up to date price data on hotels or flights to identified cheap travel opportunities; or even query the daily movement of bitcoin for your subscribers!

However one thing that is common with APIs is the need for a “date” field to specify a range of time to retrieve data for – and while AMPscript gives us some very intuitive date functions to work with – SSJS can be a little trickier to use. So lets go through a few basic principals that can help you to master the datetime data type in Javascript!

Step 1: Getting a Date value

There are many ways you can get and set a date value in Javascript, including setting the value manually, retrieving it from a Data Extension, or even rendering it in real time. Here are some examples:

var static_date = "January 1, 2021";

var de_date = Platform.Function.Lookup('CustomerData','DateofBirth',['FirstName','LastName'],['Angela','Ruiz'];

var Current_date = new Date();

Step 2: Convert String to a Date

Now we need to convert the datetime value into the Javascript “date” datatype. By converting your string or number value into the date datatype, Javascript will be able to use some of the specific date functions. You can read more about JS date functions here: https://www.w3schools.com/jsref/jsref_obj_date.asp

Lets use the date values we captured above. We can use the following JS codes to convert these dates into the correct JS date objects:

var static_date = new Date("January 1, 2021");

var de_date = Platform.Function.Lookup('CustomerData','DateofBirth',['FirstName','LastName'],['Angela','Ruiz'];
var new_de_date = new Date(de_date);

var Current_date = new Date();

Step 3: Altering a Date

Now that we have date objects, we can make adjustments to the date values easily using the JavaScript Date Functions. This could be needed for triggering a reminder communication a few hours before an appointment, or for setting a voucher expiry 30 days into the future. To achieve this, we can use the setHours() function to add or subtract hours from the date, or we could use the setDate() function to add or subtract days from the date. Here are some example of these functions in use:

static_date.setDate(static_date.getHours()-6); //subtracts 6 hours from the "static_date" value


new_de_date.setDate(new_de_date.getDate()+30); //adds 30 days to the "new_de_date" value

Current_date.setMonth(13); //adds 1 year to the "Current_date" value

Note that at the time of writing, Salesforce Marketing Cloud has a problem with the “setFullYear()” function, however the “setMonth(13)” workaround shown above is a suitable way to add years to your dates.

Step 4: Output your Date value

Now it’s time to output the date value into your code. I’ve seen a number of different date format requirements in APIs, so lets step through a little trick I use to make date formats as easy as possible.

Lets assume we need to produce the following date structure for our API call: 2021-05-14 00:00:00
The current date that we have in our date value is: Fri, 14 May 2021 20:58:13 GMT-06:00
So we can manually output the date parts in the correct format by using the following codes:

var NowDate = new Date("Fri, 14 May 2021 20:58:13 GMT-06:00");
var output= NowDate.getFullYear()+'-'+("0"+(NowDate.getMonth()+1)).slice(-2)+'-'+NowDate.getDate()+" 00:00:00";
//Output: 2021-05-14 00:00:00

What the code is doing:
Firstly we are setting the “NowDate” value using new Date();
Next we are building the “output” value by concatenating each of the date parts as we need them:
getFullYear() will return the YYYY date part.
getMonth() will return the month number from 0 to 11, so we need to +1. Some APIs enforce a 2-digit month value, so we can add a “0” to the front and use the slice() function to return only the last 2 characters in the string.
The getDate() function will give us the date from 1 to 31.
And lastly the ” 00:00:00″ is added to be compliant with the example date time format, assuming we wish to get data starting from midnight. The getHours(), getMinutes(), and getSeconds() functions could have been used to specify these dynamically if required.

Summary

I’ve seen some creative examples of people using AMPscript to set and alter the date values before using them in SSJS – however working with Dates in SSJS doesn’t need to be a burden. Below is a copy of my code snippet that I use for quickly building dates for API calls:

//Format: YYYY-MM-DD 00:00:00
var Today = new Date(); 
var Yesterday = new Date();
Yesterday.setDate(Yesterday.getDate()-1);

var Today_string = Today.getFullYear()+'-'+("0"+(Today.getMonth()+1)).slice(-2)+'-'+Today.getDate()+" 00:00:00";
var Yesterday_string = Yesterday.getFullYear()+'-'+("0"+(Yesterday.getMonth()+1)).slice(-2)+'-'+Yesterday.getDate()+" 00:00:00";
Read more

A little while ago I encountered an interesting issue; a prospect was unable to complete a form on a cloud page, and after some isolation testing I found that their email address was the problem – they we’re using a “Role-Based” email address which apparently Salesforce Marketing Cloud didn’t like.

Salesforce Support was able to confirm that “News@<domain>” is in fact a “Role-Based” email that is blocked in Marketing Cloud, however they weren’t willing to provide a list of other email usernames that are blocked by the “List Detective” function. This was a problem for me as I wanted to ensure that other known/safe email usernames could also be pre-emptively added to the List Detective exemptions.

After some quick research into Role Based emails, I came across this repository of email usernames, which was far more extensive than the Pardot Role-Based and Reserved Email Addresses.

Working with Abikarsa Kristanto from Datarati, we we’re able to append a safe domain to all ~950 usernames and load them into the All Subscribers list in Salesforce Marketing Cloud. Since the List Detective triggers during subscriber imports, the emails that failed to import would be blocked, giving us a list of all (at the time & from this list) email usernames that are in List Detective.

From ~950 emails, only 28 were blocked – as below:

  • abuse
  • admin
  • email
  • feedback
  • hostmaster
  • info
  • junk
  • mail
  • mailerdaemon
  • marketing
  • news
  • newsletter
  • nobody
  • none
  • noreply
  • nospam
  • null
  • ops
  • orders
  • postmaster
  • press
  • remove
  • root
  • security
  • spam
  • subscribe
  • unsubscribe
  • www

I hope this list saves you time and helps any fellow trailblazers who find themselves in a similar situation. Again a huge thanks to Abikarsa Kristanto for his efforts on this discovery!

Read more

To prevent spam and malicious emails from reaching your inbox, email service providers filter out and block emails from suspicious IP addresses. You’re fresh new IP address rolled out in your Sender Authentication Package with Salesforce Marketing Cloud is suspicious in the eyes of most inboxes, and to protect their users most providers take a “guilty until proven innocent” approach to new IPs, So an IP Wamp-ip activity is needed to prove that you are a safe sender.

To oversimplify it, IP Warm-up (or IP Ramp-up) is the process of “getting email inbox providers to trust you”. It’s a necessary task to ensure your emails make it to your customers inboxes and don’t get flagged as spam.

Warming up your new Salesforce Marketing Cloud Email Sending IP can be a daunting task for some businesses since there is very little margin for error, so here are some quick ways to assess your subscriber database, plan a successful IP Warm-up Strategy, and reduce the impact to your business during this process.

Understand your database

Run a report on your subscribers’ email addresses to see what domains they use. Some enterprise marketing systems have this feature built in; however you can also run a quick SQL query if needed, for example:

SELECT
	RIGHT(subs.EmailAddress, LENGTH(subs.EmailAddress)-INSTR(subs.EmailAddress, '@')) as 'Domain'
	,COUNT(*) as 'Count'
FROM [mySubscribers] subs
GROUP BY RIGHT(subs.EmailAddress, LENGTH(subs.EmailAddress)-INSTR(subs.EmailAddress, '@'))

The above SQL will strip the email username’s off every email address – leaving just the domain sections – and then count how many email addressed had that domain. Large public email providers like Gmail and Hotmail will top the counts in most circumstances. By itself this is already a really useful data set, however one more thing needs to be done before we can use this information for planning out IP Warm-up Strategy.

Some email providers operate from multiple addresses, and we need to group any related domains together. For example, Microsoft owns @hotmail.com, @outlook.com, @live.com and @msn.com – to name a few – so it’s important to group these all as “Microsoft Domains”.

You’ll also find that 95% of the unique domains will only have a handful of subscribers each. For all the domains with under 1000 subscribers you can group them into a segment called “other”.

IP Warm-Up: Strategy Overview

The goal of your IP Warm-up strategy is to gradually increase the reputation of your email sending IP address so that you can send an email to your entire database at once.

One of the ways Email service providers protect their users from spam is by limiting the volume of emails coming from an IP address over a 24 hour period. For a fresh new IP address this is typically around 20,000 emails per 24 hour period, however some providers are more restrictive and only allow 10’000 or 5’000 per day!

Additionally, inbox providers are watching how your subscribers interact with your emails; do they open, click, or mark as spam? Sending IP addresses that frequently get ignored or marked as spam will find themselves relegated to the junk box very quickly!

Knowing these daily safe limits and key indicators that the inbox providers are looking for, you can build a sending schedule that demonstrates how safe and valuable your emails are – ensuring you get that VIP inbox placement from the spam filters.

IP Warm-Up: Daily Limits

The Internet is littered with Daily (and Hourly) volume recommendations, ranging from <100 right up to 20,000 from day 1. In truth, there isn’t a direct science here since each provider keeps their spam filter rules a tightly guarded secret. With that in mind, here are a few resources you can check out to get more information about the various recommended daily limits.

In my experience, starting with around 5,000 per domain-group per 24 hour period, then doubling it every 7 days is a safe warm-up strategy. Note that Microsoft and Gmail have notoriously sensitive spam filters, so starting at 2,000 (or even 500) per day for these providers would be a very risk-adverse strategy.

The “Other” segment (made up of all the smaller domains with only a few emails addresses each) can be sent to 100% from day 1. This group doesn’t need to be IP Warmed-up since each domain has a volume that is too small to trigger any send volume issues.

Doubling the daily limits each week will soon escalate to large numbers for each domain very quickly, and for some smaller domains you will quickly run out of subscribers to send to. Once you’ve met or surpassed the daily requirement for a domain, you can remove them from you Warm-up segmentation process and just send to them normally, you don’t need to send to the same group multiple times to fulfil the sending thresholds.

IP Warm-Up: Email Engagement

After your email sending volume, the next most important element of your IP Warm-Up strategy is ensuring your subscribers are engaging positively with your first few emails. The content of your emails is a key consideration of course, however there is one strategy you can use to improve overall engagement from the beginning – send to your best subscribers first!

Ordering your IP Warm-up email segments from highest to lowest engagement (recent Clicks & Opens) is the best way to ensure your first few sends get really strong open and click rates! When I’m working on a warm-up strategy, I use the following formulae in Salesforce Marketing Cloud to sort subscribers:

SELECT
 s.EmailAddress
 ,(DATEDIFF(day, DateAdd(dd,-91,GETDATE()),Last90Click) * Count90Click * 5) + (DATEDIFF(day, DateAdd(dd,-91,GETDATE()),Last90Open) * Count90Open) as 'Score'
 FROM ent._subscribers s
 LEFT JOIN (
     SELECT
     Subscriberkey
     ,MAX(EventDate) as 'Last90Open'
     ,Count() as 'Count90Open'
     FROM ent._Open
     WHERE EventDate > DateAdd(dd,-90,GETDATE())
     GROUP BY Subscriberkey ) o ON o.Subscriberkey = s.Subscriberkey
 LEFT JOIN (
     SELECT
     Subscriberkey
     ,MAX(EventDate) as 'Last90Click'
     ,Count() as 'Count90Click'
     FROM ent._Click
     WHERE EventDate > DateAdd(dd,-90,GETDATE())
     GROUP BY Subscriberkey
 ) c ON c.Subscriberkey = s.Subscriberkey

The SQL above produces a list containing every “EmailAddress” and a “Score” based on their clicks and opens over the last 90 days; with high scores indicating higher engagement. Records that don’t have a score next to them have no open or clicks in the last 90 days.

Remember that the goal of the IP Warm-up is to show the email service providers that you send high-quality emails that your subscribers want to see in their inboxes. For this reason you need to focus on subscribers that have a high propensity to open and click your emails – the metrics that count in the inbox. Don’t make the mistake of including high purchase value or membership duration figures into your engagement calculations; which these may be your “high value” subscribers, they are not the subscribers you need for this activity.

IP Warm-Up: Example

Below is an IP Warm-up Strategy example for a Retail (B2C) customer who sends 1 newsletter each week to their whole database. Firstly, the subscriber database is reviewed and grouped into Domains and Engagements.

Example of a database containing 767,436 Subscribers grouped into 8 Warm-Up Domains, showing recent engagement scores

From the above, we can see that a large majority of subscriber emails belong to Gmail and Hotmail, however almost 1/3 also belong to miscellaneous addresses grouped into “Other”.

Next, we can plan out how many emails can be sent each day, remembering to keep under the daily limit for each domain group. For this example I’ve used 5,000 for each domain and included a 5-day sending schedule – however I recommend selecting a starting volume and send schedule that is inline with your research and risk tolerance.

Looking at the above sheets, we can see that each week we are increasing in daily send volume, and by Week 3 there are already 3 domain groups that we can sent to completely in 1 day, with 3 more that would be fully sendable by Week 4.

The key insight that sits behind this sheet is that higher engaging subscribers are sent to ahead of lower engaging subscribers. This means that even though by Week 3 the “Yahoo, Telstra and TPG” domain groups are not being 100% sent to on 1 day, their “12229, 15117 and 12977” engaged subscriber populations are being sent on 1 day!

So even though the email hasn’t been sent to everyone, the subscribers who were most likely to Open and Click the email have being sent to, meaning your email campaign will still get a good volume of opens and click; thus reducing the business impact of the IP Warm-up!

Summary

An IP Warm-up is like a first date with the inbox providers; you only really get one chance to make a good impression (and it’s very difficult to get a second date if the first one didn’t go so well) – however if you put your best foot (subscribers) forward and follow some basic rules, it can result in a long lasting relationship!

Like all first dates, a bit of thought and preparation will go a long way, and after a while you’ll see there’s really nothing to be scared of. However if you think you need a wing-person to help you through the process, I’ve personally had some great experiences with the team at Validity (Formally Return Path), they’ve got some great resources and blogs to help navigate the email & deliverability landscape!

Read more

From my very first days in Salesforce Marketing Cloud learning how to create personalised content, I was always told that AMPscript is far easier to learn and much raster to run. While most typical digital marketers will agree with the first point, I’ve always held a shred of scepticism for the latter; is SSJS really slower than AMPscript?

I’ve been spending more time using SSJS recently, so I thought it was high time to qualify this long standing bias.

Methodology

To make these tests as fair as possible, I’ll be running each language in a cloud page with timestamp markers at the top and bottom of the code block being tested. The AMPscript and SSJS test functions will be as close as possible to one another, and I will run each code multiple times to achieve an average.
The following code was added to the top & bottom of each test to track the run times:

<script runat="server">
Platform.Load("Core","1");
var startDate = new Date();
</script>

//Code being tested

<script runat="server">
var endDate   = new Date();
var seconds = (endDate.getTime() - startDate.getTime()) / 1000;
Write("<br><br>Call took "+seconds+" seconds.");
</script>

Test 1 – A simple For Loop with no outputs

My first test was a simple FOR LOOP, testing how quickly the AMPscript and SSJS can execute a cruel 1 million loops.
The codes used for each language were as follows:

%%[
FOR @i=1 To 1000000 DO
NEXT
]%%
<script runat="server">
for (i = 0; i < 1000000; i++) {
}
</script>

AMPScript was able to complete this task in an average 7.5 seconds, while SSJS ran in an average 2.5 seconds!

I upped the ante to a near sisyphean task of 5 million loops (sorry Salesforce) to create more difference between the times.
AMPscript again came in 2nd place with an average 33 seconds, and SSJS completed in a respectable 12 seconds.

This was not what common knowledge was predicting would happen, so I altered the test conditions to see if I could find a weak point.

Test 2 – A Simple For Loop with some personalised text outputs

Similar to the first test, however this time there would be outputs. The “name” variable would be set on every loop to simulate personalisation. I also lowered the loop count to 10,000 to make sure the page load time could handle all the data being transferred.
The codes I tested were as follows:

%%[
FOR @i=1 To 10000 DO
SET @name = "Test"
output(concat("Hi there ",@name, " ", @i, "<br>"))
NEXT
]%%
<script runat="server">
for (i = 0; i < 10000; i++) {
var name = "Test"
  Write("Hi there " + name + " " + i + "<br>");
}
</script>

The AMPscript code ran in under 0.01 seconds every time, while the SSJS code averaged 4.5 seconds! To confirm the findings, I whispered a small prayer and ran it a few times at 100,000 loops.

AMPscript had processed all 100k rows in 0.75 seconds, while SSJS took on average 37 seconds to complete it’s 2.2 MB payload.

This was more inline with my expectations; however given how well SSJS performed in the non-output tests, LOOPS weren’t the problem, and there was clearly something more worth testing about how each language handled outputting data.

Test 3 – DE Lookups

A more practical and real world example is looking up and presenting personalised data from data extensions during an email send or cloud page load. To simulate this test I downloaded a list of over 900 Lord of the Rings characters (link) and loaded it into a DE. The code below looks up that data and then produces a table with all of the character info, which was looped over 10 times – producing over 9000 rows – to help emphasise a winner.

<table border="1">
<tr><th>Row</th><th>name</th><th>birth</th><th>death</th><th>gender</th><th>race</th></tr>  
%%[
FOR @x=1 TO 10 DO
SET @lotr = LookupRows('LOTR Characters','Show',1)
FOR @i=1 TO RowCount(@lotr) DO]%%
<tr><td>%%=v(FIELD(ROW(@lotr,@i),'Row'))=%%</td><td>%%=v(FIELD(ROW(@lotr,@i),'name'))=%%</td><td>%%=v(FIELD(ROW(@lotr,@i),'birth'))=%%</td><td>%%=v(FIELD(ROW(@lotr,@i),'death'))=%%</td><td>%%=v(FIELD(ROW(@lotr,@i),'gender'))=%%</td><td>%%=v(FIELD(ROW(@lotr,@i),'race'))=%%</td></tr>
%%[NEXT
NEXT]%%
</table>
<table border="1">
<tr><th>Row</th><th>name</th><th>birth</th><th>death</th><th>gender</th><th>race</th></tr>  
<script runat="server">
  for (x = 0; x < 10; x++) {
var lotr = DataExtension.Init("battleoftwolanguages");
var data = lotr.Rows.Lookup(["Show"], [1]);
  for (i = 0; i < data.length; i++) {
  Write("<tr><td>"+data[i]['Row']+"</td><td>"+data[i]['name']+"</td><td>"+data[i]['birth']+"</td><td>"+data[i]['death']+"</td><td>"+data[i]['gender']+"</td><td>"+data[i]['race']+"</td></tr>");
  }
}
</script>
</table>

AMPscript consistently ran 10 loops in under 0.5 second; while SSJS took 9 seconds to complete 10 loops. Not forgetting how well SSJS ran in the first test, I removed the text outputs (leaving only the lookup functions), and increased the loops to 100. This would remove the now known SSJS Achilles heal of text outputs and just test the lookup function.

After rerunning each language a few times, AMPscript was the clear winner at around 0.25 second for 100 loops, while SSJS really lagged behind on 25 seconds.

This confirmed that raw looping functions seemed to be SSJS’s strength, while it really struggled with any data or text handling. However there was still 1 practical use case left to test.

Test 4 – API GET Request

I decided to host a small .txt file on my web-server and use the AMPscript and SSJS HTTPGET requests to see how well each of them handles getting data from an external source. The text file only contained the word “empty” to keep data transfer size low, and I added the LOOP count to the call to ensure each request was unique (to bypass any caching).

I placed the call in a FOR LOOP for 50 cycles, as below:

%%[
FOR @x=1 TO 50 DO
set @HTMLContent = HTTPGet(concat(".../text.txt?i=",@i),false,0,@CallStatus)
output(concat(@HTMLContent,"<br>"))
NEXT
]%%
<script runat="server">
for (x = 0; x < 50; x++) {
var response = HTTP.Get(".../text.txt?x="+x);
  Write(response.Content + '<br />');
}
</script>

AMPscript continued it’s winning streak with a consistent run time of under 1 second, while SSJS took an average 20 seconds to complete all 50 calls.

I removed the output/write lines to see if that was affecting the times (like it did in previous tests), however after a few runs the execution times remained largely the same.

Summary

I think most SFMC Trailblazers would agree this was an unsurprising result, however it also wasn’t as bad as I thought it was going to be. Given most of these tests were run using FOR LOOPS that far exceed any practical use case, the run time difference for small batch processes would be practically negligible. Enterprise customers sending emails to millions of customers would experience a send time difference using SSJS for personalisation – however that kind of scale attracts other operational limitations/bottlenecks anyway.

For me the key learning has been that AMPscript is a faster language for Marketing Cloud to process, so use it over SSJS where possible; however don’t avoid SSJS for fears of degraded performance.
SSJS has some amazing benefits and strengths over AMPScript – such as JSON handling, WSProxy and GET Requests with headers – and it’s worth your time to understand the benefits and applications of each language.

Read more

The following article assumes you have read/completed the following:

How the GA360 Connector works

Put simply, the GA360 Connector creates a link between your Google Analytics 360 and Salesforce Marketing Cloud accounts that enables Google to send non-personally identifiable information to SFMC for the purpose of re-marketing.

This is achieved with the following user/data flow:

  1. An email is sent from SFMC to a Subscriber with personalized links/tracking. The Subscriber clicks on the email and is sent through to the company website with tracking parameters; including “sfmc_id=”.
  2. Google Analytics detects these parameters and stores the “sfmc_id” value against it’s own Google ID.
  3. As users interact with your website, Google Analytics checks the Audience Definitions in your account to see if they qualify with any of the definitions you’ve built.
  4. When a web user has qualified for an audience that has been connected with Salesforce Marketing Cloud, Google Analytics checks to see if they have a “sfmc_id” value stored. If the user has a sfmc_id, then Google Analytics sends the value through to Salesforce Marketing Cloud as a Synchronized Data Source.

How tracking changes everything

Not so long ago, the only data most email marketers had access to was Open/Click data from their email platform, and any uploaded 1st party data such as customer information or purchases. Now with the GA360 connector, email marketers can access website traffic generated by their subscribers, closing the loop and uncovering the interactions that take place in-between the email engagement and purchase.

Best of all, the website tracking data isn’t limited to the session that started with an email click! Google Analytics stores the sfmc_id value against their own customer identifier; so your subscribers are tracked even if they return to the website days later and from different channels!

The advanced conditions & sequences in Google’s Audience Builder allows marketers to create an incredible range of re-targeting opportunities. Google has created a sample list of activities you can create using Audience Builder, however the sky is the limit!

Activating your audiences in Salesforce Marketing Cloud

After you’ve created your Google Audiences and selected Salesforce Marketing Cloud as the Audience Destination, it will take up to 24 hours for them to appear in SFMC. For Australian users, Google Analytics publishes audience data at 4am EST each day.

The standard way to activate this data is via a GA360 Entry Source in Journey Builder. This will be sufficient for most use cases, however it can become impractical when you have multiple audiences/activities that a subscriber could qualify for at the same time.
There is a way to access GA360 Audience data in a Data Extension:

  1. Create a new Journey using the “Google Analytics 360” entry source.
  2. Add some flow controls to the journey – such as a Random Split or Wait Activity – and press save.
  3. Close and reopen the Journey to refresh the interface.

If you look at the details in the Entry Event tile you will see the “Data Extension Name” has a funny value starting with “EA_”. This is the name of the synchronized Data Extension used by Google Analytics. Since this is a Data Extension, it can be accessed just like every other DE in SFMC.
The following SQL can be used to access a GA360 Audience Data Extension:

SELECT
SubscriberKey
FROM ent.[EA_xxxxxxx_X_xxxxxx]

More ways to activate web traffic

Accessing the data directly allows marketers to use GA Audience data to create opportunities outside of the standard Journey Builder activation. Rather than thinking of GA Audiences as a tool to create re-targeting segments to activate on directly – we can expand the use cases to include data sets based on customer behaviors that may not be individually actionable, but can instead support other activities.

For a B2C Retail/eCommerce example, we could make an audience that captures web users who have visited over 10 generic pages, but have not made it further down the conversion funnel into the shopping cart. We could use this audience data to infer that the customer is just browsing and not in a purchase state of mind, and then suppress them from more sales-heavy sends.

Alternatively, we could make an audience for customers that have visited the “shipping prices” page in the same session as an “Add to Cart” event, but has not clicked purchase. Normally this would trigger an “Abandoned Cart” activity, however we could infer that this customer is conscious of the shipping cost, and perhaps we could add them to a DE of Subscribers in SFMC that will have a “Free Shipping” promotion banner added in the body of their next EDM.

Closing thoughts

Listening to activities after the initial outbound click of an email gives marketers access to each customers full digital story, and these insights can help to personalized targeted messages and make you customers feel like you truly understand their experience with your brand.

Unassuming actions such as a visit to the Contact Us or Shipping Information page are not reason enough to trigger an activity or communication, but are a great modifiers to existing activities like Card Abandonment, Welcome Emails or general Sales and Newsletters sends.

Read more

Re-Engagement (re-activation, win-back, lapsed subscribers, dormant, etc) Campaigns are a core feature of every good customer marketing strategy, and while the immediate goal of these campaigns is well understood – to retain keep existing customers engaged – the true benefits of these targeted and automated communications is often lost on non-CRM marketers.

It’s time to talk in detail about one of the highest performing campaigns in an email marketers toolkit.

The benefits of a Re-Engagement Journey

Re-Engagement is cheaper than Re-Acquisition

Most companies know their allowable cost per acquisition – the cost they are willing to spend to acquire a new customer based on their predicted lifetime value. Most of the time the lions share of these costs come from above the line campaign spend in display, social, search, and other paid media channels.
Reinvesting this cost as a voucher to a lapsing customer via a re-engagement journey can significantly reduce media spend to re-acquire them later down the track. Re-Engagement vouchers have the added benefit of being a soft/internal cost to your business; meaning they only cost if they are being used, which also means they are working!

Reduce Subscriber Churn

Your churn rate isn’t simply the number of subscribers who click unsubscribe – it’s also all the subscribers who stop opening your emails! While these 2 subscriber states are functionally the same for your business – your emails are not being seen by customers – they are materially different for your database health and operating costs. Sending emails to customers who are never going to open them is a waste of your time, money and reputation (more on that shortly).
You can reduce churn by detecting lessening engagement and reaching out with a non-sales message.

Improve Subscriber Database Health

Sometimes your subscribers will become disengaged and there’s nothing you can do to prevent it – they no longer need to be affiliated with your brand – however they won’t say it to your face (your unsubscribe page). The best thing you can do is to let them go.
If a subscriber fails to re-engage during a win-back campaign, unsubscribe them. Tell them why they are being unsubscribed and how they can subscriber again in the future. Removing these records from your active database will decrease your send volumes/costs, and artificially improve your email open rates.

Improve Domain Health & Reputation

One of the lesser known benefits of a Re-Engagement Campaign is the impact it has to your sending IP address. Inbox providers are watching your sends; they review how many of your emails bounce, don’t get opened or are flagged as spam! Inbox providers are known for requisitioning abandoned inboxes and converting them into “spam traps”; inboxes that silently connect the addresses of senders who don’t adhere to good email database principals.
Your email domain reputation is affected negatively by disengaged subscribers and spam traps, and positively by subscribers who regularly engage with your emails. Identifying and removing disengaged subscribers can help keep your emails away from the junk folder.

What a good Re-Engagement Journey should have

A clear objective & measures of success

Although your Re-Engagement Journey is different to your standard marketing sends – it’s performance should not be measured or treated differently. Define what you consider engagement (open, click, add to cart, purchase, etc) and report on it’s performance to ensure ongoing success. Don’t be afraid to identify “removing disengaged subscribers” as an objective, as we covered above, this will benefit your IP Reputation.

Point of difference from your standard sends

Subscribers should know your Re-Engagement emails are different from the moment they see them in their inbox. The subject line should be short and clear, the email content should be clean and to the point.
In some cases this can be enough to get your email placed in the inbox rather than the promotion/spam folder for some subscribers!

Clear engagement message and actions

Re-Engagement emails should have a clear objective – don’t muddy the message with sales or product information – keep the focus on their continued engagement with your brand. Remove detracting messages banners and use simple Call to Action devices like buttons to identify what you want them to do with the email.

Give lapsing subscribers a choice

Black and white messaging will work on some lapsing subscribers; however sometimes “now is just not the right time”, and an in-between option could have retained a valuable future customer. Enter the Snooze option.
Giving subscribers the option to “snooze” emails for a duration of time is the easiest way to respect their changing communication preferences. Customers who use this option will be in a very particular mind-set; they want to deal with you again in the future, just not right now.
Use this information to your advantage. Design an “awaken” campaign at the completion of their snooze duration; welcome them back to the brand or invite them to snooze a little longer – we’ve all been there…

Keep testing and improving the Journey

Ensure the messaging in your re-engagement emails is performing by splitting all eligible subscribers into 3 groups and conducting an “ABC” test:

  • 70% receives the primary “A” version of the email.
  • 20% receives the testing “B” version of the email.
  • 10% is held as a control group “C” to measure email performance.

Building an ABC testing methodology into your journeys gives you the flexibility to create an ongoing testing process to validate hypothesis and business requests. The 70|20|10 split is relatively safe as the majority of your disengaging subscribers will receive the primary version, however there is enough statistical significance in the remaining groups to prove any test case you try.

Log and Record everything

Depending on your Re-Engagement Journey entry conditions, you may have subscribers who enter the activity multiple times per year! Keep a record of every subscriber who goes through your re-engagement activities and use it to report on long-term subscriber loyalty. The insights gained from viewing long-term subscriber engagement may help to identify problems in your on-boarding or always-on marketing strategy.

Read more

Loops are one of my favorite processes in programming – they allow you to step through a variable amount of data/inputs to produce a customized output! Recently while discussing an API solution with a colleague I realized just how difficult it can be to explain how to use a loop function to solve a problem, and the differences between the various loop statements.

In an attempt to help other users to navigate this topic, lets cover the key recursive functions that are supported in Salesforce SSJS.

Introduction to the FOR Loop

In my opinion the FOR Loop is best SSJS loop statement to start with – it’s the most common, practical and understandable – and shares a very similar syntax with the AMPScript FOR statement, among other programming languages. Consider the following statement and the output below:

<script runat="server">
Platform.Load("core","1");

for (i = 0; i < 10; i++) {
  Write('The Count is: '+i+'<br>');
}

</script>

//Output:
The Count is: 0
The Count is: 1
The Count is: 2
The Count is: 3
The Count is: 4
The Count is: 5
The Count is: 6
The Count is: 7
The Count is: 8
The Count is: 9

The FOR statement contains 3 parts: The Initialization, Condition, and Ending Expression.

The Initialization runs a the start of the code and sets the scene. In the example above, it’s declaring that the variable “i” shall be equal to be 0.

After Initialization, the statement evaluates the expression in the Condition section. In the example above the Condition expression is “i < 10”. Is the value of “i” less than 10?
As the function just initialized and we just declared that i = 0, the expression evaluates as TRUE, and the area of code between the curly brackets “{}” is run.

After the code between the curly brackets has run, the third and last part of the statement is run, the Ending Expression. In the example above, the expression is “i++”, which is short hand for “i = i + 1” or “Add 1 to the value of i”. You can read more about JavaScript Operators here.
The value of “i” was previously 0, however this expression has incremented it by 1. Now that the Ending Expression is complete, the statement returns (loops back) to the Condition expression to be evaluated again.
“i < 10” is re-evaluated as TRUE – since “i” is now equal to 1, and “1 is less then 10” – and the code between the brackets is run again. Once complete, the statement processes the Ending Expression and increments the value of “i” by 1.

The process continues to “loop” over itself until the Condition expression evaluates as FALSE, at which time the statement concludes. In the example above, the last output was “The Count is 9” because as soon as the value of “i” reached 10, the Condition of “i < 10” was no longer true.

Practical uses for the FOR Loop

The beautiful simplicity of the FOR Loop is that it will simply run as many times as the statement requires it to. The example above used “(i = 0; i < 10; i++)” which resulted in the statement running 10 times. Having control over these values means we can make small adjustment to achieve other goals – such as counting down to 0, or counting in numbers other than 1.
Now that we understand the basics, lets use this functionality to solve a business problem.

Lets say we needed to send an email at the start of each day with a scoreboard that displays a list of employees who scored at least 100 points the previous day.
For simplicity lets also assume that the results from the previous day are saved directly into a Data Extension along with the employee names, so there is no need for any other lookups or date manipulation.
The data we have access to is as follows:

IDNamePoints
14Ameen245
6Coby45
42Nolan25
38Trey77
62Lee112
79Duke145
24Conrad37

Based on the above information, we can use the following SSJS code to print a table of the employees that met the condition:

<table border=1>
<tr><th>ID</th><th>Name</th><th>Points</th></tr>
  
<script runat="server">
Platform.Load("core","1");

var EMP_Points = DataExtension.Init("EmployeePointsCampaign");
var filter = {Property:"EMP_Points",SimpleOperator:"greaterThanOrEqual",Value:100};
var data = EMP_Points.Rows.Retrieve(filter);

for (i = 0; i < data.length; i++) {
  Write('<tr><td>'+data[i].EMP_ID+'</td><td>'+data[i].EMP_Name+'</td><td>'+data[i].EMP_Points+'</td></tr>');
}
</script>

</table>
Result:
IDNamePoints
14Ameen245
62Lee112
79Duke145

There’s a lot going on in the code above – so lets break it down!
Notice that the TABLE tags are outside of the script – this is because the LOOP statement is looping over the rows (tr) inside the table for each eligible employee.
You can read more about DataExtension lookups in SSJS here, however the key things to know is that the variable of “data” now holds a JSON object which contains the payload of data returned by the retrieve function that we need to output. You can brush up on your JSON understanding with this JSON intro at W3Schools. The payload returned by the retrieve function is below:

[{"EMP_ID":14, "EMP_Name":"Ameen", "EMP_Points":245},
{"EMP_ID":62, "EMP_Name":"Lee", "EMP_Points":112},
{"EMP_ID":79, "EMP_Name":"Duke", "EMP_Points":145}]

The object contains an array of objects equal to the number of eligible employees (3); where each object has within it an array of Name/Value pairs which hold the data (EMP_ID, EMP_Name, EMP_Points).
To access the name “Ameen”, we need to reference its address in the JSON structure. In this example, its “data[0]. EMP_Name”. The first address reference is data[0] because it’s in the first object in the JSON payload and arrays start at Ordinal “0”. Next we reference the Name that the value is stored in, which is “EMP_Name”.
To reference “Duke’s” point value of 145, we would write: “data[2]. EMP_Points” .

Now that we’ve covered how to address the data, lets explore how the Loop works in this example.
Notice how the Condition statement in this Loop is “i < data.length”. The “.length” property detects how many elements are in the JSON object – which in this example is 3 – therefore the Condition statement actually resolves as “i < 3”.
As we learnt in the earlier example, the for loop will continue until the “i” is no longer less than 3; meaning “i” will step through the values starting at 0 and ending at 2: [0,1,2].

Within the Loop statement, the output line is using the Write() function to print the table elements along with the 3 values that we need; EMP_ID, EMP_Name, EMP_Points. Each of these values is prefixed with “data[i]”, meaning that as the Loop statement steps through the values [0,1,2] it will correctly address each of the 3 objects in the data payload!

The FOR IN Loop

FOR IN is another form of JavaScript Loop that uses a slightly different condition format to determine how many times to run the statement. An example of the FOR IN Loop is as follows:

<script runat="server">
Platform.Load("core","1");
var EMP_data = {"EMP_ID":14, "EMP_Name":"Ameen", "EMP_Points":245};
for (i in EMP_data) {
  Write('The Value of ' +i+ ' is: ' +EMP_data[i]+ '<br>');
}
</script>

//Output:
The Value of EMP_ID is: 14
The Value of EMP_Name is: Ameen
The Value of EMP_Points is: 245

Remember above how we covered the Name/Value pairs in JSON objects? The FOR IN Loop allows you to select every “name” within an array and store it as a value; in the example above it’s stored as “i”.
With the Name attribute known, we can correctly address each Name/Value pair in the payload.

This Loop is very useful when you don’t know the name of every Name/Value pair and just need to cycle through everything that is returned. An example of this is where the Name attributes could be optional or change based on inputs. Rather than hard-coding them like we did in the first example, they can be dynamically referenced with FOR IN.

The DO WHILE Loop

The DO WHILE Loop is a slightly more complex version of the standard WHILE Loop. Since the WHILE Loop functions in much the same way as the FOR Loop , I’ll only cover the DO WHILE version.

This is the most complex Loop to learn, however the utility benefit of this loop can not be understated, it is a truly powerful function for any automation marketing team.
The DO WHILE statement comes in 2 parts – DO{} and WHILE().

<script runat="server">
Platform.Load("core","1");
var i = 0;
do {
  Write("Count to " + i + "<br>");
  i++;
}
while (i < 10);
</script>

//Output:
Count to 0
Count to 1
Count to 2
Count to 3
Count to 4
Count to 5
Count to 6
Count to 7
Count to 8
Count to 9

The above example is a replica of the FOR Loop we used at the top of this article, and it follows a very similar process of running a statement, the DO{} section, “while” a condition is TRUE, the WHILE() section.

This means we have direct control over when the statement ends based on values that can be evaluated as part of the statement. This can be used to interesting effect, such as running a random number generator that will output numbers until it hits the correct number:

<script runat="server">
Platform.Load("core","1");
var random = Math.floor(Math.random() * 10);
do {
  Write("random is " + random + "<br>");
  var random = Math.floor(Math.random() * 10);
}
while (random != 7);
</script>

//Output:
random is 4
random is 9
random is 8
random is 1
random is 3
random is 2
random is 8
random is 2
random is 9
random is 5
random is 9
random is 9
random is 8
random is 1
random is 4
random is 5
random is 0

In the above, the code will continue to run while the value of “random” is not equal to 7. In my test run it took 17 iterations before the number 7 was selected by the random number generator.

Now lets have some fun and make a function that needs to count up to a target number by adding randomly generated numbers together. It will keep generating random numbers (between 1-10) until it reaches its goal; disregarding numbers that will put it over the specified target of 100.

<script runat="server">
Platform.Load("core","1");
var target = 100;
var keepgoing = true;
var total = 0
var steps = 0
do {
  var random = Math.floor(Math.random() * 10)+1;
  steps++;
  if ((total + random) == target) {
    Write("The final Number was " + random + ", bringing to total to "+target+" on step number "+steps+"<br>");
    var keepgoing = false;
  } else {
   if ((total + random) > target) {
    Write("Number " + random + " disregarded - it would push the total to "+(total+random)+"<br>");
     var keepgoing = true;
   } else {
    Write("Number " + random + " added - total is "+(total+random)+"<br>");
    var total = (total + random);
     var keepgoing = true;
   }
  }
}
while (keepgoing);
</script>

//Output:
Number 2 added - total is 2
Number 4 added - total is 6
Number 8 added - total is 14
Number 8 added - total is 22
Number 6 added - total is 28
Number 6 added - total is 34
Number 3 added - total is 37
Number 6 added - total is 43
Number 8 added - total is 51
Number 1 added - total is 52
Number 2 added - total is 54
Number 2 added - total is 56
Number 8 added - total is 64
Number 7 added - total is 71
Number 7 added - total is 78
Number 7 added - total is 85
Number 5 added - total is 90
Number 6 added - total is 96
Number 9 disregarded - it would push the total to 105
Number 3 added - total is 99
Number 5 disregarded - it would push the total to 104
Number 2 disregarded - it would push the total to 101
Number 7 disregarded - it would push the total to 106
Number 9 disregarded - it would push the total to 108
Number 4 disregarded - it would push the total to 103
Number 7 disregarded - it would push the total to 106
Number 2 disregarded - it would push the total to 101
Number 8 disregarded - it would push the total to 107
Number 8 disregarded - it would push the total to 107
Number 2 disregarded - it would push the total to 101
Number 2 disregarded - it would push the total to 101
Number 2 disregarded - it would push the total to 101
Number 2 disregarded - it would push the total to 101
Number 7 disregarded - it would push the total to 106
Number 9 disregarded - it would push the total to 108
Number 7 disregarded - it would push the total to 106
Number 6 disregarded - it would push the total to 105
Number 4 disregarded - it would push the total to 103
Number 6 disregarded - it would push the total to 105
The final Number was 1, bringing to total to 100 on step number 40

Practical Examples for the DO WHILE Loop

The function above was creative, but the real world applications for this kind of loop are endless!
We could write an automated email that selects items from a customer’s cart in price acceding order until it reaches the minimum spend needed for Free Shipping. Imagine getting an email from your favorite fashion retailer with the subject line “Buy these 7 items in your wishlist today and get FREE SHIPPING!”
Retailers with annual spend milestones required to maintain a loyalty status – such as Sephora, Witchery and Mimco – could use the same logic to create a personalized Call to Action for the customer to maintain membership for another year. “Hey %%fName%%, restock on these %%count%% items and maintain %%tier%% status for another year!”

The versatility of SSJS Loops is that they can be run on Cloud Pages, in Emails, and as activities in Automation Studio, so there are plenty of ways to ingest and activate your data! Be sure to ask for help on the Salesforce StackExchange if you need assistance building your SSJS Loops.

Read more

I recently read the Marketing Cloud Security module on Trailhead and found the Secure Your Web and Landing Pages unit to be extremely interesting, however it glossed over the reason and function of the Example Headers that are recommended.

As the defined role gap between IT and Marketing is closing quickly, I thought this content needs to be given the attention it deserves from a non-IT perspective.

Website Headers and why you should care

Response headers are used to give a more detailed context of the response provided by a server. To critically over simplify this: When you hit SEARCH on Google.com your request is sent to Google to process – the response is then sent back and interpreted by your browser where is it rendered accordingly. If you are using Chrome, you can open the DevTools to see this in action: Instructions here.

With the example above, the response from Google contains a number of headers that instruct your browser what to do, for example:

content-encoding: br
content-type: text/html; charset=UTF-8
strict-transport-security: max-age=31536000
  • content-encoding: The response is compressed, and your browser needs to know which encodings were used by the server so it can decode the message.
  • content-type: The type of content contained in the response so that your browser knows how to render it.
  • strict-transport-security: This forces the browser to use the more secure HTTPS for communicating with the server, and sets a duration (in seconds) for this to be upheld.

So Headers are important. Why does Salesforce recommend we use these 6 on our pages?

The 6 headers outlined by Salesforce in their Landing Pages Security Best Practices provide an additional layer of security to prevent malicious actors from accessing your Cloud Page in ways that may hurt your business. Each of these 6 headers provide explicit instructions about how your Cloud Page can be accessed. Lets cover these in detail:

<script runat=server>
   Platform.Response.SetResponseHeader("Strict-Transport-Security","max-age=200");
   Platform.Response.SetResponseHeader("X-XSS-Protection","1; mode=block");
   Platform.Response.SetResponseHeader("X-Frame-Options","Deny");
   Platform.Response.SetResponseHeader("X-Content-Type-Options","nosniff");
   Platform.Response.SetResponseHeader("Referrer-Policy","strict-origin-when-cross-origin");
   Platform.Response.SetResponseHeader("Content-Security-Policy","default-src 'self'");
</script>

Strict-Transport-Security:
As above, this Header ensure all communication from a browser is sent over HTTPS. Without going into detail, HTTPS will protect your user’s data as it is sent to and from your Cloud Page. You really want to be using HTTPS if you are asking your subscribers to enter any form of personal information on your Cloud Page.
Note: SSL certificates are required to be installed by Salesforce Support and at the time of writing have an annual fee of ~$450 USD each. Adding this header without have an SSL installed on the appropriate Marketing Cloud Subdomain will result in the page request being blocked.

X-XSS-Protection:
XSS stands for Cross Site Scripting, and is a form of attach where a malicious actor injects scripts onto your page to make it perform differently.
This is a common header and most browsers support this by default. The “1; mode=block” value indicates that the header is active, and to block the page from rendering if a XSS attack is detected.

X-Frame-Options:
This header prevents your page from being loaded into a frame/iframe on another website.
The “Deny” value prevents any site from referencing you page in an iframe, however you can also use the “SameOrigin” or “Allow-From” values to specify trusted exceptions. You should always use this header unless you have an explicit plan for the page to be used in an iframe.

X-Content-Type-Options:
Content-Type sniffing is where an attacker can exploit a security vulnerability to interpret metadata from your site and possibly execute functions that the site was not built to expect. Forcing such errors can expose sensitive code or customer data.
Using the “nosniff” parameter tells the browser that the content-type is defined and not to be queried further.

Referrer-Policy:
The Referrer Policy controls how much referrer (origin site) information is sent in the request (destination site).
The “strict-origin-when-cross-origin” parameter will enforce HTTPS policy and only send the Full Referrer URL when the request comes from the same Origin. Cross-Origin requests will only see the base URL.

Content-Security-Policy:
This header acts as an additional layer of protection against Cross Site Scripting and other attacks.
The value recommended by Salesforce for this header is “default-src ‘self'”. This will force the browser to only load content that is served from the same place as the page with this header. This may cause problems with externally references images and CDN hosted JS libraries such as jQuery, so check the assets used on your page before enabling this one.

More ways to keep you Pages & Data safe

Cloud Pages are extremely versatile and powerful feature of Salesforce Marketing Cloud – however if used incorrectly can they can open up numerous serious security risks to your business and customers. Check with your company’s WebAdmin before enabling these headers and ask the Marketing Cloud community on Stack Exchange for help if you run into problems.

Read more

Regular Expression (RegEx) is an instruction set for matching and returning a part of a larger string, and is recognised by most programming languages.

AMPScript is unable to nativity parse JSON objects, meaning that string manipulation is the only way to extract the value from the larger object. As discussed in my OAuth 2.0 Access Token Quick Start guide, the SubString function is a quick way to return text if it’s location in the payload is reliable.

Since the order of objects in JSON payloads are not designed to be preserved, RegEx is a more reliable way of searching and returning values from a string. The following RegExMatch function is that I use to return the access_code from the V2 Token API:

SET @access_token = REGEXMATCH(@apistatusCode,'^(?:.*"access_token":")(.*?)(?:".*)$',1)

If you need more detail on how the RegExMatch() function works, I suggest checking out the examples on The AMPScript Guide.

RegEx can take some time to get your head around as it’s a different syntax to most languages. To understand what the Regular Expression string above is achieving, I suggest using an online visual RegEx guide such as regexr.com or regex101.com to build and test your expressions. Copying the example JSON V2 Token response into either of these services and inspecting the rule set will give a far more detailed explanation to how this expression works; however in short, the expression above is returning everything that comes after "access_token":" and before the very next " character.

Due to the simplicity of this Regular Expression, you can amend it relitivly quickly for any value in any JSON endpoint. For example, the icanhazdadjoke.com API will return a random Dad Joke every time it’s requested:

{
  "id": "R7UfaahVfFd",
  "joke": "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.",
  "status": 200
}

We can alter the RegEx code from earlier to return the value of the “joke” object in the JSON response above:

^(?:.*"joke":.*")(.*?)(?:".*)$

Note that the Dad Joke API returns a space after the : character, so I used the .* wildcard to indicate that anything could optionally appear there.

Regular Expressions are a very powerful and can be used to great effect if you understand how they work. This is a language syntax worth spending some time learning!

Read more