This article showcases how to create and automate Maintenance Mode for Cloud pages in Salesforce Marketing Cloud.
What’s Maintenance Mode?
In a nutshell, Maintenance Mode is a process that prevents customers from accessing web pages when a scheduled maintenance is taking place.
How does this apply to Marketing Cloud?
For the purpose of this example, consider a Cloud page with a form that creates leads directly in Salesforce.
What happens to the form when Salesforce is down for maintenance?
Since the form doesn’t work without Salesforce, the best practice is to redirect the customer to a maintenance page until the maintenance is over and Salesforce is up and running again.
This is easily achieved with a single line of code that we manually add to and remove from the Cloud page:
%%=Redirect("https://www.somedomain.com/maintenance-page")=%%
But how do we go about it when instead of a single page, we have dozens or hundreds of Cloud pages affected by the maintenance?
Let’s find out!
Create and automate
Before we start creating Maintenance Mode for Marketing Cloud, we need to consider that Maintenance is a process that usually happens on a scheduled basis.
And something that happens on a scheduled basis can and should be automated!
But don’t get too excited, as the Maintenance schedules are rarely set in stone (ref. Preferred Salesforce Maintenance Schedule).
We therefore need to come up with a method that also allows us to quickly modify the schedule at any point in time.
How it works
The method is simple and straightforward:
- We create a Data Extension and store the start dates and the end dates of all the scheduled maintenances.
- We retrieve the dates from the Data Extension in the beginning of every Cloud page.
- We loop through every scheduled maintenance record and if the current date is between the start date and the end date, we redirect the customer to the maintenance page.
Data Extension
Let’s call our Data Extension “MaintenanceSchedule” and create the following fields.
Name | FieldType | IsPrimaryKey | IsRequired | MaxLength |
Id | Number | true | true | 50 |
Name | Text | false | false | 250 |
Active | Boolean | false | false | |
Start | Date | false | false | |
End | Date | false | false |
Cloud page
It doesn’t matter where the code is located on the Cloud page, as the redirect is performed on the server level before the HTML is rendered.
%%[
/* Maintenance Redirect */
VAR @Timestamp, @Start, @End, @MaintenanceRedirect
SET @MaintenanceDE = 'MaintenanceSchedule'
SET @MaintenanceRedirect = "https://www.mydomain.com/maintenance-page"
SET @Timestamp = Format(SystemDateToLocalDate(NOW()), "yyyyMMddHHmm")
SET @Rows = LookupRows(@MaintenanceDE,'Active','True')
IF ROWCOUNT(@Rows) > 0 THEN
FOR @i = 1 TO ROWCOUNT(@Rows) DO
SET @Start = Format(Field(Row(@Rows,@i), 'Start'), "yyyyMMddHHmm")
SET @End = Format(Field(Row(@Rows,@i), 'End'), "yyyyMMddHHmm")
IF @Timestamp > @Start AND @Timestamp < @End THEN
Redirect(concat(@MaintenanceRedirect))
ENDIF
NEXT @i
ENDIF
]%%
Note that the date comparison is achieved by converting the date into a timestamp, which is just one of the ways we can compare dates in AMPscript.
That’s it! No fuss, no muss!
Not so fast!
Even though the method is simple, in practice, managing this setup is a bit of a challenge for 2 reasons:
- Marketing Cloud doesn’t allow us to enter the exact hours and minutes in a Date field of a Data Extension.
- Inserting 27 lines of code in the beginning of every Cloud page is a hassle and can’t be easily maintained across all the pages at once.
Let’s address every issue separately.
How to insert hours and minutes in a Date field
There are 2 possible solutions to this problem:
- We create a CSV file with all the records and import them into the Data Extension.
- We use code (SSJS or AMPscript) to create records in the Data Extension.
The first solution is self-explanatory; as for the second, here is how it can be achieved using AMPscript:
%%[
InsertData(
"MaintenanceSchedule",
"Id", "1",
"Name", "Salesforce Maintenance #1",
"Active", "True",
"Start", "01/21/2022 12:30:00 AM",
"End", "01/21/2022 08:30:00 PM"
)
]%%
Or using server-side JavaScript:
<script runat='server'>
Platform.Load('core', '1');
var de = DataExtension.Init("MaintenanceSchedule");
var result = de.Rows.Add({
"Id": "1",
"Name": "Salesforce Maintenance #1",
"Active": "True",
"Start": "01/21/2022 12:30:00 AM",
"End": "01/21/2022 08:30:00 PM"
});
</script>
Note that for both languages, the format of the date should be one of the supported Marketing Cloud date formats.
How to insert and maintain Maintenance Mode code in Cloud pages
As previously stated, inserting the full code in the beginning of every Cloud page is out of the question.
Then how do we insert the same code in all the pages and modify them all at once?
The solution is simple: we insert our code in an HTML content block in the Content Builder and we use AMPscript to request the code on every Cloud page!
All we need is the ID of the content block:
%%=ContentBlockbyId("123456")=%%
Going one step further
Even though we now have a Data Extension that holds the schedule and the Maintenance Mode code implemented in every Cloud page, the setup is still difficult to manage, especially if multiple Business Units are involved.
What if we could come up with a method that creates the schedules for all the Business Units in a matter of seconds and that can be used to add additional schedule slots in the simplest way possible?
There is such a way and it only requires a chunk of server-side JavaScript code to be executed form the Parent Business Unit!
How it works
When the following code is executed, it creates a MaintenanceSchedule Data Extension on each Business Unit and populates it with a predefined schedule.
All we need is to provide a structured data to the script, where each schedule is associated to one or multiple Business Units, or to all of them if nothing is specified.
{
de: 'MaintenanceSchedule',
schedule: [
{
Name: 'Event #1',
Start: '09/01/2021 23:00',
End: '10/01/2021 08:00'
},
{
Name: 'Event #2',
Start: '20/03/2021 23:00',
End: '21/03/2021 08:00',
BU: ['BU1','BU2']
},
{
Name: 'Event #3',
Start: '24/04/2021 23:00',
End: '25/04/2021 08:00',
BU: ['BU3']
}
],
bus: {
'BU1': '000000001',
'BU2': '000000002',
'BU3': '000000003'
}
}
Note that this example uses the European date format (dd/MM/YYYY hh:mm). Please adapt the code accordingly in the formatDate function.
But what happens if the Data Extension already exists?
In this case, there are 2 possible ways of achieving this:
- We delete all the Data Extensions first and then re-create them again.
- We clear the Data Extension if it exists and we create it if it doesn’t exist yet.
This behavior is defined by the cleanup variable in the beginning of the code.
var cleanup = false;
Full code
<script runat='server'>
Platform.Load('core', '1');
var api = new Script.Util.WSProxy();
var guid = String(Platform.Function.GUID()).toUpperCase();
var cleanup = false;
//////////////////////////////////////////////
///// SETUP
//////////////////////////////////////////////
var setup = {
de: 'MaintenanceSchedule',
schedule: [
{
Name: 'Event #1',
Start: '09/01/2021 23:00',
End: '10/01/2021 08:00'
},
{
Name: 'Event #2',
Start: '20/03/2021 23:00',
End: '21/03/2021 08:00',
BU: ['BU1','BU2']
},
{
Name: 'Event #3',
Start: '24/04/2021 23:00',
End: '25/04/2021 08:00',
BU: ['BU3']
}
],
bus: {
'BU1': '000000001',
'BU2': '000000002',
'BU3': '000000003'
}
}
try {
// Loop through every Business Unit
for(var b in setup.bus) {
var client = b;
var bu = setup.bus[b];
api.resetClientIds();
api.setClientId({'ID': bu}); // Switch to the Business Unit
// Filter out the schedule according to the current Business Unit
var schedule = Platform.Function.ParseJSON(Stringify(setup.schedule));
var records = []
for(var k in schedule) {
var item = schedule[k];
if(item.BU == null || inArray(item.BU, client)) records.push(item);
}
// Format records to match the fields in the Data Extension
var records = formatRecords(records, client);
// Create a batch for data insertion
var batch = createBatchData(setup.de, records);
if (batch.length > 0) {
// Do we clear/create or destroy and re-create the Data Extension?
if(!cleanup) {
var clean = clearOrCreateDataExtension(setup, client);
} else {
var del = deleteDataExtension(setup, client);
var clean = clearOrCreateDataExtension(setup, client);
}
// Create records in the Data Extension
var createREC = api.createBatch('DataExtensionObject', batch);
if (createREC.Status == 'OK') {
Write('(+) New records were created in ' + setup.de + ' in ' + client + '<br><br>');
} else {
Write('(-) No records were created in ' + setup.de + ' in ' + client + '.<br><br> Results: ' + Stringify(createREC.Results) + '<br><br>');
}
}
}
} catch(err) {
Write(Stringify(err) + '<br><br>');
}
function deleteDataExtension(setup, client) {
/*
This function completely deletes a Data Extension
when its name and a Business Unit are provided.
*/
var req = api.retrieve("DataExtension", ["ObjectID", "CustomerKey"], {
Property: "DataExtension.Name",
SimpleOperator: "equals",
Value: setup.de
});
var objectId = req.Results[0].ObjectID;
var customerKey = req.Results[0].CustomerKey;
if(objectId != null) {
var deleteDE = api.deleteItem("DataExtension", { "ObjectID": objectId });
if(deleteDE.Status == 'OK') {
Write('(!) ' + setup.de + ' was deleted in ' + client + '<br><br>');
} else {
Write('(-) ' + setup.de + ' wasn\'t deleted in ' + client + '.<br><br> Results: ' + Stringify(createREC.Results) + '<br><br>');
}
} else {
Write('(-) No DataExtension by the name of ' + setup.de + ' was found in ' + client + '.<br><br>');
}
return deleteDE;
}
function clearOrCreateDataExtension(setup, client) {
/*
This function removes the records from a Data Extension
or creates a new Data Extension when it's not found
based on its name and a Business Unit.
*/
var req = api.retrieve("DataExtension", ["ObjectID", "CustomerKey"], {
Property: "DataExtension.Name",
SimpleOperator: "equals",
Value: setup.de
});
var objectId = req.Results[0].ObjectID;
var customerKey = req.Results[0].CustomerKey;
if (objectId != null) {
// Clear the records
var action = 'cleared';
var properties = {
CustomerKey: customerKey
};
var createDE = api.performItem("DataExtension", properties, 'ClearData', {});
if (createDE.Status == 'OK') {
Write('(!) ' + setup.de + ' was cleared in ' + client + '<br><br>');
} else {
Write('(-) ' + setup.de + ' wasn\'t cleared in ' + client + '.<br><br> Results: ' + Stringify(createDE.Results) + '<br><br>');
}
} else {
// Create the Data Extension
var action = 'created';
var fields = [
{
'Name': 'Id',
'FieldType': 'Number',
'IsPrimaryKey': true,
'IsRequired': true
},
{
'Name': 'Name',
'FieldType': 'Text',
'MaxLength': 254
},
{
'Name': 'Active',
'FieldType': 'Boolean',
'DefaultValue': true
},
{
'Name': 'Start',
'FieldType': 'Date'
},
{
'Name': 'End',
'FieldType': 'Date'
}
];
var config = {
'CustomerKey': setup.de,
'Name': setup.de,
'CategoryID': 0,
'Fields': fields
};
var createDE = api.createItem('DataExtension', config);
if (createDE.Status == 'OK') {
Write('(!) ' + setup.de + ' was created in ' + client + '<br><br>');
} else {
Write('(-) ' + setup.de + ' wasn\'t created in ' + client + '.<br><br> Results: ' + Stringify(createDE.Results) + '<br><br>');
}
}
return createDE;
}
function formatRecords(records, client) {
/*
This function formats the data before inserting it
in the Data Extension
*/
var records = Platform.Function.ParseJSON(Stringify(records));
var results = [];
for(var k in records) {
var item = records[k];
item.Id = k;
item.Start = formatDate(item.Start);
item.End = formatDate(item.End);
if(item.BU == null || !!inArray(item.BU, client)) {
delete item.BU;
results.push(item);
}
}
return results;
}
function inArray(arr, key) {
/*
This function checks if a value exist in the Array object.
*/
var is = false;
for(var i = 0; i < arr.length; i++) {
var item = arr[i];
if(item == key) is = true;
}
return is;
}
function formatDate(dt) {
/*
This function formats the date from dd/MM/YYYY hh:mm
to YYYY-MM-ddThh:mm
*/
var date = dt.split(' ')[0].split('/');
var time = dt.split(' ')[1];
var dateString = [
date[2],
date[1],
date[0]
].join('-');
return dateString + "T" + time;
}
function createBatchData(key, records) {
/*
This function creates batches for an efficient
data insertion in a Data Extension.
*/
var batch = [];
for (var k in records) {
batch.push({
CustomerKey: key,
Properties: wsPack(records[k])
});
}
return batch;
}
function wsPack(obj) {
/*
This function splits the JSON in sub-objects for
a successful WSProxy data insert.
*/
var out = [];
for (k in obj) {
out.push({
Name: k,
Value: obj[k]
});
}
return out;
}
</script>
Conclusion
In the end, why does it matter to have an automated Maintenance Mode in Marketing Cloud?
Because we are not in the business of losing leads!
Today, the user experience and customer retention matter more than ever for the success of our marketing campaigns.
So let’s make the most of it!
Have I missed anything?
Please poke me with a sharp comment below or use the contact form.
To get better clarity on this, if SFMC itself is down, how can read data from Data extension, where can we execute SSJS code to display maintainance page, could you please explain more details on this.
in this case, we’re screwed 😀