AMPScript

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

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

Working in Marketing Automation, I spend a lot of my time in Salesforce Marketing Cloud creating API integrations to push and pull data from various sources.

Writing these calls from scratch or copying bespoke code from previous solutions can be messy, so to save time I created a boilerplate of the basic OAuth 2.0 GetToken request process in AMPScript and SSJS that was easily editable for my purposes.

TL;DR. Here’s the codes!

AMPScript OAuth 2.0 using the V2 Token Endpoint:

%%[
VAR @httppost,@authurl,@apiid,@apistatusCode,@apiresponse,@apitoken
SET @apiid = '{"client_id": "zzzzzzzzzzzzzzzzzz","client_secret": "xxxxxxxxxxxxxxxxxxxxxxx", "grant_type": "client_credentials"}'
SET @authurl = "https://ccccccccccccccccccccc.auth.marketingcloudapis.com/v2/token";
SET @httppost = HTTPPost2(@authurl,"application/json",@apiid,false,@apistatusCode,@apiresponse)
SET @access_token = REGEXMATCH(@apistatusCode,'^(?:.*"access_token":")(.*?)(?:".*)$',1)
SET @rest_instance_url = REGEXMATCH(@apistatusCode,'^(?:.*"rest_instance_url":")(.*?)(?:".*)$',1)
]%%

access_token: %%=v(@access_token)=%% <br>
rest_instance_url: %%=v(@rest_instance_url)=%% <br>
apistatusCode: %%=v(@apistatusCode)=%% <br>

The Access Token can be retrieved using @access_token. Since the API response is in JSON – a data structure not supported by AMPScript – we need to use other string functions to extract the value we need, but more on that later.

SSJS OAuth 2.0 using the V2 Token Endpoint:

<script runat="server">
    Platform.Load("Core","1");
    var payload = {
    "grant_type": "client_credentials",
    "client_id": "yyyyyyyyyyyyyyyyyyy",
    "client_secret": "xxxxxxxxxxxxxxxxxx"
};
var authurl = "https://zzzzzzzzzzzzzzzzzz.auth.marketingcloudapis.com/v2/token";
var result = HTTP.Post(authurl, 'application/json', Stringify(payload));
  
if (result.StatusCode == 200) {
    var responseJson = Platform.Function.ParseJSON(result.Response[0]);
    var accessToken = responseJson.access_token;
    var restUrl = responseJson.rest_instance_url;
} else {
    throw new Error("Error fetching access token");
}
Write(result.Response[0]); //payload response
Write(accessToken); //token
</script>

Luckily Server Side JavaScript is able to parse JavaScript Object Notation (JSON), so the “access_token” value can be referenced and stored in the variable “accessToken” for later use.

Parsing JSON Values in Salesforce Marketing Cloud

If you work with API endpoints you’ll be familiar with the XML and JSON content types. JSON took the most-popular content-type throne from XML in around 2013 and has been the dominant format ever since. As of February 2020 there is still no AMPScript-only solution for parsing JSON values in Salesforce Marketing Cloud, so we need to employ other tools to extract the values we need.

Below is an Example JSON Response of the V2 Token Endpoint from earlier:

{
"access_token":"eyJhbLciOiJIPzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIifQ.eyJhY2Nlc3NfdG9rZW4iOiJhYmJUQTlpSHZqRjkyd3Jkb0xWZEFCaloiLCJjbGllbnRfaWQiOiI3ZTRmYW1xaWUzcWtzdzlhNDRrcmxvZDgiLCJlaWQiOjEwNzU3Njc2LCJzdGFja19rZXkiOiJRQTFTMSJ9.wSFfEdeNrkoiU_tnmJ2ihm8iUqnJKlZoI3GlavTGBhs.hU4EsiC1e9txh_TCt90YlI2l7xZZ5E6_oa0xku3Jj9CCk1B72M4bhO3kUIyhwfVuB0MFbL0y9KD_RRFzg-nuqPgjPyONnby-iWopdZPBHd-3woupxCMST5-vfJO9qAED9qiUfYLS4WmHRuJTCX4NPScyu8BdROTVEe-D3iAoAeFoJX_rLZ9d5eEhIn1AvkYgoj9siuxAprHEvmySTgNIXkQA6uT_IQ-H1dbfOyJmlFKpYzvhvHb0KH7NJ24zy5bd2MQ5",
"expires_in":1200,
"token_type":"Bearer",
"rest_instance_url":"mc563885gzs27c5t9-63k636tzgm.rest.marketingcloudapis.com",
"soap_instance_url":"mc563885gzs27c5t9-63k636tzgm.soap.marketingcloudapis.com",
"scope": "email_read email_write email_send"
}

In the example above, we could use a SubString function to extract the specific character index range that we need for the token. In this example, it’s character 17 through 529; or from character 17 for a length of 512 characters, written as: %%=Substring(@response,17,512)=%% in AMPScript.

SubString works well so long as the response payload doesn’t change in any way, however by default JSON is an unordered set of name/value pairs, so even though Salesforce does preserve object orders, we shouldn’t rely on it.

RegEx is the most reliable way to extract a string value from a JSON payload in AMPScript. I cover this topic in more detail here: RegEx to get JSON values in AMPScript

Read more