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.

Simple Marketing Cloud App hosted on a CloudPage

DISCLAIMER: The Marketing Cloud App component has been designed for use with externally hosted apps. Use any alternative solutions presented in this article with caution. The settings we are going to use are simplified compared to the ones required by externally hosted apps, so make sure that only designated users have access to the apps you create using this workaround.

A Marketing Cloud app is an externally hosted application that is iframed into Marketing Cloud. Marketing Cloud apps include custom apps built by your organization or apps installed from AppExchange. You launch an app via the Marketing Cloud app menu:

A common Marketing Cloud app you might be familiar with is Query Studio for Salesforce Marketing Cloud:

Query Studio

Query Studio is an externally hosted app, which lets you write and run SQL queries and instantly see the query results onscreen, with a similar experience to SQL Server Studio or MySQL workbench.

Another example of a Marketing Cloud app that you might be familiar with is Deployment Manager for Marketing Cloud:

Deployment Manager

Deployment Manager lets you import and export Marketing Cloud Configuration and easily distribute it to other Marketing Cloud Enterprises and Business Units.

Both those apps, Query Studio and Deployment Manager, have been developed by Salesforce Labs and are publicly available to download for free from AppExchange.

There are many more commercial apps for Marketing Cloud in AppExchange, all of them hosted externally. This means that for security, they all must use a web app or public app OAuth 2.0 integration to acquire an access token, and use that access token to request information about the end-user by calling the v2/userinfo REST endpoint.

Here’s a brief overview of creating an externally hosted Marketing Cloud app:

  1. Create an installed package, or navigate to an existing package.
  2. Under Components, click Add Component.
  3. Select Marketing Cloud App.
  4. Enter a name and description for your app.
  5. Enter your app’s login, redirect, and logout URLs. Point to localhost or test locations first, if needed, and edit these values later. All URLs must be HTTPS (TLS).
    • Login – Marketing Cloud uses this endpoint to iframe your externally hosted app. Your app can show anything here. Your app must set a cookie at login. To retrieve information about the end user, ensure that your externally hosted app immediately kicks off Marketing Cloud’s OAuth 2.0 authorization code flow and then calls the v2/userinfo route after calling your login endpoint. Legacy packages only: Marketing Cloud posts the JWT here.
    • Logout – Marketing Cloud performs a GET on the logout endpoint from the browser. This logout URL ends the user’s session and unsets the cookie set on login. When the user logs out of Marketing Cloud, the app session also ends.
  6. Save the component.
  7. Log out of Marketing Cloud, and log back in to see your app in the AppExchange menu in Marketing Cloud.

Source: Create a Marketing Cloud App

But what if you cannot, or don’t even want to build an elaborate, externally hosted app? If you’re looking for a simple, yet elegant solution that will only be used internally by you and your colleagues, hosting your app in Salesforce Marketing Cloud’s Web Studio might be the workaround that will fulfil your requirements.

My teams have built numerous Marketing Cloud apps that way. To give you some examples of what we used them for:

  • monitoring all journeys in the account,
  • monitoring all automations in the account,
  • creating robust email tracking dashboards,
  • creating functionalities to help users get their work done faster and with less manual steps.

Let’s get to it!

Create an app on a CloudPage

Let’s start by creating a simple app on a CloudPage. We can use an app I already described on this blog in the article called Find a Data Extension and it’s folder path using SSJS. Here is the full script you will need for your CloudPage:

<table style="padding: 20px;"><tr><td>
<b>How would you like to identify the Data Extension?</b><br>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<select name="DEprop">
<option value="Name">Name</option>
<option value="CustomerKey">External Key</option>
</select>
equals
<input type="text" name="DEval" value="" maxlength="128"><br>
<input type="submit" value="Submit">
</form><br><b>Folder path: </b>
<script runat="server">
Platform.Load("core","1.1.5");
var DEprop = Request.GetQueryStringParameter("DEprop");
var DEval = Request.GetQueryStringParameter("DEval");
var FindDE = DataExtension.Retrieve({Property:DEprop,SimpleOperator:"equals",Value:DEval});
var FolderID = FindDE[0].CategoryID;
var DEname = FindDE[0].Name;
var list = [];
list.push(DEname);
var path = function(id) {
if (id> 0) {
var results = Folder.Retrieve({Property:"ID",SimpleOperator:"equals",Value:id});
list.unshift(results [0].Name);
return path(results[0].ParentFolder.ID);
} else {
return id;
}
};
path(FolderID);
Write(list.join("> "));
</script>
</td></tr></table>
Find a Data Extension and it’s folder path using SSJS

Now you can publish the CloudPage and test if it works correctly before we move on to the next step.

Create a Marketing Cloud App

Let’s now move on to the main Setup and in there to Platform Tools > Installed Packages. On the page with All Packages click on New to add a new package. Give the package a name and describe it’s function (note, that this name and description will only be visible to administrators who have access to setup in Salesforce Marketing Cloud).

In the Components section, click on Add Component and chose a Marketing Cloud App:

Now you will need to Set Marketing Cloud App Properties:

  • Name – the name of your app which will be visible for all users
  • Description – description of what your app does
  • Login Endpoint – the external URL of the CloudPage you created and published earlier, eg. https://pub.s10.exacttarget.com/xzy
  • Logout Endpoint – use the same URL as above

Once your app is saved, log out of Marketing Cloud and log back in, to see your app in the main menu, under the AppExchange icon, from where you can launch it:

Access and Sharing

Just like with all enhanced packages, you can license packages installed from parent business units to other business units across your enterprise. On top of that, you can choose which users should be able to access the app and from which BUs. This means that in a few easy clicks, you can either:

  • give access to the app to all users in all Business Units,
  • choose certain Business Units, where all existing and future users of those BUs would be granted access to the app,
  • or pick your designated users and choose in which Business Units they should be able to access the app.

Here’s a step-by-step guide on Licensing for Enhanced Packages:

Installed Packages in Marketing Cloud
Manage Licensing for Installed Packages
  1. From the detailed view for any package, click the Access tab to manage user licensing.
  2. Search for a business unit to grant or restrict licensing for the package. If you have only one business unit, the search field and tree is hidden.
  3. License specific users in the selected business unit. When users are added, add licensing for those users here.
  4. For server-to-server integrations only: To assign licenses to users or to make API requests on behalf of this business unit, enable the package’s server-to-server integration for that business unit. You can assign licenses only if the package contains another component in addition to the server-to-server API integration, which isn’t licensable. Enable Business Units isn’t shown for other integration types.
  5. License all current and future users in the business unit.
  6. For server-to-server integrations only: Enable the package’s server-to-server integration for all business units in your account. You can assign licenses to users in all business accounts and make API requests on behalf of all business units in your account. This option isn’t shown for other integration types.
  7. License all current and future users for all business units in your account. If this option isn’t shown, you don’t have permission to administer installed packages in all business units in your account. To gain access, work with your account administrator.

Make your users feel at home

If the app you’re planning to create will be processing a lot of data or multiple requests that might take some time, you can add a Salesforce-style spinner that your users are familiar with.

Now you’re all set, so have fun with creating your own app!


Questions? Comments?

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

Salesforce-style spinner for use on CloudPages

Spinners are loading indicators that can be shown when retrieving data or performing other operations.

We will use a common spinner that Salesforce Lightening users are familiar with – the full documentation can be found here. This spinner is a Lightning component, which means that it cannot be directly used on a CloudPage. My awesome colleague Anna rewrote it into HTML and CSS, so that it can be easily copied and pasted for use on a CloudPage. This way, you can emulate the look and feel of other Salesforce clouds on your CloudPages.

It doesn’t necessarily add any value for external users of your CloudPages, but it can improve the experience of Salesforce Marketing Cloud users who internally use apps created on CloudPages and are familiar with other Salesforce clouds.

Here’s a preview of the spinner we will create in a few easy steps:

Let’s start with adding the CSS for our spinner. You can paste below code anywhere in your existing style-sheet, or create a separate one and link to it in the document head:

.slds-spinner_container{position:absolute;top:0;right:0;bottom:0;left:0;z-index:9050;background-color:hsla(0,0%,100%,.75);visibility:visible;opacity:1;transition:opacity .2s ease,visibility 0;transition-delay:0s,.3s;display:none}
.slds-spinner{position:absolute;top:50%;left:50%;z-index:9051;transform:translate(-50%,-50%) rotate(90deg)}
.slds-spinner,.slds-spinner__dot-a,.slds-spinner__dot-b{transform-origin:50% 50%;will-change:transform}
.slds-spinner__dot-a,.slds-spinner__dot-b{position:absolute;top:0;left:0;width:100%}
.slds-spinner:after,.slds-spinner:before,.slds-spinner__dot-a:after,.slds-spinner__dot-a:before,.slds-spinner__dot-b:after,.slds-spinner__dot-b:before{content:"";position:absolute;background:#b0adab;border-radius:50%;animation-duration:1s;animation-iteration-count:infinite;transform:translateZ(0)}
.slds-spinner__dot-a{transform:rotate(60deg)}
.slds-spinner__dot-b{transform:rotate(120deg)}
.slds-spinner:before{animation-delay:-.083s}
.slds-spinner__dot-a:before{animation-delay:.083s}
.slds-spinner__dot-b:before{animation-delay:.25s}
.slds-spinner:after{animation-delay:.41666667s}
.slds-spinner__dot-a:after{animation-delay:.583s}
.slds-spinner__dot-b:after{animation-delay:.75s}
@keyframes dotsBounceBefore-medium {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(-.5rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateZ(0)}
}
@keyframes dotsBounceAfter-medium {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(.5rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateX(0)}
}
.slds-spinner–large,.slds-spinner_large{width:2.75rem}
.slds-spinner–large .slds-spinner__dot-a:after,.slds-spinner–large .slds-spinner__dot-a:before,.slds-spinner–large .slds-spinner__dot-b:after,.slds-spinner–large .slds-spinner__dot-b:before,.slds-spinner–large.slds-spinner:after,.slds-spinner–large.slds-spinner:before,.slds-spinner_large .slds-spinner__dot-a:after,.slds-spinner_large .slds-spinner__dot-a:before,.slds-spinner_large .slds-spinner__dot-b:after,.slds-spinner_large .slds-spinner__dot-b:before,.slds-spinner_large.slds-spinner:after,.slds-spinner_large.slds-spinner:before{width:.625rem;height:.625rem}
.slds-spinner–large .slds-spinner__dot-a:before,.slds-spinner–large .slds-spinner__dot-b:before,.slds-spinner–large.slds-spinner:before,.slds-spinner_large .slds-spinner__dot-a:before,.slds-spinner_large .slds-spinner__dot-b:before,.slds-spinner_large.slds-spinner:before{animation-name:dotsBounceBefore-medium;top:-.3125rem;left:-.3125rem}
.slds-spinner–large .slds-spinner__dot-a:after,.slds-spinner–large .slds-spinner__dot-b:after,.slds-spinner–large.slds-spinner:after,.slds-spinner_large .slds-spinner__dot-a:after,.slds-spinner_large .slds-spinner__dot-b:after,.slds-spinner_large.slds-spinner:after{animation-name:dotsBounceAfter-medium;top:-.3125rem;right:-.3125rem}
@keyframes dotsBounceBefore-large {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(-.75rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateX(0)}
}
@keyframes dotsBounceAfter-large {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(.75rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateX(0)}
}
view raw spinner-styles.css hosted with ❤ by GitHub

Now let’s add our spinner to the CloudPage. We will create an HTML button and add an onclick event, so that the spinner activates once the user clicks the button:

<button onClick="showSpinner()">Try me!</button>
<div class="slds-spinner_container">
<div role="status" class="slds-spinner slds-spinner_large"><span class="slds-assistive-text"></span>
<div class="slds-spinner__dot-a"></div>
<div class="slds-spinner__dot-b"></div>
</div>
</div>
view raw spinner-html.html hosted with ❤ by GitHub

And finally, let’s add the script with the showSpinner() function:

<script>
function showSpinner() {
$spinner = document.getElementsByClassName("slds-spinner_container")[0];
$spinner.style.display = "block";
}
</script>
view raw spinner-js.html hosted with ❤ by GitHub

Above function will activate the spinner once the button is clicked and it will run indefinitely, which means that it’s appropriate for any use cases where the website gets reloaded or redirected to another one after all operations have been completed.

If you would like to control for how long the spinner is displayed, you can use the setTimeout method to deactivate the spinner after a given time. Below script will hide the spinner after 5 seconds:

<script>
function showSpinner() {
$spinner = document.getElementsByClassName("slds-spinner_container")[0];
$spinner.style.display = "block";
setTimeout(() => {
$spinner.style.display = "none";
}, 5000);
}
</script>

You can also control the activation/deactivation of the spinner by adding a hideSpinner() function to other functions used for performing operations. Below example will post data to another website using the fetch method and hide the spinner after the operation is complete:

<script>
function showSpinner() {
$spinner = document.getElementsByClassName("slds-spinner_container")[0];
$spinner.style.display = "block";
}
function hideSpinner() {
$spinner.style.display = "none";
}
...
function postData() {
fetch("http://example.com/myData.json&quot;, {
method: "POST",
headers: {
},
body: ({
})
}).then(function(res) {
hideSpinner(), window.alert("Success")
}).catch(function(err) {
hideSpinner(), window.alert("Error")
})
}
</script>
view raw hideSpinner.html hosted with ❤ by GitHub

To see the spinner in action, visit my demo CloudPage: https://pub.s10.exacttarget.com/c5luwxbwtxa.


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.

Create a CloudPages form with an image/file upload option

The HTML <input> tag specifies an input field where the user can enter data. An input field can vary in many ways, depending on the type attribute. The <input> elements with type="file" let the user choose one or more files from their device storage. The file can then be manipulated using JavaScript in order to process it further.

In this article, we will focus on creating a CloudPages form with an image input field, and uploading that image into Marketing Cloud’s Content Builder. The high-level outline of this process, which can be found in this Stack Exchange post, is: once the file has been uploaded via the form, it needs to be Base64 encoded, and then passed in a REST API call to the /asset/v1/content/assets route in order to create the image in Content Builder.

We will need to create two separate CloudPages: the first one will run client-side JavaScript in order to encode the file into a Base64 string and it will then pass it to the second CloudPage, which will run server-side JavaScript to make an API call to create a new asset in Content Builder.

I will break it down into smaller parts to make it easier to understand the different steps along the way.

Create an input field

Let’s start with creating an <input> field on a CloudPage. The input needs to be of the type="file" in order to allow the end-user to choose a file for upload from their local machine. We will also add an accept attribute, which specifies a filter for what file types the user can pick from the file input dialogue box. For our use case, it will be image/*: [see code snippet]

Please select a file to upload:
<br>
<input id="file" type="file" accept="image/*">
<br>
<button id="button">Upload</button>
view raw image-input.html hosted with ❤ by GitHub

We also need to include an id attribute in order to be able to reference the input field in JavaScript.

Encode the image using Base64

Base64 is a group of binary-to-text encoding schemes which enable storing and transferring data over media that are designed to deal with ASCII (text) only. In order to encode the image, we will use the FileReader.readAsDataURL() method to securely read the contents of a file stored on the user’s computer and return a result attribute containing a data: URL representing the file’s data. We will use the addEventListener() method to attach a click event to the Upload button, which will trigger the encoding function: [see code snippet]

document.getElementById('button').addEventListener('click', function() {
var files = document.getElementById('file').files;
if (files.length > 0) {
getBase64(files[0]);
}
});
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
console.log(reader.result);
};
reader.onerror = function (error) {
console.log('Error: ', error);
};
}

The format of the returned result is the following:

data:[<mediatype>][;base64],<data>

The mediatype is a MIME type string, such as ‘image/jpeg’ for a JPEG image file and <data> is the Base64 encoded string representing the uploaded file.

Fetch the data to processing page

We now need to pass the data onto our second CloudPage for further processing. The reason why we are using two pages instead of one is that client-side JavaScript is run after the server-side is complete, which makes it impossible to run them in reverse order on the same page.

In order to pass the data between our CloudPages, we will use the fetch() method. But first, let’s prepare the data that we will need for our API call: [see code snippet]

var fileEncoded = reader.result;
var base64enc = fileEncoded.split(";base64,")[1];
var fullFileName = document.getElementById("file").files[0].name;
var fileName = fullFileName.split(".")[0];
var assetName = fullFileName.split(".")[1];
  • base64enc is the encoded string which represents the image
  • fullFileName is the full name of the file uploaded by the user (eg. “astro.png”)
  • fileName is the first part of the file name, before the filename extension (eg. “astro”)
  • assetName is the filename extension (eg. “png”)

Once we have the above, we can POST it in JSON format to the processing page that will contain the server-side script. Our request will look like this: [see code snippet]

fetch("https://pub.s10.exacttarget.com/xxxxxxx&quot;, { //provide URL of the processing page
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
base64enc: base64enc,
fileName: fileName,
assetName: assetName
})
})
.then(function(res) {
window.alert("Success!");
})
.catch(function(err) {
window.alert("Error!");
});
view raw fetch-data.js hosted with ❤ by GitHub

where the method is POST, headers specify that the content-type is JSON and in the JSON body we will have three attribute-value pairs, base64enc, fileName and assetName.

The fetch() method argument is the path to the CloudPage containing our server-side script, so, for now, you can create an empty CloudPage just to obtain the link.

The fetch() method returns a promise that resolves to a response to that request, whether it is successful or not: .then(function(res) / .catch(function(err).

The full client-side script

Let’s now put all of the above elements together to create our first CloudPage. The complete code will look like this: [see code snippet]

Please select a file to upload:
<br>
<input id="file" type="file" accept="image/*">
<br>
<button id="button">Upload</button>
<script runat="client">
document.getElementById("button")
.addEventListener("click", function() {
var files = document.getElementById("file").files;
if (files.length > 0) {
getBase64(files[0]);
}
});
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
//prepare data to pass to processing page
var fileEncoded = reader.result;
var base64enc = fileEncoded.split(";base64,")[1];
var fullFileName = document.getElementById("file").files[0].name;
var fileName = fullFileName.split(".")[0];
var assetName = fullFileName.split(".")[1];
fetch("https://pub.s10.exacttarget.com/xxxxxxxxx&quot;, { //provide URL of the processing page
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
base64enc: base64enc,
fileName: fileName,
assetName: assetName
})
})
.then(function(res) {
window.alert("Success!");
})
.catch(function(err) {
window.alert("Error!");
});
};
reader.onerror = function(error) {
console.log('Error: ', error);
};
}
</script>

Now we can move on and prepare the second part of the script.

Retrieve the posted data

In order to retrieve the data posted from our first CloudPage, we will use the getPostData() server-side JavaScript method, which sadly is not documented in the official Server-Side JavaScript Syntax Guide, but you can read about it in this post on Salesforce Stack Exchange. Once we access the body of our request, we can parse the data using the ParseJSON() function: [see code snippet]

//retrieve posted data
var jsonData = Platform.Request.GetPostData();
var obj = Platform.Function.ParseJSON(jsonData);
view raw get-posted-data.js hosted with ❤ by GitHub

Prepare the data for API call

Now we can prepare the data for our API call. Let’s take a look at an example JSON body of a create asset request: [see code snippet]

POST /asset/v1/content/assets
{
"name": "Astro",
"assetType": {
"name": "png",
"id": 28
},
"file": "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"
}

In the above, the first name represents the name of the file. The second parameter assetType, consists of a name which represents the file extension and an id of the asset type. Asset types are the file or content types supported by Content Builder and the full list can be found here: List of Asset Types.

Let’s prepare the data and match the asset type based on the name of the file extension: [see code snippet]

//prepare data for API call
var base64enc = obj.base64enc;
var fileName = obj.fileName;
var assetName = obj.assetName;
//match asset type with uploaded file (https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/base-asset-types.htm)
var assetTypes = { ai: 16, psd: 17, pdd: 18, eps: 19, gif: 20, jpe: 21, jpeg: 22, jpg: 23, jp2: 24, jpx: 25, pict: 26, pct: 27, png: 28, tif: 29, tiff: 30, tga: 31, bmp: 32, wmf: 33, vsd: 34, pnm: 35, pgm: 36, pbm: 37, ppm: 38, svg: 39, "3fr": 40, ari: 41, arw: 42, bay: 43, cap: 44, crw: 45, cr2: 46, dcr: 47, dcs: 48, dng: 49, drf: 50, eip: 51, erf: 52, fff: 53, iiq: 54, k25: 55, kdc: 56, mef: 57, mos: 58, mrw: 59, nef: 60, nrw: 61, orf: 62, pef: 63, ptx: 64, pxn: 65, raf: 66, raw: 67, rw2: 68, rwl: 69, rwz: 70, srf: 71, sr2: 72, srw: 73, x3f: 74, "3gp": 75, "3gpp": 76, "3g2": 77, "3gp2": 78, asf: 79, avi: 80, m2ts: 81, mts: 82, dif: 83, dv: 84, mkv: 85, mpg: 86, f4v: 87, flv: 88, mjpg: 89, mjpeg: 90, mxf: 91, mpeg: 92, mp4: 93, m4v: 94, mp4v: 95, mov: 96, swf: 97, wmv: 98, rm: 99, ogv: 100, indd: 101, indt: 102, incx: 103, wwcx: 104, doc: 105, docx: 106, dot: 107, dotx: 108, mdb: 109, mpp: 110, ics: 111, xls: 112, xlsx: 113, xlk: 114, xlsm: 115, xlt: 116, xltm: 117, csv: 118, tsv: 119, tab: 120, pps: 121, ppsx: 122, ppt: 123, pptx: 124, pot: 125, thmx: 126, pdf: 127, ps: 128, qxd: 129, rtf: 130, sxc: 131, sxi: 132, sxw: 133, odt: 134, ods: 135, ots: 136, odp: 137, otp: 138, epub: 139, dvi: 140, key: 141, keynote: 142, pez: 143, aac: 144, m4a: 145, au: 146, aif: 147, aiff: 148, aifc: 149, mp3: 150, wav: 151, wma: 152, midi: 153, oga: 154, ogg: 155, ra: 156, vox: 157, voc: 158, "7z": 159, arj: 160, bz2: 161, cab: 162, gz: 163, gzip: 164, iso: 165, lha: 166, sit: 167, tgz: 168, jar: 169, rar: 170, tar: 171, zip: 172, gpg: 173, htm: 174, html: 175, xhtml: 176, xht: 177, css: 178, less: 179, sass: 180, js: 181, json: 182, atom: 183, rss: 184, xml: 185, xsl: 186, xslt: 187, md: 188, markdown: 189, as: 190, fla: 191, eml: 192, text: 193, txt: 194, freeformblock: 195, textblock: 196, htmlblock: 197, textplusimageblock: 198, imageblock: 199, abtestblock: 200, dynamicblock: 201, stylingblock: 202, einsteincontentblock: 203, webpage: 205, webtemplate: 206, templatebasedemail: 207, htmlemail: 208, textonlyemail: 209, socialshareblock: 210, socialfollowblock: 211, buttonblock: 212, layoutblock: 213, defaulttemplate: 214, smartcaptureblock: 215, smartcaptureformfieldblock: 216, smartcapturesubmitoptionsblock: 217, slotpropertiesblock: 218, externalcontentblock: 219, codesnippetblock: 220, rssfeedblock: 221, formstylingblock: 222, referenceblock: 223, imagecarouselblock: 224, customblock: 225, liveimageblock: 226, livesettingblock: 227, contentmap: 228, jsonmessage: 230 };
var assetTypeID = assetTypes[assetName];

If you’re only going to upload images, you don’t need to include the whole list of asset types – the ones that start with “2” will be sufficient.

Create an asset using REST API

In order to make an API call using the /asset/v1/content/assets route, we first need to authenticate. You will need the Installed Package for API integration, the Client Id and Cient Secret. If you’re not sure how to authenticate programmatically in Salesforce Marketing Cloud, refer to my article, Salesforce Marketing Cloud API Authentication using Server-Side JavaScript.

As a security measure, it’s best to store your Client Id and Secret as encoded values in a Data Extension to avoid exposing them in your script. My preferred way is to use the EncryptSymmetric and DecryptSymmetric AMPscript functions for encryption, and a simple lookup to get their values from a Data Extension.

Here’s the code snippet for the authentication part:

<script runat="server">
Platform.Load("Core","1.1.1");
var authEndpoint = 'https://mcxxxxxxxxxxxxxxxxxxxxxxxxx.auth.marketingcloudapis.com&#39;;
var payload = {
client_id: "insert Client Id",
client_secret: "insert Client Secret",
grant_type: "client_credentials"
};
var url = authEndpoint + '/v2/token';
var contentType = 'application/json';
try {
var accessTokenRequest = HTTP.Post(url, contentType, Stringify(payload));
if(accessTokenRequest.StatusCode == 200) {
var tokenResponse = Platform.Function.ParseJSON(accessTokenRequest.Response[0]);
var accessToken = tokenResponse.access_token;
}
} catch (error) {
Write(Stringify(error));
}
</script>

Once we obtain the accessToken, we will need to include it in our final API call, along with the rest_instance_url: [see code snippet]

var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var jsonBody = {
"name": fileName,
"assetType": {
"name": assetName,
"id": assetTypeID
},
"file": base64enc
};
var requestUrl = rest_instance_url + "asset/v1/content/assets"
var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);

The above call will create an image in Content Builder’s main folder and return the following response: [see code snippet]

{
"id":xxx,
"customerKey":"xxx",
"objectID":"xxx",
"assetType":{
"id":23,
"name":"jpg",
"displayName":"Image"
},
"fileProperties":{
"extension":"jpg",
"fileSize":24328,
"fileCreatedDate":"xxx",
"width":700,
"height":421,
"publishedURL":"xxxx"
},
"name":"astro",
"owner":{
"id":xxx,
"email":"",
"name":"APIuser",
"userId":"xxx"
},
"createdDate":"xxx",
"createdBy":{
"id":xxx,
"email":"",
"name":"APIuser",
"userId":"xxx"
},
"modifiedDate":"xxx",
"modifiedBy":{
"id":xxx,
"email":"",
"name":"APIuser",
"userId":"xxx"
},
"enterpriseId":xxx,
"memberId":xxx,
"status":{
"id":2,
"name":"Published"
},
"category":{
"id":xxx8,
"name":"Content Builder",
"parentId":0
},
"availableViews":[
],
"modelVersion":2
}

The full server-side script

Here is the full script for our second CloudPage: [see code snippet]

<script runat="server">
Platform.Load("Core","1");
try {
//fetch posted data
var jsonData = Platform.Request.GetPostData();
var obj = Platform.Function.ParseJSON(jsonData);
//prepare data for API call
var base64enc = obj.base64enc;
var fileName = obj.fileName;
var assetName = obj.assetName;
//match asset type with uploaded file (https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/base-asset-types.htm)
var assetTypes = { ai: 16, psd: 17, pdd: 18, eps: 19, gif: 20, jpe: 21, jpeg: 22, jpg: 23, jp2: 24, jpx: 25, pict: 26, pct: 27, png: 28, tif: 29, tiff: 30, tga: 31, bmp: 32, wmf: 33, vsd: 34, pnm: 35, pgm: 36, pbm: 37, ppm: 38, svg: 39, "3fr": 40, ari: 41, arw: 42, bay: 43, cap: 44, crw: 45, cr2: 46, dcr: 47, dcs: 48, dng: 49, drf: 50, eip: 51, erf: 52, fff: 53, iiq: 54, k25: 55, kdc: 56, mef: 57, mos: 58, mrw: 59, nef: 60, nrw: 61, orf: 62, pef: 63, ptx: 64, pxn: 65, raf: 66, raw: 67, rw2: 68, rwl: 69, rwz: 70, srf: 71, sr2: 72, srw: 73, x3f: 74, "3gp": 75, "3gpp": 76, "3g2": 77, "3gp2": 78, asf: 79, avi: 80, m2ts: 81, mts: 82, dif: 83, dv: 84, mkv: 85, mpg: 86, f4v: 87, flv: 88, mjpg: 89, mjpeg: 90, mxf: 91, mpeg: 92, mp4: 93, m4v: 94, mp4v: 95, mov: 96, swf: 97, wmv: 98, rm: 99, ogv: 100, indd: 101, indt: 102, incx: 103, wwcx: 104, doc: 105, docx: 106, dot: 107, dotx: 108, mdb: 109, mpp: 110, ics: 111, xls: 112, xlsx: 113, xlk: 114, xlsm: 115, xlt: 116, xltm: 117, csv: 118, tsv: 119, tab: 120, pps: 121, ppsx: 122, ppt: 123, pptx: 124, pot: 125, thmx: 126, pdf: 127, ps: 128, qxd: 129, rtf: 130, sxc: 131, sxi: 132, sxw: 133, odt: 134, ods: 135, ots: 136, odp: 137, otp: 138, epub: 139, dvi: 140, key: 141, keynote: 142, pez: 143, aac: 144, m4a: 145, au: 146, aif: 147, aiff: 148, aifc: 149, mp3: 150, wav: 151, wma: 152, midi: 153, oga: 154, ogg: 155, ra: 156, vox: 157, voc: 158, "7z": 159, arj: 160, bz2: 161, cab: 162, gz: 163, gzip: 164, iso: 165, lha: 166, sit: 167, tgz: 168, jar: 169, rar: 170, tar: 171, zip: 172, gpg: 173, htm: 174, html: 175, xhtml: 176, xht: 177, css: 178, less: 179, sass: 180, js: 181, json: 182, atom: 183, rss: 184, xml: 185, xsl: 186, xslt: 187, md: 188, markdown: 189, as: 190, fla: 191, eml: 192, text: 193, txt: 194, freeformblock: 195, textblock: 196, htmlblock: 197, textplusimageblock: 198, imageblock: 199, abtestblock: 200, dynamicblock: 201, stylingblock: 202, einsteincontentblock: 203, webpage: 205, webtemplate: 206, templatebasedemail: 207, htmlemail: 208, textonlyemail: 209, socialshareblock: 210, socialfollowblock: 211, buttonblock: 212, layoutblock: 213, defaulttemplate: 214, smartcaptureblock: 215, smartcaptureformfieldblock: 216, smartcapturesubmitoptionsblock: 217, slotpropertiesblock: 218, externalcontentblock: 219, codesnippetblock: 220, rssfeedblock: 221, formstylingblock: 222, referenceblock: 223, imagecarouselblock: 224, customblock: 225, liveimageblock: 226, livesettingblock: 227, contentmap: 228, jsonmessage: 230 };
var assetTypeID = assetTypes[assetName];
//authenticate to get access token
var authEndpoint = 'https://xxxxxxxx.auth.marketingcloudapis.com/&#39; //add authentication endpoint
var payload = {
client_id: "xxxxx", //pass Client ID
client_secret: "xxxxx", //pass Client Secret
grant_type: "client_credentials"
};
var url = authEndpoint + '/v2/token'
var contentType = 'application/json'
var accessTokenRequest = HTTP.Post(url, contentType, Stringify(payload));
if (accessTokenRequest.StatusCode == 200) {
var tokenResponse = Platform.Function.ParseJSON(accessTokenRequest.Response[0]);
var accessToken = tokenResponse.access_token
var rest_instance_url = tokenResponse.rest_instance_url
}
//make api call to create asset
if (base64enc != null) {
var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var jsonBody = {
"name": fileName,
"assetType": {
"name": assetName,
"id": assetTypeID
},
"file": base64enc
};
var requestUrl = rest_instance_url + "asset/v1/content/assets"
var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);
}
} catch (error) {
Write("<br>error: " + Stringify(error));
}
</script>

You can now go back to your first CloudPage and start uploading!

To see this script in action, visit my CloudPage.


Questions? Comments?

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

Find a Data Extension and it’s folder path using SSJS

If you work with a lot of Data Extensions in multiple folders, you know the pain of using the built-in search feature. It only searches the folder that you’re in, with the exclusion of any sub-folders, which is simply impractical.

Below, I would like to show you how to find a Data Extension and it’s path using Server-Side JavaScript: which functions to use and how to iterate through them.

If you’re not interested in the technical part and want to jump straight into building the search app, click here.

SSJS Data Extension and Folder Functions

In order to find a Data Extension, we will use two SSJS Core functions: DataExtension.Retrieve and Folder.Retrieve. The first function will help us identify the Data Extension, either by its name or the External Key. The second function retrieves an array of folders based on the specified criteria. It also retrieves details regarding the parent folder of a given folder if one exists, and that’s what we will use to build the Data Extension path.

<script runat="server">
Platform.Load("core","1.1.5");
var DEprop = //use either "Name" or "Customer Key"
var DEval = //provide either the Name or External Key
var FindDE = DataExtension.Retrieve({Property:DEprop,SimpleOperator:"equals",Value:DEval});
var FolderID = FindDE[0].CategoryID;
var DEname = FindDE[0].Name;
var list = [];
list.push(DEname);
var path = function(id) {
if (id> 0) {
var results = Folder.Retrieve({Property:"ID",SimpleOperator:"equals",Value:id});
list.unshift(results [0].Name);
return path(results[0].ParentFolder.ID);
} else {
return id;
}
};
path(FolderID);
Write(list.join("> "));
</script>
view raw ssjs-find-de-path.js hosted with ❤ by GitHub

In the above script, you will need to define two variables, DEprop and DEval, depending on whether you have the name or the External Key of the Data Extension. You can paste the script on a CloudPage, define the two variables, and once you click on the “Publish” button, the script will be executed and results will be visible on-screen, without the need to actually publish the CloudPage:

Create a simple Data Extension search app

If users of your instance of Salesforce Marketing Cloud often struggle with finding Data Extensions, you can create an app on a CloudPage to help them navigate the folders. By adding a simple form, you can enable the users to perform a search from a CloudPage:

Paste the below code onto a CloudPage and publish it. Anyone with access to the link will be able to search for Data Extensions located in the Business Unit where the CloudPage was created. If the CloudPage has been created in the Parent Business Unit, they will also be able to search the Shared Data Extensions folder.

<table style="padding: 20px;"><tr><td>
<b>How would you like to identify the Data Extension?</b><br>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<select name="DEprop">
<option value="Name">Name</option>
<option value="CustomerKey">External Key</option>
</select>
equals
<input type="text" name="DEval" value="" maxlength="128"><br>
<input type="submit" value="Submit">
</form><br><b>Folder path: </b>
<script runat="server">
Platform.Load("core","1.1.5");
var DEprop = Request.GetQueryStringParameter("DEprop");
var DEval = Request.GetQueryStringParameter("DEval");
var FindDE = DataExtension.Retrieve({Property:DEprop,SimpleOperator:"equals",Value:DEval});
var FolderID = FindDE[0].CategoryID;
var DEname = FindDE[0].Name;
var list = [];
list.push(DEname);
var path = function(id) {
if (id> 0) {
var results = Folder.Retrieve({Property:"ID",SimpleOperator:"equals",Value:id});
list.unshift(results [0].Name);
return path(results[0].ParentFolder.ID);
} else {
return id;
}
};
path(FolderID);
Write(list.join("> "));
</script>
</td></tr></table>

Last but not least, always remember to put security measures into practice when setting up this kind of functionality on a CloudPage, to prevent your data from being exposed in a breach.

PS. There is a free tool available called SFMC Object Finder. It’s a Chrome extension that lets you search for a variety of objects, like images, data extensions and other assets, in your instance of Salesforce Marketing Cloud. All it requires is that you have an active session of Salesforce Marketing Cloud open in your Chrome browser. However, if you cannot install this extension or need to be able to find Data Extensions programmatically, you can use Server-Side JavaScript to achieve it!

Create a SmartCapture form prefilled (prepopulated) with Salesforce data

Smart Capture allows you to easily create forms on your Cloud Pages to collect information about your subscribers and then use that data for future sends and campaigns. While creating a Smart Capture form doesn’t require any coding skills and can be done using only the drag-and-drop editor, adding a simple script to your form can significantly increase the user experience. Forms prepopulated with data can be used for any type of campaign, as long as you already have some data about your subscriber in your database. Subscribers are more likely to register for an event or a raffle when they don’t need to manually fill in all the form fields, and the only thing they have to do is to submit the form.

Create a SmartCapture form

The first thing you will have to do is to create a Smart Capture form on your Cloud Page. Depending on your requirements, you can either create your own Data Extension to collect data submitted by your subscribers or use the system Data Extension called CloudPages_DataExtension.

For the purpose of this tutorial, let’s use the system data extension and create a very simple form with three fields: First Name, Last Name and Email Address.

SmartCapture form on a CloudPage

Add AMPscript to retrieve Salesforce data

In this example, we will be retrieving Sales Cloud data to prepopulate the form using the RetrieveSalesforceObjects function. We will be looking up Contacts in Sales Cloud based on the Subscriber Key. Add the following code in a separate content area, above your Smart Capture form:

%%[
VAR @subscriberkey, @email, @firstname, @lastname
SET @subscriberkey = _subscriberkey
IF NOT EMPTY(@subscriberkey) THEN
SET @rs= RetrieveSalesforceObjects("Contact", "Id, FirstName,LastName,Email", "Id", "=", @subscriberkey)
SET @rowCount = rowcount(@rs)
IF @rowCount > 0 THEN
SET @row = row(@rs,1)
SET @email = field(@row,"Email")
SET @firstname = field(@row,"FirstName")
SET @lastname = field(@row,"LastName")
ENDIF
ELSE
SET @email = emailaddr
SET @firstname = ""
SET @lastname = ""
ENDIF
]%%

If you’re using Campaigns in Sales Cloud, which can admit both Contacts and Leads, you can extend your code with a Substring function, so that it’s able to correctly process both:

%%[
VAR @subscriberkey, @email, @firstname, @lastname
SET @subscriberkey = _subscriberkey
IF NOT EMPTY(@subscriberkey) THEN
IF Substring(@subscriberkey, 1,3) == "00Q" THEN
/* Lead */
SET @rs= RetrieveSalesforceObjects("Lead", "Id, FirstName,LastName,Email", "Id", "=", @subscriberkey)
SET @rowCount = rowcount(@rs)
IF @rowCount > 0 THEN
SET @row = row(@rs,1)
SET @email = field(@row,"Email")
SET @firstname = field(@row,"FirstName")
SET @lastname = field(@row,"LastName")
ENDIF
ELSE
/* Contact */
SET @rs= RetrieveSalesforceObjects("Contact", "Id, FirstName,LastName,Email", "Id", "=", @subscriberkey)
SET @rowCount = rowcount(@rs)
IF @rowCount > 0 THEN
SET @row = row(@rs,1)
SET @email = field(@row,"Email")
SET @firstname = field(@row,"FirstName")
SET @lastname = field(@row,"LastName")
ENDIF
ENDIF
ELSE
SET @email = emailaddr
SET @firstname = ""
SET @lastname = ""
ENDIF
]%%

You can also use data stored in Marketing Cloud to prefill the form – just remember, that you will need to use the LookupRows function instead of RetrieveSalesforceObjects.

Prepopulate the data in the form

In order to add the data retrieved from Salesforce to your form, you will have to modify each of the form input fields separately. Click once on the Smart Capture form, and then once again on the field you wish to modify.

Now edit the field in the HTML view and add an inline display of the adequate field retrieved from Sales Cloud as the default value at the end of the HTML input tag:

<input type="text" name="FirstName"
data-field-type="Text" value="%%=v(@firstname)=%%">

Do this for all the fields that should be prepopulated with data. Publish your CloudPage and test it – the fastest way to test the form is to create an email with a button leading to your CloudPage and previewing the email against a subscriber, who is present in your Salesforce database.

You can also connect this form to a journey in Journey Builder to send a confirmation email to anyone who filled it in, update their Salesforce data or add them to a consecutive campaign.

Read more here about Using a Smart Capture Form as a Journey Builder Entry Event.