Insights

Enhancing Web Experiences with Sitecore Personalize and Real-time Activity Decision Models

From concept to reality

The Personalization Problem

How do we personalize a user’s web experience based upon real-time activity? To spell it out more plainly. How do we show a banner to a user that depends upon the the results of a user logging in, that happened on the previous page?

In the beginning I thought this was a simple process of supplying extension data to a VIEW event and then segmenting that user based upon said data. That would’ve been easy, except that users are not immediately added to Segments that you can then pull. And what happens if the data returned from the login changes? The Segment the user is on may have to change thus changing their personalization.

Decision Models and Decision Tables to the rescue. As I don’t get to play with CDP that much and Personalize even less, it’s easy to forget some of these extremely powerful features exist. And this is where I have to call out Freddy Rueda Barros for sharing his article, How to get Sitecore Personalize Decision Model result on my Sitecore Backend code. It gave me a solid starting point for building what I needed.

So he’s what we’re going to cover.

Supplying a VIEW / IDENTITY Event With a Data Extension Create a Decision Model to Grab Recent Session Data Create a Web Experience Using Decision Model API Output

Supplying a VIEW / IDENTITY Event With a Data Extension

While it might not always be required, depending on your needs, leveraging a data extension within your Personalization, whether it be for display or calculation, can truly open up the doors of possibilities.

If we look back at the previous article on triggering events, Trigger a CDP Event With Custom Extension Data Using Sitecore Engage SDK, we can see how custom events and utilizing the data extension can be useful. You’ll also remember seeing seeing the data extension in the code below:

const addToCart = async (burger: Burger) => {
    setCartItems((prevItems) => {
      const existingItem = prevItems.find(item => item.id === burger.id);
      if (existingItem) {
        return prevItems.map(item =>
          item.id === burger.id ? { ...item, quantity: (item.quantity ?? 1) + 1 } : item
        );
      } else {
        return [...prevItems, { ...burger, quantity: 1 }];
      }
    });

      const extensionData = {
        cartId: burger.id,
        cartName: burger.name
      };
      await engage?.event("mythicalburger:ADD_TO_CART", eventData, extensionData);
  };

Specifically, the bit of code referencing extensionData.

const extensionData = {
  cartId: burger.id,
  cartName: burger.name
};

It’s a key value pair object. This time around we’re going to extend that data a bit to include additional features.

const extensionData = {
  cartId: burger.id,
  cartName: burger.name,
  isChicken: burger.isChicken,
  isBeef: burger.isBeef,
  isVegan: burger.isVegan
};

We’re going to obtain that information by extending our Burger type object.

interface Burger {
  id: string; // Unique identifier for each burger
  name: string; // Name of the burger
  price: number; // Price of the burger
  image: string; // Image URL of the burger
  quantity?: number; // Quantity of the burger in the cart
  isChicken: boolean; // is the menu item made of chicken
  isBeef: boolean; // is the menu item made of beef
  isVegan: boolean; // is the menu item vegan
  isDrink: boolean; // is the menu item a drink
  isSide: boolean; // Is the menu item a side
}

Now, when the user opens the cart we’re going to send an event mythicalburger:OPEN_CART. And the extension data we’re sending is going to look like this:

interface CDPExtensionData {
  hasBeef: boolean;
  hasChicken: boolean;
  hasFish: boolean;
  hasVegan: boolean;
  hasSide: boolean;
  hasDrink: boolean;
}

If we left it as this we’d actually get this error:

Argument of type 'CDPExtensionData' is not assignable to parameter of type 'INestedObject'.
  Index signature for type 'string' is missing in type 'CDPExtensionData'.

So to avoid it, we’re going to reduce that object to this.

interface CDPExtensionData {
  [key: string]: boolean;
}

And now we have to build our data extension object based upon objects in the cart. So let’s create that. This is a simple function to run through all the cart items and determine whether or not the cart items matches the type. We could very easily extend this to more complicated features.

// Function to create extensionData from cart
  const createExtensionData = (cart: Burger[]): CDPExtensionData => {
    return cart.reduce<CDPExtensionData>(
      (acc, burger) => {
        if (burger.isBeef) acc.hasBeef = true;
        if (burger.isChicken) acc.hasChicken = true;
        if (burger.isFish) acc.hasFish = true;
        if (burger.isVegan) acc.hasVegan = true;
        if (burger.isSide) acc.hasSide = true;
        if (burger.isDrink) acc.hasDrink = true;
        return acc;
      },
      {
        hasBeef: false,
        hasChicken: false,
        hasFish: false,
        hasVegan: false,
        hasSide: false,
        hasDrink: false,
      }
    );
  };

And lastly we’re going to, when a user opens the modal, trigger that event with the data extension.

const toggleCartModal = async () => {
    if (!isCartModalOpen) {
      const cdpExtensionData = createExtensionData(cartItems);
      await engage?.event("mythicalburger:OPEN_CART", eventData, cdpExtensionData);
    }
    setIsCartModalOpen(!isCartModalOpen);
  };

We’re going to come back to this in a bit when we want to trigger a personalization. Because we’re not actually changing pages here, we need to fire off a personalize event so that it actually triggers. You won’t need to do this if you have your CDPPageView.tsx component initialized on every page as Engage (pre-21.6.0) will actually determine if a Personalization needs to be run.

Now you can see, when we open the cart after first adding the Phoenix Fire Burger to the cart, that an event is fired off with our cart details.

If we close this, add another option, and then re-open the cart, another event will be fired off.

Shopping cart pop-up window showing Phoenix Fire Burger with price on a restaurant website.

This got us setup. CDP is now receiving our information and Personalize will be able to act upon it via our Decision Model, so let’s go create that now.

Create a Decision Model to Grab Recent Session Data

One of the best ways to personalize based upon real-time data is to explore the recent session activity of a guest. This includes all the events, like the one above,

In Personalize inside the Decisioning section, we created a Decision Model called Mythical Burger DM. After which we will need to create a variant. Think of this as a version of the Decision Model itself.

Create and Setup a Variant in Our Decision Model

Let’s walk through some steps creating the variant and the Canvas of our Decision Model.

User interface of a decision model titled 'Mythical Burger DM' with sections for Draft, Test, and Production.

Now we will click Add Variant and name it v1. You can use any alpha numeric name you wish. I like the version notation as it allows me to keep track which version is the most recent. If we had already created a variant, it would give us the option to base the new variant on that older version.

Popup window for creating a new variant titled 'v1' in a decision model system.

Clicking on the Variant will now take us to the Decision Model canvas.

Blank decision model canvas with prompts to add shapes to build a decision model.

It’s here we’re going to create a really simple process for our Model to follow.

Decision model canvas with a decision table, programmable component, and input data sessions connected.

We have set our Input Data to be Sessions and are using the Programmable Decision type and that flows into our Decision Table.

Important: While you can indeed add more Programmable types, they warn you that it will impact performance. Optimally Decision Models should run in their entirety in under 200ms. Going beyond that and it will potentially impact the experience for your user.

Selecting Sessions as our Input Data allows us to access recent session data of our visitors which includes the data extension information we built above.

Setup a Programmable in Our Decision Model

If we open up our Programmable, we will get an area to add JavaScript. It’s going to be very simple.

(function () {
  var session = guest.sessions[0];
  if (!session) {
    return {};
  }
  for (var j = 0; j < session.events.length; j++) {
    var data = session.events[j].arbitraryData;
    if (
      (session.events[j].type === "mythicalburger:OPEN_CART") &&
      data &&
      data.ext
    ) {
      return data.ext;
    }
  }
  return {}; //Default
})();

Here you can see we’re looking for the latest session type that matches mythicalburger:OPEN_CART. Once we have that we’re going to check there is a data extension and then return that.

 Javascript code within a programmable component for a decision model and output reference settings.

On the right hand side we’ve created our Output Reference as mbData. Think of this as the variable that will receive the return type which is of type Map. By using Map we can effectively take the entire data extension and send it to the Decision Table.

Now you don’t have to just return the arbitrary data that is in that Event. You could very well add in additional logic and transform that data. The trouble with doing so though is maybe you have a marketing team and they aren’t JavaScript savvy. Moving the logic to the Decision Table allows them to maintain that power in a more user friendly way.

If you want to validate the information we’re obtaining, you can click Test Canvas at the top.

Inside the Test Canvas we can select either a Visitor or a Customer or if we know someone we’re trying to validate against, we can look them up and in our case search for the mythicalburger:OPEN_CART event. And voila, we can see our event data showing.

 Test canvas displaying a request payload with event type 'mythicalburger:OPEN_CART' highlighted.

Now if we click Test Canvas right now, we’re going to get an error that looks like this and that’s because we haven’t yet gone and setup the Decision Table.

Error message on a test canvas indicating an issue with transforming decisions from input stream.

So let’s go set that up now.

Setup a Decision Table in Our Decision Model

Let’s open up the Decision Table that’s back on our Canvas that we setup in the beginning. First thing we see is, well, a table. You have the option to add both input and output fields.

Decision table interface with options to add input and output columns for creating business rules.

The input fields are all the fields that are returned from our Programmable and the output fields are all the fields that will be available to our API of our Web Experience. In our case, we’re going to create input columns for every key within our data extension. Once we’re done, it will look like this.

Clicking Add Input Column allows us to select the source of the input. We will select our Programmable Mythical Burger Programmable.

Popup window showing options for adding an input column in a decision model: Guest Data, Script, and Programmables.

After selecting it, we will be prompted to name our reference our first column.

Interface for adding a programmable input column named 'Cart Had Beef' with a map key 'mbData.hasBeef' and type boolean.

The Map Key is initially filled out and all we had to do was add in the attribute hasBeef on to it. We also selected the type as boolean. The Name of the Column is a human readable name for that column.

We will then repeat this for all of the attributes within the returned mbData object. It gets a bit unwieldly but it’s now complete and we need to move on to setting up the Output Columns.

Decision table showing input columns such as 'Cart Had Beef', 'Cart Had Chicken', and others with their map keys.

Setting Up Output Columns

The complexity of the output columns is really dependent on how the data will be used. For us, my strategy is to, depending upon the results, display an appropriate banner at the top of the cart.

Popup window for creating an output column named 'Banner Url' with a reference and type string.

Decision table displaying an output column named 'Banner Url' along with various input columns.

You can see what it looks like here.

What will be important next is to fill in some of the potential results with inputs and corresponding outputs.

For the purposes of this example, I’m only going to need one row, because I want to display the following ad if someone chooses a chicken burger. This is a “mythical” burger restaurant after all, so we can have a little fun.

Promotional banner featuring an animated chicken next to a burger, with text 'Made with Real Chicken! Kinda.'

It’s also important to determine what’s called the Hit Policy. This will determine whether one or more or no results are returned to the Web Experience API. It allows for one or more result to be returned to the Personalization API. It’s also one of the first places I often look when I don’t receive any results returned.

For us, we’re going to pick the First option. This way, whatever rule is satisfied first, that’s the one that is returned. Again, this depends on the purpose of your personalization.

Dropdown menu for selecting a hit policy in a decision model with options like 'Unique', 'First', 'Any', and 'Collect'.

We’ve added in our rule for when the Cart Had Chicken is set to true we return a bannerUrl value with the URL to the banner above.

Decision table with a rule showing 'true' for 'Cart Had Chicken' and a banner URL output.

Before we move on, let’s ensure everything is working as it should. We’re going to go back into Test Canvas, reselect our customer, and click Test Canvas. No more error. We can see in the Full Response the JSON that’s being sent to the API. We can see it successfully chose the rule in the Decision Table and is returning the bannerUrl with the right value. We can also see just how fast this whole process ran. A mere 5.96ms. Fantastic!

Test canvas displaying execution results with outputs section showing a rule and banner URL for an image.

If there were errors, it might look something like this. I would strongly recommend you click Save prior to running your Test Canvas otherwise you might not be looking at what the true result might be.

Execution time of 6.38 ms displayed on the Test Canvas for 'Mythical Burger Programmable'.

While it’s not immediately obvious, if you click the Mythical Burger Programmable and it will expand and show you the real error.

As we’re satisfied with the outcome we need to move that variant to Production in order for it to be used.

Popup window with the title 'Move to Production' explaining the effects of the action on the decision model.

An important thing to note. Once we move this variant to production we can no longer edit it. In order to make changes we must first create a new variant based upon v1 and then move that to production and archive the old one.

Decision model interface showing a variant 'v1' ready to move from 'Draft' to 'Production'.

Let’s say we needed to make some changes, then we can click Add Variant. And in the popup, we can select v1 as the Base Variant.

Popup window for selecting a base variant for a decision model with options 'No Base' and 'v1'.

Create a Web Experience Using Decision Model API Output

We’re nearly there. The last step within Sitecore Personalize is to create a Web Experience.

Popup for naming a new web experience, with fields for experience name and ID. 

We’re naming our Web Experience MB Cart Banner and that generates an ID with a value of mb_cart_banner. We will be utilizing this value later when initiating our Personalization.

Once we click Create we will next need to create a Variant for the experience.

Draft screen for a web experience named 'MB Cart Banner' indicating no content has been created yet.

As we are creating something unique, we’re going to select Create Template.

Interface showing different template options for creating a web experience, with a button to 'Create template'.

We’re now presented with four tabs:

  • HTML
  • CSS
  • JavaScript
  • API - in the format of a JSON object

Setup the API Response of the Web Experience

Let’s look at the API tab first as this is where we need to input the data coming from the Decision Table. The code below is in template engine called Freemarker.

<#-- Construct the API response using Freemarker -->
{
   <#if (decisionModelResults)??>

    <#if (decisionModelResults.decisionModelResultNodes)??>
    <#list decisionModelResults.decisionModelResultNodes as result>
    <#if (result.outputs)??>
    <#list result.outputs as output>
    <#list output as outputName, outputValue>
    <#if (!outputValue?is_hash) && outputName != "ruleId">
    "${outputName}" : "${outputValue}",
    </#if>
    </#list>
    </#list>
    </#if>
    </#list>
    </#if>
  </#if>
  "guestref": "${guest.ref}"
}

Now Freemarker isn’t the easiest to work with. Thankfully Sitecore has snippets you can copy from to get you going and you can find these on the right side under the Snippets tab.

You might be asking, why am I returning the guest.ref attribute from the guest object? The answer is simple. I don’t plan on using it, but by returning something simple at the end, I’ve avoided complex loop logic from being needed within the Freemarker template. You can see how complicated the Freemarker script can get so just simplify it as best as you can. I could have just as easily created an empty attribute value.

Selecting Preview API at the top of the page, selecting a test subject within the Test Scenario tab and running it, we can now see we’re returning this to the Web Experience.

Screen showing API code construction for a variant with a test condition and a 'Preview API' button.

But where is the bannerUrl you ask? It’s not there, because we haven’t hooked up the Decision Model to the Web Experience so let’s go do that. Save and then hit the arrow next to Variant 1.

Click the Decisioning option.

'Decisioning' tab in the Experience Builder indicating where to connect a decision model to return offers or content.

And then click Add decision model.

Overview of decisioning features, including adding a decision model and information on personalization and external APIs.

Then select the Decision Model we created earlier.

List of decision models with 'Mythical Burger DM' highlighted for selection.

Once selected, click the X and close the tab.

Now with the Decision Model selected, back in our variant, let’s click Preview API again.

Test condition interface with JSON output for a banner URL and guest reference.

Now, the next bit it very different. If we had changed pages in between initiating the OPEN_CART and displaying of said cart, we could run the entire personalization from within Personalize. However, in our case, because it’s a modal, we can’t.

That said, I’m going to show both ways this would work, because I’m a nice guy.

If We Changed Pages

Voila, choosing our guest, we can see the bannerUrl now being processed. This is vital as we need to access bannerUrl as part of our HTML template.

Within our CartModal.tsx file, we’ve got a div tag ready to accept a marketing banner.

<div id="#marketing-banner"></div>

Thus, in our HTML tab we would change it to be this.

<div id="#marketing-banner">
    <img src="{{bannerUrl}}" />
</div>

In our JavaScript tab, we would change it to be this.

(function () {
  replaceHTMLExact('#marketing-banner');
})();

Now if triggering the opening of a cart changed the page and caused effectively a new VIEW event, we’d see this Web Experience. Sadly, we don’t have that option this time around.

If We Stayed on the Same Page

If we are not changing pages, which in our case we aren’t, we will need to redraw the CartModal.tsx file using a useEffect. We can do this by first initiating the Personalize Web Experience manually as follows.

const toggleCartModal = async () => {
    if (!isCartModalOpen) {
      const cdpExtensionData = createExtensionData(cartItems);
      await engage?.event("mythicalburger:OPEN_CART", eventData, cdpExtensionData);


      // Manually call personalize via friendlyId of Web Experience 'mb_cart_banner'.
            const personalizeData = {
        channel: "WEB",
        currency: "USD",
        pointOfSale: "cdp-test-1",
        friendlyId: "mb_cart_banner"
      }
      const response = await engage?.personalize(personalizeData) as MarketingBannerType;
      // Add marketing banner response to cart context
            setModalMarketingBanner(response);
    }
    setIsCartModalOpen(!isCartModalOpen);
  };

From within the CartModal.tsx file we can add the following useEffect to update the bannerUrl.

useEffect(() => {
    // Update bannerUrl whenever modalMarketingBanner changes
    setBannerUrl(modalMarketingBanner.bannerUrl);
  }, [modalMarketingBanner]);

And then inside the return statement, right where we had our empty div tag from earlier, add the image being returned.

<div id="#marketing-banner">
  {bannerUrl ? <img src={bannerUrl} alt="Marketing Banner" /> : <></>}
</div>

And that’s it.

Now, when a user adds a chicken related item to their cart, proceeds to open their cart, they’re shown the banner and if they don’t have chicken, they don’t see it.

A shopping cart interface showing an animated chicken and a promotional banner for 'Made with Real Chicken! Kinda.'

I wonder what we should personalize next?



Meet David Austin

Development Team Lead | Sitecore Technology MVP x 3

📷🕹️👪

David is a decorated Development Team Lead with Sitecore Technology MVP and Coveo MVP awards, as well as Sitecore CDP & Personalize Certified. He's worked in IT for 25 years; everything ranging from Developer to Business Analyst to Group Lead helping manage everything from Intranet and Internet sites to facility management and application support. David is a dedicated family man who loves to spend time with his girls. He's also an avid photographer and loves to explore new places.

Connect with David