Working with Salesforce Multi-Select Picklists in AMPscript

Custom Multi-Select Picklists in Sales/Service Cloud allow users to select one or more values from a predefined list:

When a user picks more than one value, the selected values show in a field separated by a semicolons:

The values will be in the same, semicolon-delimited format, if you synchronize the picklist field into Marketing Cloud and store the data in a Data Extension. This means that you will have to break each of those fields up into separate values to be able to use them in your script.

My example multi-select picklist in Sales Cloud is used for storing subscriber preferences and has five possible values:

  • Events
  • Newsletters
  • Promotions
  • Special Offers
  • Surveys

In this tutorial, we will first pull the multi-select picklist data onto a CloudPage to display it for the subscriber as an HTML form, where they can pick which of the above they are interested in, and once they submit the form, we will write that data back into Sales Cloud.

Splitting a string by a delimiter

First, let’s pull the data into our script. In the example below, I will use the RetrieveSalesforceObjects() function to pull the data onto a CloudPage directly from Sales/Service Cloud. Depending on your use case, you might want to use the Lookup() or LookupRows() functions instead, if you are working with Synchronized Data Extensions.

My multi-select picklist field is called Preferences__c and it’s on the Contact object:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberrows = retrievesalesforceobjects(
"contact",
"firstname,lastname,email,preferences__c",
"id", "=", @subscriberKey)
if rowcount(@subscriberrows) == 1 then
set @subscriberrow = row(@subscriberrows, 1)
set @firstname = field(@subscriberrow, "firstname")
set @lastname = field(@subscriberrow, "lastname")
set @email = field(@subscriberrow, "email")
set @preferences__c = field(@subscriberrow, "preferences__c")
endif
]%%
%%=v(@preferences__c)=%%

When you output the data using %%=v(@preferences__c)=%%, you will see it in exactly the same semicolon-delimited format as mentioned earlier:

In order to split multi-select picklist data into separate values and to be able to assign those values to AMPscript variables, we will use the BuildRowSetFromString() function. The BuildRowSetFromString function will create a rowset from a character string by splitting the string at the specified delimiter – in our case, a semicolon.

As a first step, we will check if our @preferences__c variable has any data inside by using the Empty() function. If the variable is not empty, we can proceed with the BuildRowSetFromString function:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberrows = retrievesalesforceobjects(
"contact",
"firstname,lastname,email,preferences__c",
"id", "=", @subscriberKey)
if rowcount(@subscriberrows) == 1 then
set @subscriberrow = row(@subscriberrows, 1)
set @firstname = field(@subscriberrow, "firstname")
set @lastname = field(@subscriberrow, "lastname")
set @email = field(@subscriberrow, "email")
set @preferences__c = field(@subscriberrow, "preferences__c")
endif
if not empty(@Preferences__c) then
set @rs = buildrowsetfromstring(@preferences__c,';')
Output(rowcount(@rs))
endif
]%%

Output(rowcount(@rs)) will output the number of values contained in our multi-select picklist field.

Now let’s add a loop to iterate through the rowset and match the values in our script with multi-select picklist values from Saleforce. Remember, that the more picklist values you have in Salesforce, the more complicated your loop will get.

The biggest challenge we need to tackle in our script, is the fact that we never know how many and which values each subscriber will have assigned to them. That’s why we need to iterate through all of the values and use a conditional statement to check if each of the values found matches a picklist value we know exists in Salesforce.

Here’s how the script will look like once we add our loop with the conditional statements:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberRows = RetrieveSalesforceObjects(
"Contact",
"FirstName,LastName,Email,Preferences__c",
"Id", "=", @subscriberKey )
if RowCount(@subscriberRows) == 1 then /* there should only be one row */
var @subscriberRow, @firstName, @lastName, @email
set @subscriberRow = Row(@subscriberRows, 1)
set @firstName = Field(@subscriberRow, "FirstName")
set @lastName = Field(@subscriberRow, "LastName")
set @email = Field(@subscriberRow, "Email")
set @Preferences__c = Field(@subscriberRow, "Preferences__c")
IF NOT EMPTY(@Preferences__c) THEN
SET @rs = BuildRowsetFromString(@Preferences__c,';')
IF rowcount(@rs) > 0 THEN
FOR @i=1 TO rowcount(@rs) DO
SET @val = Field(Row(@rs,@i),1)
IF @val == "Surveys" THEN
SET @Surveys = true
ELSEIF @val == "Events" THEN
SET @Events = true
ELSEIF @val == "Newsletters" THEN
SET @Newsletters = true
ELSEIF @val == "Promotions" THEN
SET @Promotions = true
ELSEIF @val == "Special Offers" THEN
SET @Offers = true
ENDIF
NEXT @i
ELSE
SET @Newsletters = ""
SET @Events = ""
SET @Offers = ""
SET @Survays = ""
SET @Promotions = ""
ENDIF
ENDIF
endif
]%%
@Surveys: %%=v(@Surveys)=%%<br>
@Events: %%=v(@Events)=%%<br>
@Newsletters: %%=v(@Newsletters)=%%<br>
@Promotionss: %%=v(@Promotions)=%%<br>
@Offers: %%=v(@Offers)=%%<br>

Above script will display true for all values where it found a match.

Let’s now add an HTML form and display our picklist data as checkboxes.

HTML checkboxes and AMPscript

We will use AMPscript and the values retrieved using the BuildRowSetFromString function to conditionally mark checkboxes as checked if there is a matching value in Salesforce. In HTML forms, the checked attribute specifies that an element should be pre-selected when the page loads (for type=”checkbox” or type=”radio”):

<input type="checkbox" checked>

For each of the checkboxes representing the multi-select picklist values, we will add an AMPscript variable to conditionally add the checked attribute when the value is found in our rowset. Let’s also add the basic subscriber information like First Name, Last Name and Email to the form:

<h2>Please fill in the form:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<label>First name: </label><input type="text" name="firstname" required="" value="%%=v(@firstName)=%%"><br>
<label>Last name: </label><input type="text" name="lastname" required="" value="%%=v(@lastName)=%%"><br>
<label>Email: </label><input type="text" name="email" required="" value="%%=v(@email)=%%"><br><br>
<h2>Update your subscriptions:</h2>
<input name="newsletters" type="checkbox" %%=v(@Newsletters)=%%><label>Newsletters</label><br>
<input name="events" type="checkbox" %%=v(@Events)=%%><label>Events</label><br>
<input name="offers" type="checkbox" %%=v(@Offers)=%%><label>Special Offers</label><br>
<input name="surveys" type="checkbox" %%=v(@Surveys)=%%><label>Surveys</label><br>
<input name="promotions" type="checkbox" %%=v(@Promotions)=%%><label>Promotions</label><br>
<input name="submitted" type="hidden" value="true"><br>
<input type="submit" value="Submit">
</form>

Now we need to adjust or script to display the word checked instead of true in case there is a match:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberRows = RetrieveSalesforceObjects(
"Contact",
"FirstName,LastName,Email,Preferences__c",
"Id", "=", @subscriberKey )
if RowCount(@subscriberRows) == 1 then /* there should only be one row */
var @subscriberRow, @firstName, @lastName, @email
set @subscriberRow = Row(@subscriberRows, 1)
set @firstName = Field(@subscriberRow, "FirstName")
set @lastName = Field(@subscriberRow, "LastName")
set @email = Field(@subscriberRow, "Email")
set @Preferences__c = Field(@subscriberRow, "Preferences__c")
IF NOT EMPTY(@Preferences__c) THEN
SET @rs = BuildRowsetFromString(@Preferences__c,';')
IF rowcount(@rs) > 0 THEN
FOR @i=1 TO rowcount(@rs) DO
SET @val = Field(Row(@rs,@i),1)
IF @val == "Surveys" THEN
SET @Surveys = "checked"
ELSEIF @val == "Events" THEN
SET @Events = "checked"
ELSEIF @val == "Newsletters" THEN
SET @Newsletters = "checked"
ELSEIF @val == "Promotions" THEN
SET @Promotions = "checked"
ELSEIF @val == "Special Offers" THEN
SET @Offers = "checked"
ENDIF
NEXT @i
ELSE
SET @Newsletters = ""
SET @Events = ""
SET @Offers = ""
SET @Survays = ""
SET @Promotions = ""
ENDIF
ENDIF
endif
]%%
<h2>Please fill in the form:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<label>First name: </label><input type="text" name="firstname" required="" value="%%=v(@firstName)=%%"><br>
<label>Last name: </label><input type="text" name="lastname" required="" value="%%=v(@lastName)=%%"><br>
<label>Email: </label><input type="text" name="email" required="" value="%%=v(@email)=%%"><br><br>
<h2>Update your subscriptions:</h2>
<input name="newsletters" type="checkbox" %%=v(@Newsletters)=%%><label>Newsletters</label><br>
<input name="events" type="checkbox" %%=v(@Events)=%%><label>Events</label><br>
<input name="offers" type="checkbox" %%=v(@Offers)=%%><label>Special Offers</label><br>
<input name="surveys" type="checkbox" %%=v(@Surveys)=%%><label>Surveys</label><br>
<input name="promotions" type="checkbox" %%=v(@Promotions)=%%><label>Promotions</label><br>
<input name="submitted" type="hidden" value="true"><br>
<input type="submit" value="Submit">
</form>

Now you should be able to correctly display the multi-select picklist data for each subscriber visiting your CloudPage.

Update Multi-Select Picklist values in Salesforce using AMPscript

Once the subscriber makes changes to their preferences and clicks on the Submit button, the page will reload and the form parameters will be posted back to the same page. If you would like to learn more about this approach of working with forms on CloudPages, take a look at one of my earlier articles: Create a Sales Cloud-integrated lead capture form using AMPscript.

Now we can add the script to process the posted form data and update it in Salesforce:

SET @preferences = CONCAT(
Iif(RequestParameter("newsletters") == "on", "Newsletters;", ""),
Iif(RequestParameter("events") == "on", "Events;", ""),
Iif(RequestParameter("offers") == "on", "Special Offers;", ""),
Iif(RequestParameter("surveys") == "on", "Surveys;", ""),
Iif(RequestParameter("promotions") == "on", "Promotions", ""),
)
if not Empty(@preferences) then
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"Preferences__c", @preferences)
else
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"fieldsToNull", "Preferences__c")
endif

To pass the data back to Salesforce, we need to transform the posted form data back into the initial form: a string with semicolon-delimited values. In order to achieve this, I have used the Iif() function to conditionally check which values have been selected in the form (a selected checkbox will return the value of on). If a parameter returns the on value, we substitute it with the corresponding picklist value and concatenate all the returned values together into one string to pass them to Salesforce.

The full script

Here is the full script, including a try/catch statement for debugging:

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
%%[
IF RequestParameter("submitted") == true THEN
SET @preferences = CONCAT(
Iif(RequestParameter("newsletters") == "on", "Newsletters;", ""),
Iif(RequestParameter("events") == "on", "Events;", ""),
Iif(RequestParameter("offers") == "on", "Special Offers;", ""),
Iif(RequestParameter("surveys") == "on", "Surveys;", ""),
Iif(RequestParameter("promotions") == "on", "Promotions", ""),
)
if not Empty(@preferences) then
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"Preferences__c", @preferences)
else
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"fieldsToNull", "Preferences__c")
endif
]%%
Your preferences have been updated.
%%[ELSE
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberRows = RetrieveSalesforceObjects(
"Contact",
"FirstName,LastName,Email,Preferences__c",
"Id", "=", @subscriberKey)
if RowCount(@subscriberRows) == 1 then
var @subscriberRow, @firstName, @lastName, @email
set @subscriberRow = Row(@subscriberRows, 1)
set @firstName = Field(@subscriberRow, "FirstName")
set @lastName = Field(@subscriberRow, "LastName")
set @email = Field(@subscriberRow, "Email")
set @Preferences__c = Field(@subscriberRow, "Preferences__c")
IF NOT EMPTY(@Preferences__c) THEN
SET @rs = BuildRowsetFromString(@Preferences__c,';')
IF rowcount(@rs) > 0 THEN
FOR @i=1 TO rowcount(@rs) DO
SET @val = Field(Row(@rs,@i),1)
IF @val == "Surveys" THEN
SET @Surveys = "checked"
ELSEIF @val == "Events" THEN
SET @Events = "checked"
ELSEIF @val == "Newsletters" THEN
SET @Newsletters = "checked"
ELSEIF @val == "Promotions" THEN
SET @Promotions = "checked"
ELSEIF @val == "Special Offers" THEN
SET @Offers = "checked"
ENDIF
NEXT @i
ELSE
SET @Newsletters = ""
SET @Events = ""
SET @Offers = ""
SET @Survays = ""
SET @Promotions = ""
ENDIF
ENDIF
endif
]%%
<h2>Please fill in the form:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<label>First name: </label><input type="text" name="firstname" required="" value="%%=v(@firstName)=%%"><br>
<label>Last name: </label><input type="text" name="lastname" required="" value="%%=v(@lastName)=%%"><br>
<label>Email: </label><input type="text" name="email" required="" value="%%=v(@email)=%%"><br><br>
Pref: %%=v(@Preferences__c)=%%<br><br>
<h2>Update your subscriptions:</h2>
<input name="newsletters" type="checkbox" %%=v(@Newsletters)=%%><label>Newsletters</label><br>
<input name="events" type="checkbox" %%=v(@Events)=%%><label>Events</label><br>
<input name="offers" type="checkbox" %%=v(@Offers)=%%><label>Special Offers</label><br>
<input name="surveys" type="checkbox" %%=v(@Surveys)=%%><label>Surveys</label><br>
<input name="promotions" type="checkbox" %%=v(@Promotions)=%%><label>Promotions</label><br>
<input name="subkey" type="hidden" value="%%=v(@subscriberKey)=%%"><br>
<input name="submitted" type="hidden" value="true"><br>
<input type="submit" value="Submit">
</form>
%%[ ENDIF ]%%
<script runat="server">
}catch(e){
Write(Stringify(e));
}
</script>

To see this script in action, visit my CloudPage here.


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Exception handling in Salesforce Marketing Cloud CloudPages

Errors and exceptions are inevitable, no matter how defensively you code your CloudPage. There is always a possibility of a human error, for example if the person who creates and sends the email doesn’t pass all the parameters to your CloudPage correctly. There are also things that sometimes cannot be prevented, for example Marketing Cloud Connector getting disconnected while your script utilizes the Sales and Service Cloud AMPscript functions.

Although this solution has been mentioned multiple times in various articles on this and other blogs, I decided it needed it’s own article, as we can often see that Salesforce Marketing Cloud developers struggle to troubleshoot problems and errors on their CloudPages.

In one of my previous article, Exception handling in Salesforce Marketing Cloud emails, I have presented a way to log any errors evaluated by the RaiseError() function into a Data Extension. Today, I would like to show you a very quick and easy way to achieve this on a CloudPage.

Try/catch statement

The best way, not only to debug your CloudPages, but also to handle all exceptions, is to include a JavaScript Try/Catch statement in all your CloudPages, regardless of whether you are coding them using AMPscript or Server-Side JavaScript.

The try statement allows you to define a block of code to be tested for errors while it is being executed. The catch statement allows you to define a block of code to be executed, if an error occurs in the try block. Wrapping the whole code included in your CloudPage in a try/catch statement, will not only help you catch errors, regardless of in which part of the code they appear, but it will also prevent your subscribers from seeing the dreaded 500 error if something goes wrong. Instead, you will be able to still display your CloudPage properly with all it’s branding, and include either a generic or a personalized error message: [see code snippet]

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
{{your script goes here}}
<script runat="server">
}catch(e){
Write("Oops, something went wrong!<br>Please contact info@email.com so we can help you finalize your request.");
}
</script>

If you would like to see an example of a more complex script with the try/catch statement included, you can take a look at my Salesforce-Integrated Subscription and Profile Center script.

Log all caught errors

Now that we have a mechanism for catching errors on a CloudPage, it would be also good to have somewhere to store them, so that they can be reviewed on a regular basis. This can be easily achieved by creating a dedicated Data Extension and inserting a new row to that Data Extension every time an error is caught.

Here is an example of a Shared Data Extension created for error logging. It holds information about the Subscriber, MID, date of the event and the error message itself:

You can add other fields if needed, but remember to make them nullable – sometimes, depending on the error, some variables or personalization strings might not be available, and in that case you will have to do with just the date and the error message.

Now let’s add a script that will allow us to log data into the above Data Extension: [see code snippet]

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
{{your script goes here}}
<script runat="server">
}catch(e){
var errorMsg = Stringify(e.message)
Write("Oops, something went wrong!<br>Please contact info@email.com so we can help you finalize your request.");
Variable.SetValue("@err",errorMsg);
}
</script>
%%[
if not empty(@err) then
InsertData('ent.ErrorLogging','SubscriberKey',_SubscriberKey,'MID',memberid,'error',@err)
endif
]%%

The above is a mix of Server-Side JavaScript and AMPscript, which can be used to log data from the Personalization Strings, but depending on your use case, you could also log errors using just SSJS: [see code snippet]

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
{{your script goes here}}
<script runat="server">
}catch(e){
var errorMsg = Stringify(e.message)
Write("Oops, something went wrong!<br>Please contact info@email.com so we can help you finalize your request.");
var targetDE = 'xxxxxxx-xxxxxxx-xxxxxxxxx'; //pass external key of the target DE
var logDE = DataExtension.Init(targetDE);
logDE.Rows.Add({ Error: errorMsg });
}
</script>

Remember to always include exception handling in your emails and CloudPages, as this will help you maintain excellent reputation with your subscribers, while constantly improving the knowledge about your data and the quality of your code.


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Two alternative ways of developing and testing CloudPages

As a Salesforce Marketing Cloud developer, you have certainly been caught up in this loop: Develop the CloudPage > Save and Publish > Wait 2, 3 or even 5 minutes > Check the results. Moreover, if you are not debugging correctly, you can lose a lot of time between looking for the problem and waiting for your updates to propagate on your CloudPage. I must take this opportunity to invite you to check out Zuzanna’s article about debugging AMPScript in CloudPages, it is very helpful and well explained.

In this article, we will be covering two ways of testing your code on a CloudPage without having to wait for the propagation of changes every time we save and publish our work. These two ways are probably well know by experimented Salesforce Marketing Cloud developers, or by those who were fortunate enough to stumble upon some answers regarding this problem on Salesforce Stack Exchange.

Create an external web page

If you already have a web server, that is great. You can use it to create your web pages. However, if you are on a budget, or you just do not want to spend money on a hosting service, don’t worry, I have done the heavy lifting for you.

We are going to use awardspace.com, an ad-free hosting which offers:

  • Free subdomain
  • 1GB disk space
  • Up to 4 websites (websites and not pages)

Those features are more than enough for our tests. Especially because we can use one or two pages only and overwrite their content every time we need to develop a new CloudPage.

Step 1: Create a freehosting account

Go to awardspace.com and click on FreeHosting on the homepage:

Create an account using one of the available options:

Step 2: Creating a free subdomain

Go to Hosting Tools > Domain Manager. Select “Create a free subdomain”, fill in the subdomain’s name, and click Create:

Step 3: Creating the web page

Go to Hosting Tools > File Manager. Double click on your subdomain’s folder:

Click on Create and check Create file. Give it a name like sfmc.html for example.

Step 4: add content and access the web page

Congrats, we have successfully created the web page. Double click on it to add the content. Click save when done editing The web page’s URL is as shown below without the /home/www/ part.

Call the page from within Salesforce Marketing Cloud

We are going to use two AMPScript functions to access our external page from within our CloudPage.

  • HTTPGet: Returns the content from our external web page. This function will only take our web page URL as a parameter.
  • TreatAsContent: Treats the content returned by the HTTPGet function as it came from a content area. This way, we make sure the AMPScript code in our web page gets evaluated.

For example, our CloudPage on Marketing Cloud should contain something like: [see code snippet]

Each time we need to update the code, we just have to update it on the external web page. No need to save and publish our CloudPage as its content will remain the same.  

This way, we are avoiding the wait time between the publishing and the propagation of our changes on the servers when using a traditional development method.

Content Block By Key / Name / Id

The second method is using one of the three functions above. These functions return the content stored in the specified Content Block.

First, we will start by creating our Content Block. We need to go to Content Builder > Create > Content Blocks > Code Snippet.

I highly suggest using a Code Snippet instead of an HTML Content Block in order to avoid Content Builder’s editor from truncating parts of our HTML code.

Once we have created our Content Block and added our content in it. Time for getting it’s Key, Id or Name depending on which function we are using. We can find it by clicking on the drop-down icon and select Properties.

Now, we can head in to our CloudPage and add the code below: [see code snippet]

%%=ContentBlockbyKey("4bd9cced-f9c5-49fe-927a-xxxxxxx")=%%

It pretty simple and straightforward. The code uses ContentBlockByKey to get our Content Block’s content. This is a onetime operation. Every time we need to update our code, we will only have to update it on the Content Block. This way, the updates are instantly live and we do not have to wait several minutes for the propagation on the servers.

Once we are done testing, we can copy the code on the external page or on the Content Block to our main CloudPage in Marketing Cloud and publish it.


About the author

Rachid Mamai is a SFMC geek and a Digital Marketing enthusiast living in France. To get in touch with Rachid, visit his LinkedIn.

Exception handling in Salesforce Marketing Cloud emails

“Hope for the best and prepare for the worst” should be every marketer’s motto when it comes to creating contextual personalized content. Your company’s reputation could be easily tarnished if you send out buggy emails, that’s why exception handling should be an essential element of your email strategy.

In this article, you will learn about some of Salesforce Marketing Cloud’s build-in features and best practices that will help you maintain high quality of content that goes out to your subscribers.

Fallback values in emails

When it comes to creating personalized emails, we often hear Salesforce Marketing Cloud users assume that the data that comes through will be correct and that they are not going to include fallback values for their variables. The assumption should be exactly the opposite and you should always plan for bad or missing data!

If you’re using a Profile Attribute or a Data Extension column for personalizing an email, the best practice is to always leverage the Empty() AMPscript function to check whether there is a value available.

Here’s an example: it’s much easier to use a simple personalization string such as Dear %%FirstName%%, in your email, but if for some reason the data is missing, your subscriber would see Dear , in the email they receive. This can be easily prevented by using the Empty() function and adding a fallback value: [see code snippet]

%%[
set @firstName = AttributeValue("FirstName")
if empty(@firstName) then
set @greeting = "Valued Customer"
else
set @greeting = @firstName
endif
]%%
<p>Dear %%=v(@greeting)=%%,</p>

In the above example, a Profile Attribute or a Data Extension column will be used if there’s a value available, and if not, the greeting in the email will be set to Dear Valued Customer.

Whether you’re using Lists or Data Extensions to segment the data in Salesforce Marketing Cloud, you can also leverage the “Default Value” feature to ensure that required data is always available:

Profile Attribute with a default value
Data Extension field with a default value

Using the RaiseError function in emails

The RaiseError() function, used along with a conditional statement, ensures that the email does not get sent if there is bad or missing data in the dataset used for personalizing the email. Depending on how you set the function, it can either skip the send for a current subscriber and move on to next subscriber, or it can stop the remaining send job. Here’s a summary of the function’s main features in Eliot Harper’s video:

RaiseError AMPscript Function in Salesforce Marketing Cloud

Let’s take a look at the function syntax and properties:

RaiseError(1, 2, 3, 4, 5)

OrdinalTypeRequiredDescription
1stringNError message to display
2booleanYIndicates whether function skips send for current subscriber and continues or stops. A value of true skips the send for current subscriber and moves to next subscriber. A value of false stops the send and returns an error. Function defaults to false.
3stringNAPI error code (set by the user)
4stringNAPI error number (set by the user)
5booleanNIndicates whether the function records information to data extensions before error occurs, even if the process skips the subscriber. A value of 1 retains information written to data extensions before the error occurs, even if the subscriber is skipped. A value of 0 does not retain information recorded before the error. This parameter refers to inserted, updated, upserted, or deleted information via AMPscript.

Let’s use the previous script example with the FirstName personalization, but instead of setting a fallback value in case the FirstName is missing, we will use the RaiseError() function to skip the send for that particular subscriber: [see code snippet]

%%[
set @firstName = AttributeValue("FirstName")
if empty(@firstName) then
RaiseError("First name missing", true)
else
set @greeting = @firstName
endif
]%%
<p>Dear %%=v(@greeting)=%%,</p>

You will notice, that upon previewing the email, the error message that you set will be displayed if the FirstName value is missing:

In a scenario where you would try to send this email to a subscriber with a missing FirstName value, the function would prevent from sending an email to the current subscriber and would move on to the next one.

This function should not be misused for data segmentation. Even though a subscriber is skipped or the whole job is cancelled, it still counts towards your totals as a sent message, meaning that you will pay for each suppressed email as you would for a sent one. This is a small price to pay for handling exceptions and sustaining a positive reputation with your subscribers, but it should not replace regular data cleansing and maintenance processes.

An additional thing to keep in mind is that AMPscript is interpreted top-down. This means, that if there are any other AMPscript functions before the RaiseError() function, they will all be executed, even if the send for a particular subscriber is skipped in the end.

On the other hand, if you have set the RaiseError() function to cancel the remaining send job, then any Update, Insert, and Delete AMPscript functions, which are processed in bulk during send time, will not be executed, unless you specify otherwise by setting the last of the RaiseError() properties to 1.

Bear in mind, that the second property of the RaiseError() function, which indicates whether function skips send for the current subscriber and continues or stops, will default to false if not specified. This means, that unless set to true, it will always stop the remaining send job.

Logging RaiseError results

By default, the emails and jobs suppressed by the RaiseError() function will only be visible in the NotSent tracking extract. If you’re using this function in Journey Builder emails or Triggered Sends, you will also be able to see how many emails were suppressed by checking the “Errored” column in Email Studio’s Tracking tab, under Journey Builder Sends/Triggered Sends. Unfortunately, you won’t be able to view any details of the suppressed sends in the UI.

In order to log all suppressed sends, you can create a Data Extension and add an InsertDE function to the email. Make sure to set the last property of the RaiseError() function to 1 and to include the InsertDE() function before the RaiseError() function, as everything placed after won’t get executed. This way, each email suppressed by the RaiseError() function, will also be logged in the Data Extension: [see code snippet]

%%[
set @firstName = AttributeValue("FirstName")
if empty(@firstName) then
InsertDE("RaiseErrorLog", "JobID", jobid, "EmailName", emailname_, "SubscriberKey", _subscriberkey, "EmailAddress", emailaddr)
RaiseError("Missing first name", true, 1)
else
set @greeting = @firstName
endif
]%%
<p>Dear %%=v(@greeting)=%%,</p>

Analyzing the log will help you investigate why some of the values are missing and improve your company’s data hygiene.


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Loops in AMPscript and Server-Side JavaScript

In programming languages, loops are a basic, yet a very powerful and indispensable concept. They shorten the code and reduce the need to repeat tasks. A loop is a set of instructions to be executed until a certain condition is met. In this article, we will focus on the most fundamental kind of loop, the for loop, and it’s usage in AMPscript and Server-Side JavaScript.

For loops in AMPscript

NOTE: In this article, we will focus on understanding the basic concepts of working with loops in Salesforce Marketing Cloud. For a strictly technical specification, defer to the following article: AMPscript Process Loops.

A for loop, which is actually the only loop available in AMPscript, lets you execute the same script repeatedly until an ending condition is met.

Let’s use the following example: we have a Data Extension with customer IDs and their ordered items. We want to use that dataset to send an email to each customer, listing the items each of them ordered.

Here’s our Data Extension called Orders:

CustomerIdOrderIdOrderItem
0031t000005D98UAAS4632Women’s Marine Hoodie
0031t000005D98UAAS4632Glass Love Bottle – 16.9 oz
0031t000005D98UAAS4632Eco Ballpoint Pen
0031t00000ZnTENAA33686Men’s Marine Hoodie
0031t00000YEM4ZAAX7354Leather Hampton Watch

In our example, the script contained within the loop would do the following: display a bullet point and display OrderItem name, and repeat the same action until all the OrderItems for a relevant Customer were displayed.

The most important element of any loop is the counter. The counter will define how many times the script in the loop should be executed. In order to define the counter, we must first set the criteria to find all the matching results in our Data Extension. We will use the LookupRows function to look up all rows, that match the Id of the Customer, that we want to send the email to. Then, we will use the RowCount function to determine how many results were found. The outcome of the RowCount function will be our counter and will decide how many times the script inside the loop will be repeated. Here’s the first part of our script, where we count how many records match the criteria: [click here to see the code on Github]

var @rows, @row, @rowCount, @subKey
set @subKey = _subscriberkey
set @rows = LookupRows("Orders","CustomerId", @subKey)
set @rowCount = rowcount(@rows)

Now let’s think through the loop logic. This is the verbal description of the loop that we would like to build:

Start at 1 and repeat the following in a loop until you reach the number defined in the RowCount variable: display the current index and the name of the OrderItem found in the referring row from the results of the LookupRows function. Increase the index by one and repeat the process. Stop looping once the index reaches the number defined in the counter.

Here’s how this looks in AMPscript: [click here to see the code on Github]

%%[ var @rows, @row, @rowCount, @subKey, @counter, @orderItem
set @subKey = _subscriberkey
set @rows = LookupRows("Orders","CustomerId", @subKey)
set @rowCount = rowcount(@rows)
for @counter = 1 to @rowCount do /* repeat in a loop for as many times as defined in rowCount */
set @row = row(@rows, @counter) /* get row based on counter */
set @orderItem = field(@row,"OrderItem")
]%%
<br>
Order item %%=v(@counter)=%%: %%=v(@orderItem)=%%<br>
%%[ next ]%%
view raw lookup-loop.amp hosted with ❤ by GitHub

You will notice the next keyword at the end of the loop – each time this keyword is reached, the system compares the current index to the value of the counter defined by the RowCount function. If the value is not equal to the end index, the loop will repeat until the end index is reached.

Here’s a full script that we can now use in an email, with added exception handling that will show “No items to display” in case the LookupRows function doesn’t find any matching records for a subscriber in the Orders Data Extension: [click here to see the code on Github]

<html>
<body>
Dear Customer,<br>
Here are the details of your order:<br>
%%[ var @rows, @row, @rowCount, @subKey, @counter, @orderItem
set @subKey = _subscriberkey
set @rows = LookupRows("Orders","CustomerId", @subKey)
set @rowCount = rowcount(@rows)
if @rowCount > 0 then
for @counter = 1 to @rowCount do
set @row = row(@rows, @counter) /* get row based on counter */
set @orderItem = field(@row,"OrderItem") ]%%
&#8226; Order item %%=v(@counter)=%%: %%=v(@orderItem)=%%<br>
%%[ next ]%%
%%[ else ]%%
No items to display
%%[ endif ]%%
<br>
We hope to see you again soon!
</body>
</html>
view raw ampscript-loop.html hosted with ❤ by GitHub

For loops in Server-Side Javascript

While JavaScript offers at least five different kinds of loops, and all of those can be used in Salesforce Marketing Cloud, in this article we will focus on the most basic one, the for loop.

We will create a similar solution to the one created in the AMPscript example above. We will use the same Data Exteniosn called Orders and we will use a loop to display all the OrderItems for a Customer on a CloudPage.

The first thing we have to do in order to interact with a data extension via server-side JavaScript is to initialize the Data Extension object. Then, we will use the Server-Side JavaScript Rows.Lookup function to find all rows that match the Id of the Customer. For the purpose of this tutorial, we will hardcode the CustomerId into the script: [click here to see the code on Github]

<script runat="server">
Platform.Load("core","1");
var custId = "0031t000005D98UAAS"
var OrdersDE = DataExtension.Init("Orders");
var rows = OrdersDE.Rows.Lookup(["CustomerId"], [custId]);
</script>

The above script will return an ArrayList, which is a collection of elements. Arrays use numbers to access its elements and the numbering always starts at zero:

Arrays in JavaScript are zero-based. This means that JavaScript starts counting from zero when it indexes an array. In other words, the index value of the first element in the array is “0” and the index value of the second element is “1”, the third element’s index value is “2”, and so on.

https://blog.kevinchisholm.com/javascript/javascript-array-length-always-one-higher/

In AMPscript we used the RowCount function to determine the counter for our loop. In JavaScript, we will use the Array length Property. Inside the loop, we will display the current index (i) and the name of the element (OrderItem) that has this index number in the ArrayList. Remember that we will start counting at “0”, not at “1” like we did in AMPscript.

Then, we will increase the index by one and repeat the process.

In JavaScript, the ++ notation is the increment operator, which means that i++ is exactly the same thing as i = i+1. The script will stop looping once the index reaches the number defined in the counter (rows.length). Here’s the script: [click here to see the code on Github]

<script runat="server">
Platform.Load("core","1");
var custId = "0031t000005D98UAAS"
var OrdersDE = DataExtension.Init("Orders");
var rows = OrdersDE.Rows.Lookup(["CustomerId"], [custId]);
if(rows.length >= 1) {
for (i = 0; i < rows.length; i++) {
OrderItem = rows[i]["OrderItem"];
Write("&#8226; OrderItem " + i + ":" + OrderItem + "<br>");
}
}
</script>

Here’s the full script that we can now use on a CloudPage, with added exception handling that will show “No items to display” in case there are no matching records found in the Orders Data Extension: [click here to see the code on Github]

Dear Customer,<br>
Here are the details of your order:<br>
<script runat="server">
Platform.Load("core","1");
var custId = "0031t000005D98UAAS"
var OrdersDE = DataExtension.Init("Orders");
var rows = OrdersDE.Rows.Lookup(["CustomerId"], [custId]);
if(rows.length >= 1) {
for (i = 0; i < rows.length; i++) {
OrderItem = rows[i]["OrderItem"];
Write("&#8226; OrderItem " + i + ":" + OrderItem + "<br>");
}
}
else {
Write("No items to display");
}
</script>
<br>
We hope to see you again soon!
view raw ssjs-loop.html hosted with ❤ by GitHub

The is how the results will be displayed on a CloudPage:

Additional resources

Here is a list of additional resources to help you understand both the general concept of loops, as well as using loops in AMPscript and Server-Side JavaScript:


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Retrieve client IP address and geolocation in CloudPages

There are many reasons for checking the client IP address, most common include tracking and personalization. What can you find out about the visitors of a webpage from their IP address? You can identify their ISP, figure out approximately where they’re located and see how often they (or someone else sharing their router) visit your website.

In the context of CloudPages, we most often see IP tracking for personalization purposes. By identifying the visitor’s location, you can automatically display text in their local language and control what kind of content they see.

Identify client IP using AMPscript

The X-Forwarded-For (XFF) HTTP header is a standard header for identifying the originating IP address of a client connecting to a web server. You can easily access this header by using the AMPscript HTTPRequestHeader function, which will return a specified header from an HTTP request. Here’s how to retrieve an IP address on a CloudPage using AMPscript:

Your IP: %%=HTTPRequestHeader("X-Forwarded-For")=%%
view raw x-forwarded-for.html hosted with ❤ by GitHub

Identify client IP using SSJS

There are dedicated Server-Side JavaScript HTTP Properties Functions, that allow you to retrieve various types of HTTP Request object properties and platform application values. The client browser passes this information to the server during an HTTP interaction, so this object contains information regarding the browser and session. One of the available properties to use with the HTTP Request object is ClientIP, which returns the IP address of the requesting client as a string value. Here’s how to retrieve an IP address on a CloudPage using SSJS:

<script type="javascript" runat="server">
Platform.Load("core","1");
var ip = Platform.Request.ClientIP();
Write("Your IP: " + ip);
</script>
view raw clientIP.html hosted with ❤ by GitHub

This is the preferred way to retrieve the client IP, as Request.ClientIP() is a dedicated and supported function, while using the XFF HTTP header proved to be unreliable in the past.

HTTP Properties Functions allow you to retrieve other useful information, for example, browser metadata or the URL of the referring web address.

Discover the precise physical location of a given IP address

The functionality of identifying a physical location of a given IP address requires using a third party API service. There are many IP Geolocation API providers and most of them have a free plan available, as well as paid plans for bigger enterprises. My preferred one is ipify.org, which allows you to run up to 1000 queries per month for free. Once you register, you will obtain your personal apiKey, that will be used for making the calls:

<script type="javascript" runat="server">
Platform.Load("core","1");
var ip = Platform.Request.ClientIP();
var apiKey = '' //insert apiKey obtained from https://geo.ipify.org/subscriptions
var url = 'https://geo.ipify.org/api/v1?apiKey=&#39;;
var response = HTTP.Get(url + apiKey + ip);
Write("response.Status: " + response.Status + '<br />');
Write("response.Content: " + response.Content);
</script>

The response will contain information about the country, region, city, latitude, longitude, postal code, timezone and GeoNames Id. Additionally, it will also show autonomous system (AS) info if available.

{
"ip": "111.100.64.10",
"location": {
"country": "JP",
"region": "Saitama",
"city": "Saitama",
"lat": 35.867,
"lng": 139.65,
"postalCode": "337-0042",
"timezone": "+09:00",
"geonameId": 6940394
},
"as": {
"asn": 2516,
"name": "KDDI",
"route": "111.100.0.0/16",
"domain": "http://www.kddi.com/english/index.html",
"type": "NSP"
},
"isp": "Kddi Corporation",
"connectionType": "cable"
}

Click here to see this script in action.

As a side-note, remember that under GDPR, IP addresses are considered personal data. Tracking the IPs of your EEA based users without their consent falls under the rules of GDPR.

Unsubscribe and Log an UnsubEvent with a LogUnsubEvent Execute Call

The LogUnsubEvent provides a convenient way of handling unsubscribes when you create your own unsubscribe landing page or profile center functionality. This call allows you to unsubscribe a subscriber and log an UnsubEvent that is tracked against a specific Job, which means that you will be able to track from which email they unsubscribed and see the results on the tracking dashboard. You can configure the call to either unsubscribe a subscriber from a specific list, publication list, or all subscribers, which will effectively unsubscribe them from receiving any emails.

What’s more important, if you’re using Marketing Cloud Connect to connect with your Salesforce Sales or Service Cloud org, the unsubscribe will be captured on the subscriber record in Sales/Service Cloud. It will automatically check the Email Opt Out (HasOptedOutOfEmail) flag and will also be visible in the Individual Email Results for the corresponding email send.

The LogUnsubEvent Execute call uses the following parameters:

  • SubscriberID – The Marketing Cloud generated ID that uniquely identifies a subscriber.
  • SubscriberKey – The client supplied ID that uniquely identifies a subscriber.
  • EmailAddress – The email address of the subscriber.
  • JobID – The ID of the Job that sent the message.
  • ListID – The ID of the List that the subscriber belonged to. You can use subscriber or publication lists (not suppression lists).
  • BatchID – The ID of the Batch within the Job.
  • Reason – (Optional) The reason the subscriber is being unsubscribed.

The parameters can be divided into 3 sections:

  1. Subscriber context
  2. Job context
  3. Unsub reason

If you make this call from the parent unit of an Enterprise 2.0 account, ensure that you include the ClientID of the child business account to return information specific to that business unit.

Subscriber Context

The Subscriber Context is defined by the SubscriberID, SubscriberKey and EmailAddress parameters. You must supply at least one of these parameters. If you provide more than one of these parameters, we retrieve the Subscriber using one of the values and validate that the other values match the retrieved Subscriber. If they don’t match, an error returns.

If the SubscriberKey permission is turned on and you supply the EmailAddress parameter, you must supply either the SubscriberID or the SubscriberKey.

Job Context

The Job Context is defined by the JobID, ListID and BatchID parameters. These values are used to determine which Job the UnsubEvent is tracked against. The subscriber is also unsubscribed from the List that the Job was sent to. You don’t need to supply all three values. The system looks up any missing values using the following rules:

  1. If the JobID is supplied, we can lookup a missing ListID and/or BatchID.
  2. If the ListID is supplied, we can lookup a missing JobID and/or BatchID.
    1. If the JobID is missing, we use the most recent JobID that the subscriber was sent to.
    2. This may not be the Job that the Subscriber is acting upon.
  3. If only the BatchID is supplied, we cannot lookup the remaining information and the job context is not defined.

If the job context cannot be established because you did not supply any of these parameters or only supplied the BatchID, the UnsubEvent is not created. The subscriber is also Master Unsubscribed from the system of being unsubscribed from a particular list. Remove the ListID to address the All Subscribers list in an account.

Unsub Reason

This is used to specify the reason the subscriber is being unsubscribed from the system. If the reason is not supplied, the default value is used: Unsubscribed via Log Unsub Event Execute call.

Here is an example SOAP Request envelope:

<soap-ENV:Body>
<ExecuteRequestMsg xmlns="http://exacttarget.com/wsdl/partnerAPI">
<Requests>
<Name>LogUnsubEvent</Name>
<Parameters>
<Name>SubscriberID</Name>
<Value>123456</Value>
</Parameters>
<Parameters>
<Name>SubscriberKey</Name>
<Value>Key for username@example.com</Value>
</Parameters>
<Parameters>
<Name>EmailAddress</Name>
<Value>help@example.com</Value>
</Parameters>
<Parameters>
<Name>JobID</Name>
<Value>18099</Value>
</Parameters>
<Parameters>
<Name>ListID</Name>
<Value>17914</Value>
</Parameters>
<Parameters>
<Name>BatchID</Name>
<Value>0</Value>
</Parameters>
</Requests>
</ExecuteRequestMsg>
</SOAP-ENV:Body>

We will now look at three different ways to implement this solution on a CloudPage.

LogUnsubEvent using AMPscript

This is probably the most common way to use the LogUnsubEvent call. Below script will retrieve the SubscriberKey, JobId, ListId and BatchId if you link to the unsubscribe page from your email using the CloudPagesURL function. Depending on whether a list/publication list was selected for the send, it will unsubscribe a subscriber from that particular list, or it will unsubscribe the subscriber from all emails if you used All Subscribers.

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
%%[
VAR @skey, @jid, @reason, @lue, @lue_prop, @lue_statusCode, @overallStatus, @requestId, @Response, @Status, @Error
SET @skey = _subscriberkey
SET @jid = RequestParameter("jobid")
SET @listid = RequestParameter("listid")
SET @batchid = RequestParameter("batchid")
SET @reason = "AMPscript one click unsubscribe"
SET @lue = CreateObject("ExecuteRequest")
SetObjectProperty(@lue,"Name","LogUnsubEvent")
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "SubscriberKey")
SetObjectProperty(@lue_prop, "Value", @skey)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "JobID")
SetObjectProperty(@lue_prop, "Value", @jid)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "ListID")
SetObjectProperty(@lue_prop, "Value", @listid)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "BatchID")
SetObjectProperty(@lue_prop, "Value", @batchid)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "Reason")
SetObjectProperty(@lue_prop, "Value", @reason)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_statusCode = InvokeExecute(@lue, @overallStatus, @requestId)
SET @Response = Row(@lue_statusCode, 1)
SET @Status = Field(@Response,"StatusMessage")
SET @Error = Field(@Response,"ErrorCode")
]%%
<script runat="server">
}catch(e){
Write(Stringify(e));
}
</script>
@Response: %%=v(@Response)=%%<br>
@Status: %%=v(@Status)=%%<br>
@Error: %%=v(@Error)=%%<br>

LogUnsubEvent using Server-Side JavaScript

The implementation of the LogUnsubEvent call in SSJS will be almost identical to the AMPscript solution, as the methods for accessing SOAP object data with SSJS are primarily wrappers around AMPScript functions.

Like in the previous example, the script will retrieve the SubscriberKey, JobId, ListId and BatchId if you link to the unsubscribe page from your email using the CloudPagesURL function and will unsubscribe a subscriber either from a list or All Subscribers, depending on which one you use at send time.

<script runat="server">
Platform.Load("core","1");
var subkey, jid, lue, lid, bid, lue_prop, Response;
var subkey = Attribute.GetValue("_subscriberkey");
var jid = Attribute.GetValue("jobid");
var lid = Attribute.GetValue("listid");
var bid = Attribute.GetValue("_JobSubscriberBatchID");
lue = Platform.Function.CreateObject("ExecuteRequest");
Platform.Function.SetObjectProperty(lue,"Name","LogUnsubEvent");
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "SubscriberKey");
Platform.Function.SetObjectProperty(lue_prop, "Value", subkey);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "JobID");
Platform.Function.SetObjectProperty(lue_prop, "Value", jid);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "ListID");
Platform.Function.SetObjectProperty(lue_prop, "Value", lid);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "BatchID");
Platform.Function.SetObjectProperty(lue_prop, "Value", bid);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "Reason");
Platform.Function.SetObjectProperty(lue_prop, "Value", "SSJS one click unsubscribe");
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
var statusAndRequest = [0,0];
try{
Response = Platform.Function.InvokeExecute(lue, statusAndRequest);
Write(Stringify(Response))
}catch(e){
Write(Stringify(e));
}
</script>

LogUnsubEvent using WSProxy

WSProxy is a new object for Server-Side JavaScript, introduced by Salesforce in 2018. It acts as a proxy between the Marketing Cloud SOAP Web Service and SSJS. The WSProxy object is native to the platform and simpler to use than the SSJS methods. The object reduces overhead and increases the speed of your API calls.

Therefore, the below script is the most simple, and what’s more important, the fastest way to execute the LogUnsubEvent API call. In a speed test ran using the Chrome DevTools, it proved to be the fastest one to load on a CloudPage, with AMPscript just slightly slower and SSJS the slowest one, taking twice as much time to load.

Just like in the previous examples, the script will retrieve the SubscriberKey, JobId, ListId and BatchId if you link to the unsubscribe page from your email using the CloudPagesURL function and will unsubscribe a subscriber either from a list or All Subscribers, depending on which one you use at send time.

<script runat="server">
Platform.Load("Core","1.1.1");
var subkey = Attribute.GetValue("_subscriberkey");
var jid = Attribute.GetValue("jobid");
var lid = Attribute.GetValue("listid");
var bid = Attribute.GetValue("_JobSubscriberBatchID");
var prox = new Script.Util.WSProxy();
var props = [
{ Name: "SubscriberKey", Value: subkey },
{ Name: "JobID", Value: jid },
{ Name: "ListID", Value: lid },
{ Name: "BatchID", Value: bid },
{ Name: "Reason", Value: "WSProxy one click unsubscribe" }
];
try{
var data = prox.execute(props, "LogUnsubEvent");
Write(Stringify(data));
}catch(e){
Write(Stringify(e));
}
</script>

If you would like to find out more about logging an UnsubEvent or WSProxy, check out the following articles:

Make a simple API call in Salesforce Marketing Cloud using AMPscript

In this tutorial, I would like to show you how to make a simple API call using the HTTPGet function. In AMPscript, there are three HTTP functions used to interact with third-party APIs (HTTPGet, HTTPPost, HTTPPostTo). The HTTPGet function retrieves content from a publicly accessible URL. In the ampscript.guide, you can see an example of the HTTPGet function that pulls the content from httpbin.org, a simple HTTP request and response service that doesn’t require any authentication. In this tutorial, we are going to connect to NASA’a open API and display the Astronomy Picture of the Day on our CloudPage.

NASA’s Astronomy Picture of the Day website

Get your NASA API key

The first thing that you will need to do, is to sign up to receive your own API key to access and use NASA’s APIs: get your NASA API key. After you sign up, you will get your api_key in the email, along with the URL used to make the request. When you click the URL with your api_key appended, you will get a response in JSON format with all the details about today’s image, including a URL to the image itself:


"date":"2019-08-14",
"explanation":"What's that next to the Moon? Saturn. In its monthly trip around the Earth - and hence Earth's sky - our Moon passed nearly in front of Sun-orbiting Saturn earlier this week.",
"hdurl":"https://apod.nasa.gov/apod/image/1908/MoonSaturn_Patonai_1280.jpg",
"media_type":"image",
"service_version":"v1",
"title":"Saturn Behind the Moon",
"url":"https://apod.nasa.gov/apod/image/1908/MoonSaturn_Patonai_960.jpg"
}

Create a CloudPage

Now let’s create a CloudPage where we will make the API call and display the APOD image. In your CloudPage, you will need to include three variables: @apikey, @url and @response. For the purpose of this tutorial, you can hardcode the @apikey into your CloudPage, but as a good practice, it’s better to store any Keys, IDs or Secrets outside of CloudPages and reference them in your AMPscript. The @url we are going to use is the same as the one you got in your email: https://api.nasa.gov/planetary/apod and we are going to append the @apikey to it. The third variable, @response, is the response in JSON format with the details of today’s image. Your code should now look like this:

%%[
var @apikey, @url, @response
set @apikey = Lookup("NASAapi","apikey","id","apod") // keep your apikey in a Data Extension
set @apikey = "7Apb3QN7qVsXPuPDcSZBKAPf8twca03C5w4Oxuyc" // or hardcode it into your CloudPage
set @url = concat('https://api.nasa.gov/planetary/apod?api_key=&#39;,@apikey)
set @response = HTTPGET(@url,false,0,@CallStatus)
]%%
view raw nasa-api-key.amp hosted with ❤ by GitHub

The parameters used in the HTTPGet call are:

HTTPGet(1,2,3,4)

  1. String with URL used to retrieve content
  2. True/false string that controls whether the process continues on an error
  3. String that defines whether the function allows empty content
  4. String which outputs the function status (a value of 0 indicates the status is successful). You can display the status inline after you make the call by adding %%=v(@CallStatus)=%% to the script.

Source: https://ampscript.guide/httpget/

Let’s now add an inline output of the response and publish the CloudPage. Your code should now look like this:

%%[
var @apikey, @url, @response
set @apikey = Lookup("NASAapi","apikey","id","apod") // keep your apikey in a Data Extension
set @apikey = "7Apb3QN7qVsXPuPDcSZBKAPf8twca03C5w4Oxuyc" // or hardcode it into your CloudPage
set @url = concat('https://api.nasa.gov/planetary/apod?api_key=&#39;,@apikey)
set @response = HTTPGET(@url,false,0,@CallStatus)
]%%
@response: %%=v(@response)=%%

When you publish the CloudPage, you will see the response in JSON format, that contains the details about the image, along with the image URL, which we now need to extract in order to display the image in our CloudPage.

Parse JSON using Server-Side JavaScript

Unfortunately, AMPscript doesn’t have a function that we could use to parse JSON. If you know the format of the response, you could use the Substring function to extract the URL, but this would be a very clumsy solution – that’s why it’s much better to use the Server-Side JavaScript ParseJSON function. We will first have to pass the @response from AMPscript to SSJS using Variable.GetValue function, then parse it and pass the URL of the high definition image back to AMPscript using Variable.SetValue function. This is the script that you need to add to your CloudPage:

<script runat="server" language="javascript">
Platform.Load("Core","1");
//Get the @response variable from AMPscript
var response = Variable.GetValue("@response");
//Parse JSON
var json = Platform.Function.ParseJSON(response);
//Set the @hdurl variable to be accessible in AMPscript
Variable.SetValue("@hdurl",json.hdurl);
</script>
view raw parse-json.js hosted with ❤ by GitHub

Display the image in HTML

Now it’s time to add one final line of code to your CloudPage. We are passing the URL of the high definition image from SSJS to AMPscript in the @hdurl variable and now we need to display it. You can use the HTML image tag:

<img style="height:100vh" src="%%=v(@hdurl)=%%">
view raw nasa-image-tag.html hosted with ❤ by GitHub

The complete code on your CloudPage should now look like this:

%%[
var @apikey, @url, @response
set @apikey = Lookup("NASAapi","apikey","id","apod") // keep your apikey in a Data Extension
set @apikey = "7Apb3QN7qVsXPuPDcSZBKAPf8twca03C5w4Oxuyc" // or hardcode it into your CloudPage
set @url = concat('https://api.nasa.gov/planetary/apod?api_key=&#39;,@apikey)
set @response = HTTPGET(@url,false,0,@CallStatus)
]%%
<script runat="server" language="javascript">
Platform.Load("Core","1");
//Get the @response variable from AMPscript
var response = Variable.GetValue("@response");
//Parse JSON
var json = Platform.Function.ParseJSON(response);
//Set the @hdurl variable to be accessible in AMPscript
Variable.SetValue("@hdurl",json.hdurl);
</script>
<img style="height:100vh" src="%%=v(@hdurl)=%%">

Publish your CloudPage and enjoy!

Here’s the link to my CloudPage with the Astronomy Picture of the Day: https://pub.s10.exacttarget.com/c1my0fe3fjn

Debugging AMPscript

Debugging AMPscript in Salesforce Marketing Cloud can be a pain, as there is no built-in feature that would show script errors in CloudPages. The idea to add a debugging feature to CloudPages has been hanging in the Traiblazer Community’s “Ideas” section for two years now and haven’t yet reached the point threshold set by Salesforce – so if you’re reading this, visit the Add Debugging Support for CloudPages site and give it a thumb up!

Debugging AMPscript in CloudPages

When you’re working on a script in a CloudPage, the first indication that there is something wrong is the fact that when you try to publish your page, you never get a preview and the throbber just keeps spinning:

If you publish the page anyway and later try to access it, you will get the infamous “500 – Internal server error. There is a problem with the resource you are looking for, and it cannot be displayed.” message without any indication of what went wrong:

To get some details about the error behind this, try wrapping your AMPscript with a Server-Side JavaScript try/catch block:

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
%%[ your AMPscript block goes here ]%%
<script runat="server">
}catch(e){
Write(Stringify(e));
}
</script>
view raw ssjs-try-catch.html hosted with ❤ by GitHub

Here is a snippet of an actual script:

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
%%[
set @email = emailaddr
if (@email != "") then
set @rows = LookupRows("LeadDataExtension","Email", @email)
set @rowCount = rowcount(@rows)
if @rowCount > 0 then
set @row = Row(@rows, 1)
set @FirstName = Field(@row,"FirstName")
endif
endif
]%%
<script runat="server">
}catch (e) {
Write("<b>Error Message:</b> " + Stringify(e.message) + "<br><br><b>Description:</b> " + Stringify(e.description));
}
</script>
Hello %%=v(@FirstName)=%%!

This will allow you to catch some of the possible errors related to your AMPscript – here are some example error messages:

Error Message: "Call to create the salesforceobject Contact failed! Error status code: REQUIRED_FIELD_MISSING\nError message: Required fields are missing: [LastName]"

Description: "ExactTarget.OMM.FunctionExecutionException: Call to create the salesforceobject Contact failed! Error status code: REQUIRED_FIELD_MISSING\nError message: Required fields are missing: [LastName]\r\n Error Code: CREATESFOJBECT_FUNC_ERROR\r\n - from Jint\r\n\r\n"

As you can see from the above example, the error message and description will give you a clue which AMPscript function is causing the problems.

If you add a console log to the try/catch script, you will be able to see the message in your browser’s console, while it will remain hidden from the subscriber visiting your page:

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
%%[ your AMPscript block goes here ]%%
<script runat="server">
}
catch(e) {
Variable.SetValue("errorMessage", Stringify(e.message) + Stringify(e.description));
}
</script>
<script runat="client">
console.log(`%%=v(@errorMessage)=%%`);
</script>

Now when you open your CloudPage, you will be able to see the error message when you access the console (press F12 in Chrome or press Command+Option+C on Mac or Control+Shift+C on Windows):

Debugging with the try/catch statement will show you some errors related to the functions you used or if a record referenced in your lookup function wasn’t found in the Data Extension. Unfortunately, you will still get the 500 error in other cases, for example, if you are referencing a non-existing Data Extension in your lookup function, or if you are trying to query a parameter which hasn’t been passed correctly. In that case, the best thing you can do is to go through your script line by line, and display all the variables in your script one by one, until you find the function which is not resolving correctly, here is an example:

%%[
set @email = emailaddr
if (@email != "") then
set @rows = LookupRows("LeadsDataExtension","Email", @email)
set @rowCount = rowcount(@rows)
if @rowCount > 0 then
set @row = Row(@rows, 1)
set @UID = Field(@row,"UID")
endif
endif
]%%
<span style="color: green">@email:</span> %%=v(@email)=%%
<span style="color: green">@rowCount:</span> %%=v(@rowCount)=%%
<span style="color: green">@UID:</span> %%=v(@UID)=%%
@email: test@test.com
@rowCount: 0
@UID:

You can also use the Output function to output nested functions at the location where the code block appears in your CloudPage, without having to output them inline later using %%=v()=%%. Remember that it will only output a nested function, and it won’t work if you try to resolve a single variable, for example Output(@UID).

One last thing worth mentioning is to always remember that the purpose of AMPscript is to personalize a CloudPage for each subscriber, so you cannot expect your CloudPage to resolve correctly from the CloudPage link in the editor. You will always need to pass the required parameters used in your script, either by adding them to the URL (?myparam1={id1}&myparam2={id2}) or by creating a link or a button in an email using the CloudPagesURL function, and previewing that email against a subscriber.

Debugging AMPscript in Email Studio

It is a little bit easier to debug AMPscript in Email Studio because the system will show you the errors as soon as you try to preview the email against a subscriber. It is also a bit faster because you will see the results on-screen, without the need to publish the changes every time. Unfortunately, not all AMPscript functions will work in an email, but in general, it’s good for the cases when you are trying to find a bug in your script and have to go line by line.

Note that there are two separate error messages – for the HTML version and the Text version of your email. It often happens, that you copy an existing email to keep a consistent design, and while you make changes to the HTML version, you forget to update the text version of your email. This can lead to errors, with outdated AMPscript functions hidden in the text version.

AMPscript best practices

Always remember to follow the best practices and wherever possible, use blocks of AMPscrip. Make comments in your script so that it’s easy to read when someone else has to take over from you and use indentation. Use the RaiseError function in emails and crate rules for exception handling in CloudPages.

If you are looking for more resources on AMPscript debugging and best practices, check out the ampscript.guide and the official documentation.