30 min read
Published:
Welcome back, folks! Today, I'm continuing my mini-series on the Sitecore Personalize SDK I created, which wraps the Sitecore Personalize APIs detailed in the documentation here: https://doc.sitecore.com/personalize/en/developers/api/rest-apis.html. I'll be focusing on the REST APIs related to creating a Flow in Sitecore Personalize.
A flow in Sitecore Personalize is essentially a single object type representing two distinct types of functionality: an Experience or an Experiment. In my SDK, I've split the creation of Experiences and Experiments into separate functions. Why? To provide more control over the required fields of the Flow Definition.
Behind the scenes, I use Zod to manage which fields are absolutely required for creating an Experience or Experiment. It's worth noting that an Experiment needs more fields than an Experience. Also, creating a basic experience or experiment requires fewer fields than what's realistically needed. For instance, to create a true experience
like a Web Experience, you'd want to include template assets as well. I'll dive deeper into this later in this post.
In this post, I'll focus specifically on Experiences, as the SDK is still being developed to fully support both experiences and experiments. As mentioned earlier, I aim to make this SDK user-friendly for creating either flow type. Consequently, significant effort is being invested in Zod schema definitions to achieve this goal. It's important to note that the required data will vary depending on the flow type, such as triggered flows or web/full stack flows. Additionally, incorporating a decision model or condition into the flow can significantly impact the required fields.
If you haven't already, make sure to pull the latest SDK from https://www.npmjs.com/package/sitecore-personalize-tenant-sdk. To create experiences, you'll need version 0.4.5 or newer.
Let's dive into creating a simple experience using the SDK. This basic example highlights the essential fields needed to create an experience. However, as mentioned earlier, there are many more fields involved in a flow, which can vary depending on the scenario. I'll document these in detail on the repo's README and supporting documents, as it would be too extensive to cover every scenario here. As with previous blog posts, ensure you have a Node-based repository and have installed the latest version of the SDK. Once that's set up, you'll need to initialize the SDK as discussed in the series. Here's the code to get started:
1const config: IClientInitOptions = {2 clientId: "<ClientId>",3 clientSecret: "<ClientSecret>",4 region: RegionOptions.EU5};67const personalizeClient = new Client(config);
Where can you obtain the Client ID and Secret? I won't delve into that in this post, but you can refer to my previous blog: Sitecore Personalize: New Developer Focused API Keys (dylanyoung.dev), where I covered this topic in detail. For our current use case, let's assume you're creating keys scoped to Multiple resources
.
Once we've initialized the SDK, creating an experience is quite straightforward. However, it's crucial to keep in mind which fields are absolutely required. Let's examine the code we'll use to create our Experience:
1const testExperience: IFlowDefinition = {2 name: 'Test Blog Experience',3 friendlyId: 'test_blog_experience',4 type: FlowType.WebFlow,5 channels: [ FlowChannel.Web ],6 status: FlowStatus.Draft,7 schedule: {8 type: FlowScheduleType.Simple,9 startDate: new Date().toISOString(),10 }11}1213let testExperienceResponse = await personalizeClient.Flows.CreateExperience(testExperience);1415console.log(JSON.stringify(testExperienceResponse));
If all goes well with that code and you've initialized personalizeClient
correctly above, you should now see the experience Test Blog Experience
in your Sitecore Personalize tenant. But what about creating a more advanced experience that we can start using right away? Well, this is where adding the correct fields gets a bit trickier. Let's dive into this topic in the next section.
Now that we've explored creating a simple experience in Sitecore Personalize, let's dive into a more advanced use case. We'll assume we have an existing Decision Model
in our Sitecore Personalize tenant. As of this writing, the SDK offers limited support for Decision Models. However, a future scenario might involve creating a Decision Model via API, then using the APIs to create an Experience powered by that Decision Model. Why would anyone want to do this? Well, that's a teaser for some upcoming content in the pipeline—I've got some exciting ideas about potential applications, but we'll dive into those later.
Let's start by examining the code required to create our more complex use case. Then we'll work backwards to break down the new additions to this Web Experience.
1const testExperience: IFlowDefinition = {2 name: "Test API Experience5",3 friendlyId: "test_api_experience5",4 type: FlowType.WebFlow,5 channels: [ FlowChannel.Web ],6 status: FlowStatus.Draft,7 schedule: {8 type: FlowScheduleType.Simple,9 startDate: new Date().toISOString(),10 },11 variants: [12 {13 name: 'Variant API Test',14 isControl: false,15 assets: {16 html: "<!-- Use dynamic Guest variables, type ctrl+space or guest to explore available entities.-->\n<!-- Type \"d\" to see decisioning helpers -->\n<div id=\"pers-transition-card\">\n <div class=\"img-container\">\n <div class=\"img-container__image\"></div>\n </div>\n <div class=\"pers-transition-card__body\">\n <h3>[[Title | string | Title | {required:true, group: Title, order: 1}]]</h3>\n <p>[[Description | text | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt | {required:true, group: Description, order: 1}]]</p>\n <div class=\"options-container\">\n <a id=\"pers-transition-card--secondary\" class=\"options-container__secondary\">[[Dismiss Text | string | No Thanks | {required:true, group: Secondary Button, order: 1}]]</a>\n <a id=\"pers-transition-card--primary\" class=\"options-container__primary\">[[CTA Text | string | Yes Please | {required:true, group: CTA Button, order: 1}]]</a>\n </div>\n </div>\n</div>",17 css: "/*\n[[Background Colour | colour | #fff| {equired: true, group: General, groupOrder: 1, order: 3}]]\n[[Background Image Colour | colour | #F3F5F7| {equired: true, group: General, order: 4}]]\n[[Image URL | string || {required:true, group: General, order: 1}]]\n[[Font | enum(Arial,Arial Narrow,Brush Script MT,Calibri,Cambria,Candara,Copperplate,Courier,Courier New,Didot,Garamond,Geneva,Helvetica,Lucida Bright,Monaco,Optima,Perpetua,Times,Times New Roman,Verdana) |Arial|{group: General, order: 2}]];\n[[CTA Colour | colour | #000 | {equired: true, group: CTA Button, groupOrder: 4, order: 2}]]\n[[CTA Text Colour | colour | #fff | {equired: true, group: CTA Button, order: 3}]]\n[[CTA Hover Color | colour | #9e9e9e |{ required: true, group: CTA Button, order: 4} ]]\n[[CTA Text Hover Color | colour | #fff | { required: true, group: CTA Button, order: 5} ]]\n[[Secondary Colour | colour | #F3F5F7 | {equired: true, group: Secondary Button, groupOrder: 5, order: 2}]]\n[[Secondary Text Colour | colour | #000 | {equired: true, group: Secondary Button, order: 3}]]\n[[Secondary Hover Color | colour | #fff |{ required: true, group: Secondary Button, order: 4} ]]\n[[Secondary Text Hover Color | colour | #000 | { required: true, group: Secondary Button, order: 5} ]]\n[[Title Text Colour | colour | #4D4D4D | {group: Title, groupOrder: 2, order: 2}]]\n[[Title Text Font Size | number | 36 | { group: Title, order: 3 }]]\n[[Description Text Colour | colour | #4D4D4D | { group: Description, groupOrder: 3, order: 2 }]]\n[[Description Text Font Size | number | 16 | { group: Description, order: 3 }]]\n*/\n\n#pers-{{ref}} a,\n#pers-{{ref}} p,\n#pers-{{ref}} span {\n font-family: [[ Font ]];\n font-weight: 400;\n line-height: 1.2;\n}\n\n#pers-{{ref}} #pers-transition-card {\n position: fixed;\n width: 435px;\n right: 0;\n bottom: -1000px;\n z-index: 999999;\n transition: bottom .6s ease-in-out;\n margin-bottom: 0;\n border-radius: 4px;\n background-color: [[Background Colour]];\n overflow: hidden;\n box-shadow: 0 0.2rem 0.4rem 0.2rem rgba(0,0,0,.1);\n font-size: initial;\n}\n\n#pers-{{ref}} #pers-transition-card.open {\n bottom: 0;\n}\n\n#pers-{{ref}} #pers-transition-card .img-container {\n width: 100%;\n height: 250px;\n}\n\n#pers-{{ref}} #pers-transition-card .img-container__image {\n margin: 0;\n background: [[Background Image Colour]];\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n font-size: var(--font-size-h4);\n background-image: url(\"[[Image URL]]\");\n background-position: center;\n background-repeat: no-repeat;\n background-size: cover;\n}\n\n#pers-{{ref}} #pers-transition-card .pers-transition-card__body {\n padding: 37px 34px;\n}\n\n#pers-{{ref}} #pers-transition-card .pers-transition-card__body h3 {\n margin-top: 28px;\n line-height: 1.5;\n font-size: [[ Title Text Font Size ]]px;\n font-weight: bold;\n text-align: center;\n margin: 0;\n color: [[Title Text Colour]];\n font-family: [[Font]];\n}\n\n#pers-{{ref}} #pers-transition-card .pers-transition-card__body p {\n padding: 12px 0;\n margin: 0;\n font-size: [[ Description Text Font Size ]]px;\n line-height: 28px;\n text-align: center;\n color: [[Description Text Colour]];\n font-family: [[Font]];\n}\n\n#pers-{{ref}} #pers-transition-card .options-container {\n display: flex;\n justify-content: center;\n}\n\n#pers-{{ref}} #pers-transition-card .options-container__primary {\n display: inline-block;\n border-radius: 4px;\n color: [[CTA Text Colour]];\n background-color: [[CTA Colour]];\n padding: 10px 30px;\n cursor: pointer;\n font-size: [[Font]];\n}\n\n#pers-{{ref}} #pers-transition-card .options-container__primary:hover {\n color: [[CTA Text Hover Color]];\n background-color: [[CTA Hover Color]];\n}\n\n#pers-{{ref}} #pers-transition-card .options-container__secondary {\n display: inline-block;\n border: 1px solid #C1C9D0;\n border-radius: 4px;\n color: [[Secondary Text Colour]];\n background-color: [[Secondary Colour]];\n padding: 10px 30px;\n margin-right: 13px;\n cursor: pointer;\n font-size: [[Font]];\n}\n\n#pers-{{ref}} #pers-transition-card .options-container__secondary:hover {\n color: [[Secondary Text Hover Color]];\n background-color: [[Secondary Hover Color]];\n}",18 js: "// Adds a unique variant identifier to CSS when deployed to ensure CSS does not impact styling of other elements.\nvar compiledCSS = Engage.templating.compile(variant.assets.css)(variant);\nvar styleTag = document.getElementById('style-' + variant.ref);\nif (styleTag) {\n styleTag.innerHTML = compiledCSS;\n}\n// End Adds a unique variant identifier to CSS when deployed to ensure CSS does not impact styling of other elements.\n\n// make space in the body for the experience\ninsertHTMLAfter('body', 'pers-');\nvar persContent = document.querySelector(\"#pers-\"+variant.ref+ \" #pers-transition-card\");\n\nsetTimeout(function() {\n persContent.classList.add(\"open\");\n sendInteractionToPersonalize(\"VIEWED\");\n});\n\nvar persCardClose = document.body.querySelector(\"#pers-\"+variant.ref+ \" #pers-transition-card--secondary\");\npersCardClose.onclick = function() {\n sendInteractionToPersonalize(\"DISMISSED\");\n persContent.classList.remove(\"open\");\n}\n\nvar persCardCta = persContent.querySelector(\"#pers-\"+variant.ref+ \" #pers-transition-card--primary\");\npersCardCta.onclick = function() {\n sendInteractionToPersonalize(\"CLICKED\");\n window.location.href = \"[[CTA destination URL | string || {required:true, group: CTA Button}]]\";\n}\n\nfunction sendInteractionToPersonalize(interactionType) {\n const type = \"[[ Experience ID | String | CORNER_POPUP | {required: true}]]_INTERACTION\";\n const eventData = {\n \"channel\": \"WEB\",\n \"pointOfSale\": Engage.settings.pointOfSale,\n \"interactionID\": \"PERS_CORNER_POPUP\",\n \"interactionType\": interactionType\n };\n\n window.engage.event(type, eventData);\n}"19 },20 templateVariables: {21 "Title": "Title",22 "Description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt",23 "Dismiss Text": "No Thanks",24 "CTA Text": "Yes Please",25 "Font": "Arial",26 "CTA Colour": "#000",27 "Secondary Colour": "#F3F5F7",28 "Title Text Colour": "#4D4D4D",29 "Description Text Colour": "#4D4D4D",30 "Background Colour": "#fff",31 "CTA Text Colour": "#fff",32 "Secondary Text Colour": "#000",33 "Title Text Font Size": "36",34 "Description Text Font Size": "16",35 "Background Image Colour": "#F3F5F7",36 "CTA Hover Color": "#9e9e9e",37 "Secondary Hover Color": "#fff",38 "CTA Text Hover Color": "#fff",39 "Secondary Text Hover Color": "#000",40 "Experience ID": "CORNER_POPUP"41 },42 tasks: [43 personalizeClient.Flows.CreateTemplateRenderTaskInput('<#-- Construct the API request body using Freemarker -->\n<#-- For your Experience to run your API tab must have, at a minimum, open and closing brackets -->\n{ <#-- Freemarker will go here --> }'),44 personalizeClient.Flows.CreateDecisionModelTaskInput([ "1885d8c9-d32d-4bf3-975d-f8dbd91882bc"])45 ]46 }47 ]48 }4950 let testExperienceResponse = await personalizeClient.Flows.CreateExperience(testExperience);5152 console.log(JSON.stringify(testExperienceResponse, null, 2));
As you can see, considerably more code is needed to create this use case because we need to create a variant for our Experience. When I first learned Sitecore Personalize, you could have more than one variant per experience. However, that's no longer the case—now your main variant will receive 100% of the traffic.
The variant includes HTML, CSS, and JavaScript, typically derived from a template or custom-built. If we were creating this experience from local templates, we'd pull the template source to build out these three properties. The API output is stored in a different location, where I use personalizeClient.Flows.CreateTemplateRenderTask('Pass in your Free marker here')
to easily create this object (as there's much more involved behind the scenes). I'm working on more helper functions like this, aiming to hide the boilerplate code and simplify experience creation.
The templateVariables
is a key-value pair object where the keys are strings. This structure allows us to inject data into our template (although technically, we're not using a true template here). Our understanding of templates in Personalize evolves as we delve into the underlying code. In essence, the template functions more like a snippet—a reusable piece of code that drives the variants themselves.
I also created another task helper, personalizeClient.Flows.CreateDecisionModelTaskInput([ "decision-model-ref"])
, which simplifies this object. As mentioned earlier, you need to create the Decision Model first. I hope to cover these multi-resource scenarios soon.
There you have it! Our experience is now successfully created in our tenant:
You might wonder why you'd want to create an experience using this SDK. Well, my next blog in this mini-series will showcase a compelling scenario—leveraging Generative AI. But that's just the tip of the iceberg. I'm excited to explore numerous other valuable use cases in the coming months. So, stay tuned for these upcoming articles—you won't want to miss them!