> Synerise Documentation — Use Cases (Part 2 of 3)
>
> This is part 2 of 3 of the "Use Cases" section. To reconstruct the full section, fetch all 3 parts in order (part 1, part 2, …) and concatenate them. Each article begins with a top-level "# " heading. The manifest listing all sections is at https://hub.synerise.com/llms-full.txt
# Cross-sell recommendations with category tabs based on last purchased product
This use case presents cross-sell recommendations based on the customer's most recently purchased product. The section displays the last purchased item (or items) together with complementary product recommendations from different categories.
Recommended products are grouped into categories and presented as tabs. This allows users to browse complementary items by switching between category views instead of scrolling through a single list. The goal is to make post-purchase recommendations easier to explore while maintaining contextual relevance to the last transaction.
The setup is based on three elements:
- an aggregate identifying the most recently purchased product,
- a product catalog to retrieve product attributes and display information,
- cross-sell recommendation models generating complementary items for each purchased product.
**AI Hub > (AI Recommendations) Models > Add recommendation**.
2. Enter the name of the recommendation (it is only visible on the list of recommendations).
3. In the **Type & Items Feed** section, click **Define**.
4. From the **Items Feed** dropdown list, select an item feed.
5. In the **Type** section, choose the **Cross-sell** recommendation type.
6. Confirm the settings by clicking **Apply**.
7. In the **Items** section, click **Define**.
2. Define the minimum and maximum number of items that will be recommended to the customer in each slot - here it will be from 8 to 20 items.
3. Define [Static filters](/docs/ai-hub/recommendations-v2/creating-recommendation-campaign#static-filters).
4. In our case, in the **Static filter** section, click **Define filter**.
5. Select **Visual Builder**.
6. Click **Select value**.
5. Choose **category**.
6. As an operator, choose **is defined**.
4. Confirm by clicking **Apply**.
Check the HTML code
<!-- 1. Get last boght products --> {% set lastBoughtProducts = []%} {% aggregate 245aa662-d642-352e-acef-18198604ac13 %} {%- for item in aggregate_result -%} {% do lastBoughtProducts.append(item)%} {%- endfor -%} {% endaggregate %} <!-- 2. Get recommendations for each of the last bpugt products --> {% set productsWithCrossSell = [] %} {%- for p in lastBoughtProducts -%} {% set itemContext = []%} {% do itemContext.append(p)%} {% recommendations3 campaignId=VR6SOFcDEbAg itemsIds=itemContext %} {% set prodObj = { "id": p, "crossSell": recommended_products3 }%} {%do productsWithCrossSell.append(prodObj)%} {% endrecommendations3 %} {%- endfor -%} <div class="cross-sell-wrapper unique-identifier"> <div class="left-carousel-wrapper"> <!-- 3. For each last bought product: get info about product from catalog --> <div class="snrs-left-carousel-header">Last bought</div> <div class="left-carousel my-main-carousel"> {% for product in productsWithCrossSell %} {% set key = product.id %} {% catalogitemv2.store-1(key) allowEmpty=true %} {% set object = catalog_result %} {% set name = object.name %} {% set img = object.image %} <div class="carousel-item" data-product-id="{{ product.id }}"> <img src="{{img}}""> <!-- <p class="snrs-prod-title">{{name}}</p> --> </div> {% endcatalogitemv2 %} {% endfor %} </div> </div> <div class="right-tabs-wrapper"> <!-- 4. Group products form each reco by categories (lowest level) --> {% for product in productsWithCrossSell %} {% set prodCategories = [] %} {% set categoryGroups = [] %} {% for p in product.crossSell %} {% set category = p.category|split(">")|last %} {% if prodCategories is not containing category %} {% do prodCategories.append(category) %} {% endif %} {% endfor %} {% for category in prodCategories %} {% set prodsArray = [] %} {% for p in product.crossSell %} {% set cat = p.category|split(">")|last %} {% if category == cat %} {# {% do prodsArray.append(p.itemId) %}#} {% do prodsArray.append(p) %} {% endif %} {% endfor %} {% set categoryWithProds = { "category": category, "products": prodsArray } %} {% do categoryGroups.append(categoryWithProds) %} {% endfor %} <!-- 5. Build tabs - the first is active by default --> <div class="product-tabs" data-product-id="{{ product.id }}"> {% set rangeMax = categoryGroups|length + 1%} {% for i in range(1, rangeMax) %} <input type="radio" name="tab-{{ product.id }}" id="tab-{{ product.id }}-{{ i }}" {% if i == 1 %}checked{% endif %}> {% endfor %} <div class="tab-labels"> {% for catGroup in categoryGroups %} {% set i = loop.index %} <label for="tab-{{ product.id }}-{{ i }}" class="tab-label">{{ catGroup.category }}</label> {% endfor %} </div> <!-- 6. Display reco products --> <div class="tab-contents"> {% for catGroup in categoryGroups %} {% set i = loop.index %} <div class="tab-content"> <div class="products-carousel"> {#{% for itemId in catGroup.products %} <div class="cross-sell-item"> Produkt ID: {{ itemId }} </div> {% endfor %}#} {% for p in catGroup.products %} <a href="{{p.productUrl}}" class="cross-sell-item"> <img src="{{p.image}}"> <p class="snrs-prod-title">{{p.name}}</p> <p class="snrs-prod-price">${{p.price}}</p> </a> {% endfor %} </div> </div> {% endfor %} </div> </div> <!-- 7. Styles controlling the tabs --> <style> {% for product in productsWithCrossSell %} {% set prodCategories = [] %} {% for p in product.crossSell %} {% set category = p.category|split(">")|last %} {% if prodCategories is not containing category %} {% do prodCategories.append(category) %} {% endif %} {% endfor %} {% set rangeMax = categoryGroups|length + 1%} {% for i in range(1, rangeMax) %} #tab-{{ product.id }}-{{ i }}:checked ~ .tab-labels .tab-label:nth-of-type({{ i }}), #tab-{{ product.id }}-{{ i }}:checked ~ .tab-contents .tab-content:nth-of-type({{ i }}) { display: block; } {% endfor %} {% endfor %} </style> {% endfor %} </div> </div> {% endfor %}Check the JS code
(function(){ 'use strict'; // 1. Add carousel library var tns=function(){function e(){for(var e,t,n,i=arguments[0]||{},a=1,r=arguments.length;a<r;a++)if(null!==(e=arguments[a]))for(t in e)n=e[t],i!==n&&void 0!==n&&(i[t]=n);return i}function t(e){return["true","false"].indexOf(e)>=0?JSON.parse(e):e}function n(e,t,n){return n&&localStorage.setItem(e,t),t}function i(){var e=window.tnsId;return window.tnsId=e?e+1:1,"tns"+window.tnsId}function a(){var e=document,t=e.body;return t||(t=e.createElement("body"),t.fake=!0),t}function r(e){var t="";return e.fake&&(t=P.style.overflow,e.style.background="",e.style.overflow=P.style.overflow="hidden",P.appendChild(e)),t}function o(e,t){e.fake&&(e.remove(),P.style.overflow=t,P.offsetHeight)}function s(e){var t=document.createElement("style");return e&&t.setAttribute("media",e),document.querySelector("head").appendChild(t),t.sheet?t.sheet:t.styleSheet}function l(e,t,n,i){"insertRule"in e?e.insertRule(t+"{"+n+"}",i):e.addRule(t,n,i)}function c(e){return("insertRule"in e?e.cssRules:e.rules).length}function u(e,t){return Math.atan2(e,t)*(180/Math.PI)}function f(e,t){var n=!1,i=Math.abs(90-Math.abs(e));return i>=90-t?n="horizontal":i<=t&&(n="vertical"),n}function d(e,t){return e.className.indexOf(t)>=0}function v(e,t){d(e,t)||(e.className+=" "+t)}function h(e,t){d(e,t)&&(e.className=e.className.replace(t,""))}function p(e,t){return e.hasAttribute(t)}function m(e,t){return e.getAttribute(t)}function y(e){return void 0!==e.item}function g(e,t){if(e=y(e)||e instanceof Array?e:[e],"[object Object]"===Object.prototype.toString.call(t))for(var n=e.length;n--;)for(var i in t)e[n].setAttribute(i,t[i])}function b(e,t){e=y(e)||e instanceof Array?e:[e],t=t instanceof Array?t:[t];for(var n=t.length,i=e.length;i--;)for(var a=n;a--;)e[i].removeAttribute(t[a])}function x(e){e.style.cssText=""}function T(e){p(e,"hidden")||g(e,{hidden:""})}function E(e){p(e,"hidden")&&b(e,"hidden")}function C(e){return e.offsetWidth>0&&e.offsetHeight>0}function w(e){return"boolean"==typeof e.complete?e.complete:"number"==typeof e.naturalWidth?0!==e.naturalWidth:void 0}function N(e){for(var t=document.createElement("fakeelement"),n=(e.length,0);n<e.length;n++){var i=e[n];if(void 0!==t.style[i])return i}return!1}function O(e,t){var n=!1;return/^Webkit/.test(e)?n="webkit"+t+"End":/^O/.test(e)?n="o"+t+"End":e&&(n=t.toLowerCase()+"end"),n}function D(e,t){for(var n in t){var i=("touchstart"===n||"touchmove"===n)&&W;e.addEventListener(n,t[n],i)}}function k(e,t){for(var n in t){var i=["touchstart","touchmove"].indexOf(n)>=0&&W;e.removeEventListener(n,t[n],i)}}function A(){return{topics:{},on:function(e,t){this.topics[e]=this.topics[e]||[],this.topics[e].push(t)},off:function(e,t){if(this.topics[e])for(var n=0;n<this.topics[e].length;n++)if(this.topics[e][n]===t){this.topics[e].splice(n,1);break}},emit:function(e,t){this.topics[e]&&this.topics[e].forEach(function(e){e(t)})}}}function M(e,t,n,i,a,r,o){function s(){r-=l,u+=f,e.style[t]=n+u+c+i,r>0?setTimeout(s,l):o()}var l=Math.min(r,10),c=a.indexOf("%")>=0?"%":"px",a=a.replace(c,""),u=Number(e.style[t].replace(n,"").replace(i,"").replace(c,"")),f=(a-u)/r*l;setTimeout(s,l)}Object.keys||(Object.keys=function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t}),function(){"use strict";"remove"in Element.prototype||(Element.prototype.remove=function(){this.parentNode&&this.parentNode.removeChild(this)})}();var P=document.documentElement,I=!1;try{var S=Object.defineProperty({},"passive",{get:function(){I=!0}});window.addEventListener("test",null,S)}catch(e){}var W=!!I&&{passive:!0},H=navigator.userAgent,L=!0,z={};try{z=localStorage,z.tnsApp&&z.tnsApp!==H&&["tC","tSP","tMQ","tTf","tTDu","tTDe","tADu","tADe","tTE","tAE"].forEach(function(e){z.removeItem(e)}),z.tnsApp=H}catch(e){L=!1}localStorage||(z={});var B=document,R=window,j={ENTER:13,SPACE:32,PAGEUP:33,PAGEDOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40},q=t(z.tC)||n("tC",function(){var e=document,t=a(),n=r(t),i=e.createElement("div"),s=!1;t.appendChild(i);try{for(var l,c=["calc(10px)","-moz-calc(10px)","-webkit-calc(10px)"],u=0;u<3;u++)if(l=c[u],i.style.width=l,10===i.offsetWidth){s=l.replace("(10px)","");break}}catch(e){}return t.fake?o(t,n):i.remove(),s}(),L),G=t(z.tSP)||n("tSP",function(){var e,t,n=document,i=a(),s=r(i),l=n.createElement("div"),c=n.createElement("div");return l.style.cssText="width: 10px",c.style.cssText="float: left; width: 5.5px; height: 10px;",e=c.cloneNode(!0),l.appendChild(c),l.appendChild(e),i.appendChild(l),t=c.offsetTop!==e.offsetTop,i.fake?o(i,s):l.remove(),t}(),L),F=t(z.tMQ)||n("tMQ",function(){var e,t=document,n=a(),i=r(n),s=t.createElement("div"),l=t.createElement("style"),c="@media all and (min-width:1px){.tns-mq-test{position:absolute}}";return l.type="text/css",s.className="tns-mq-test",n.appendChild(l),n.appendChild(s),l.styleSheet?l.styleSheet.cssText=c:l.appendChild(t.createTextNode(c)),e=window.getComputedStyle?window.getComputedStyle(s).position:s.currentStyle.position,n.fake?o(n,i):s.remove(),"absolute"===e}(),L),U=t(z.tTf)||n("tTf",N(["transform","WebkitTransform","MozTransform","msTransform","OTransform"]),L),X=t(z.tTDu)||n("tTDu",N(["transitionDuration","WebkitTransitionDuration","MozTransitionDuration","OTransitionDuration"]),L),V=t(z.tTDe)||n("tTDe",N(["transitionDelay","WebkitTransitionDelay","MozTransitionDelay","OTransitionDelay"]),L),Y=t(z.tADu)||n("tADu",N(["animationDuration","WebkitAnimationDuration","MozAnimationDuration","OAnimationDuration"]),L),K=t(z.tADe)||n("tADe",N(["animationDelay","WebkitAnimationDelay","MozAnimationDelay","OAnimationDelay"]),L),Q=t(z.tTE)||n("tTE",O(X,"Transition"),L),J=t(z.tAE)||n("tAE",O(Y,"Animation"),L);F||(G=!1);var Z=function(t){function n(){return R.innerWidth||B.documentElement.clientWidth||B.body.clientWidth}function a(e){var t;do{t=e.clientWidth,e=e.parentNode}while(!t);return t}function r(e){var n=t[e];return!n&>&&yt.indexOf(e)>=0&>.forEach(function(t){mt[t][e]&&(n=!0)}),n}function o(e,n){n=n?n:xt;var i,a={slideBy:"page",edgePadding:!1,autoHeight:!0};if(!tt&&e in a)i=a[e];else if("items"===e&&o("fixedWidth"))i=Math.floor(pt/(o("fixedWidth")+o("gutter")));else if("autoHeight"===e&&"outer"===kt)i=!0;else if(i=t[e],gt&&yt.indexOf(e)>=0)for(var r=0,s=gt.length;r<s;r++){var l=gt[r];if(!(n>=l))break;e in mt[l]&&(i=mt[l][e])}return"slideBy"===e&&"page"===i&&(i=o("items")),i}function y(e){return q?q+"("+100*e+"% / "+qt+")":100*e/qt+"%"}function N(e,t,n){var i="";if(e){var a=e;t&&(a+=t),i=n?"margin: 0px "+(pt%(n+t)+t)/2+"px":lt?"margin: 0 "+e+"px 0 "+a+"px;":"padding: "+a+"px 0 "+e+"px 0;"}else if(t&&!n){var r="-"+t+"px",o=lt?r+" 0 0":"0 "+r+" 0";i="margin: 0 "+o+";"}return i}function O(e,t,n){return e?(e+t)*qt+"px":q?q+"("+100*qt+"% / "+n+")":100*qt/n+"%"}function P(e,t,n){var i="";if(lt){if(i="width:",e)i+=e+t+"px";else{var a=tt?qt:n;i+=q?q+"(100% / "+a+")":100/a+"%"}i+=ln+";"}return i}function I(e){var t="";if(e!==!1){t=(lt?"padding-":"margin-")+(lt?"right":"bottom")+": "+e+"px;"}return t}function S(e){e=e||R.event,clearTimeout(Ct),Ct=setTimeout(function(){if(st){var t=n();xt!==t&&(xt=t,W(),"outer"===kt&&en.emit("outerResized",Qe(e)))}},100)}function W(){var e=bt,t=Kt,n=Ot,i=sn;if(pt=a(ct),ot=a(ut),gt&&H(),e!==bt||Pt){var r=It,s=Lt,u=Pt,f=Mt,d=At,v=rn;if(Ot=o("items"),Dt=o("slideBy"),rn=o("disable"),sn=!!rn||!!on&&ht<=Ot,Ot!==n&&(Zt=qt-Ot,si()),rn!==v&&$(rn),sn!==i&&(sn&&(Kt=tt?jt:0),L()),e!==bt&&(St=o("speed"),Mt=o("edgePadding"),At=o("gutter"),Pt=o("fixedWidth"),rn||Pt===u||pe(),(Lt=o("autoHeight"))!==s&&(Lt||(ut.style.height=""))),It=!sn&&o("arrowKeys"),It!==r&&(It?D(B,vn):k(B,vn)),mn){var h=Dn,p=kn;Dn=!sn&&o("controls"),kn=o("controlsText"),Dn!==h&&(Dn?E(An):T(An)),kn!==p&&(Cn.innerHTML=kn[0],wn.innerHTML=kn[1])}if(yn){var m=Pn;Pn=!sn&&o("nav"),Pn!==m&&(Pn?(E(In),Ke()):T(In))}if(xn){var y=ti;ti=!sn&&o("touch"),ti!==y&&tt&&(ti?D(ft,hn):k(ft,hn))}if(Tn){var g=ri;ri=!sn&&o("mouseDrag"),ri!==g&&tt&&(ri?D(ft,pn):k(ft,pn))}if(bn){var b=Un,x=Kn,C=Jn,w=Yn;if(sn?Un=Kn=Jn=!1:(Un=o("autoplay"),Un?(Kn=o("autoplayHoverPause"),Jn=o("autoplayResetOnVisibility")):Kn=Jn=!1),Yn=o("autoplayText"),Xn=o("autoplayTimeout"),Un!==b&&(Un?(Qn&&E(Qn),jn||Gn||De()):(Qn&&T(Qn),jn&&ke())),Kn!==x&&(Kn?D(ft,fn):k(ft,fn)),Jn!==C&&(Jn?D(B,dn):k(B,dn)),Qn&&Yn!==w){var A=Un?1:0,M=Qn.innerHTML,S=M.length-w[A].length;M.substring(S)===w[A]&&(Qn.innerHTML=M.substring(0,S)+Yn[A])}}if(!F){if(sn||Mt===f&&At===d||(ut.style.cssText=N(Mt,At,Pt)),tt&<&&(Pt!==u||At!==d||Ot!==n)&&(ft.style.width=O(Pt,At,Ot)),lt&&(Ot!==n||At!==d||Pt!=u)){var W=P(Pt,At,Ot)+I(At);zt.removeRule(c(zt)-1),l(zt,"#"+an+" > .tns-item",W,c(zt))}Pt||Kt!==t||ye(0)}Kt!==t&&(en.emit("indexChanged",Qe()),ye(0),Qt=Kt),Ot!==n&&(ne(),se(),ee(),navigator.msMaxTouchPoints&&re())}lt||rn||(ae(),Ve(),pe()),z(!0),ee()}function H(){bt=0,gt.forEach(function(e,t){xt>=e&&(bt=t+1)})}function L(){var e="tns-transparent";if(sn){if(!Nt){if(Mt&&(ut.style.margin="0px"),jt)for(var t=jt;t--;)tt&&v(vt[t],e),v(vt[qt-t-1],e);Nt=!0}}else if(Nt){if(Mt&&!Pt&&F&&(ut.style.margin=""),jt)for(var t=jt;t--;)tt&&h(vt[t],e),h(vt[qt-t-1],e);Nt=!1}}function z(e){Pt&&Mt&&(sn||pt<=Pt+At?"0px"!==ut.style.margin&&(ut.style.margin="0px"):e&&(ut.style.cssText=N(Mt,At,Pt)))}function $(e){var t=vt.length;if(e){if(zt.disabled=!0,ft.className=ft.className.replace(nn.substring(1),""),x(ft),Ht)for(var n=jt;n--;)tt&&T(vt[n]),T(vt[t-n-1]);if(lt&&tt||x(ut),!tt)for(var i=Kt,a=Kt+ht;i<a;i++){var r=vt[i];x(r),h(r,nt),h(r,rt)}}else{if(zt.disabled=!1,ft.className+=nn,lt||ae(),pe(),Ht)for(var n=jt;n--;)tt&&E(vt[n]),E(vt[t-n-1]);if(!tt)for(var i=Kt,a=Kt+ht;i<a;i++){var r=vt[i],o=i<Kt+Ot?nt:rt;r.style.left=100*(i-Kt)/Ot+"%",v(r,o)}}}function _(){if(Bt&&!rn){var e=Kt,t=Kt+Ot;for(Mt&&(e-=1,t+=1);e<t;e++)[].forEach.call(vt[e].querySelectorAll(".tns-lazy-img"),function(e){var t={};t[Q]=function(e){e.stopPropagation()},D(e,t),d(e,"loaded")||(e.src=m(e,"data-src"),v(e,"loaded"))})}}function ee(){if(Lt&&!rn){for(var e=[],t=Kt,n=Kt+Ot;t<n;t++)[].forEach.call(vt[t].querySelectorAll("img"),function(t){e.push(t)});0===e.length?ie():te(e)}}function te(e){e.forEach(function(t,n){w(t)&&e.splice(n,1)}),0===e.length?ie():setTimeout(function(){te(e)},16)}function ne(){_(),oe(),de(),Ke(),le()}function ie(){for(var e,t=[],n=Kt,i=Kt+Ot;n<i;n++)t.push(vt[n].offsetHeight);e=Math.max.apply(null,t),ut.style.height!==e&&(X&&ve(St),ut.style.height=e+"px")}function ae(){Et=[0];for(var e,t=vt[0].getBoundingClientRect().top,n=1;n<qt;n++)e=vt[n].getBoundingClientRect().top,Et.push(e-t)}function re(){ct.style.msScrollSnapPointsX="snapInterval(0%, "+100/Ot+"%)"}function oe(){for(var e=Kt+Math.min(ht,Ot),t=qt;t--;){var n=vt[t];t>=Kt&&t<e?p(n,"tabindex")&&(g(n,{"aria-hidden":"false"}),b(n,["tabindex"]),v(n,En)):(p(n,"tabindex")||g(n,{"aria-hidden":"true",tabindex:"-1"}),d(n,En)&&h(n,En))}}function se(){if(!tt){for(var e=Kt+Math.min(ht,Ot),t=qt;t--;){var n=vt[t];t>=Kt&&t<e?(v(n,"tns-moving"),n.style.left=100*(t-Kt)/Ot+"%",v(n,nt),h(n,rt)):n.style.left&&(n.style.left="",v(n,rt),h(n,nt)),h(n,it)}setTimeout(function(){[].forEach.call(vt,function(e){h(e,"tns-moving")})},300)}}function le(){if(Pn&&(Ln=Hn!==-1?Hn:Kt%ht,Hn=-1,Ln!==zn)){var e=Mn[zn],t=Mn[Ln];g(e,{tabindex:"-1","aria-selected":"false"}),g(t,{tabindex:"0","aria-selected":"true"}),h(e,Bn),v(t,Bn)}}function ce(e){return"button"===e.nodeName.toLowerCase()}function ue(e){return"true"===e.getAttribute("aria-disabled")}function fe(e,t,n){e?t.disabled=n:t.setAttribute("aria-disabled",n.toString())}function de(){if(Dn&&!Wt&&!Ht){var e=Nn?Cn.disabled:ue(Cn),t=On?wn.disabled:ue(wn),n=Kt===Jt,i=!Wt&&Kt===Zt;n&&!e&&fe(Nn,Cn,!0),!n&&e&&fe(Nn,Cn,!1),i&&!t&&fe(On,wn,!0),!i&&t&&fe(On,wn,!1)}}function ve(e,t){e=e?e/1e3+"s":"",t=t||ft,t.style[X]=e,tt||(t.style[Y]=e),lt||(ut.style[X]=e)}function he(){var e;if(lt)if(Pt)e=-(Pt+At)*Kt+"px";else{var t=U?qt:Ot;e=100*-Kt/t+"%"}else e=-Et[Kt]+"px";return e}function pe(e){e||(e=he()),ft.style[Ut]=Xt+e+Vt}function me(e,t,n,i){for(var a=e,r=e+Ot;a<r;a++){var o=vt[a];i||(o.style.left=100*(a-Kt)/Ot+"%"),X&&ve(St,o),at&&V&&(o.style[V]=o.style[K]=at*(a-e)/1e3+"s"),h(o,t),v(o,n),i&&Rt.push(o)}}function ye(e,t){isNaN(e)&&(e=St),jn&&!C(ft)&&(e=0),X&&ve(e),li(e,t)}function ge(e,t){Ft&&si(),(Kt!==Qt||t)&&(en.emit("indexChanged",Qe()),en.emit("transitionStart",Qe()),jn&&e&&["click","keydown"].indexOf(e.type)>=0&&ke(),$t=!0,ye())}function be(e){return e.toLowerCase().replace(/-/g,"")}function xe(e){if(tt||$t){if(en.emit("transitionEnd",Qe(e)),!tt&&Rt.length>0)for(var t=0;t<Ot;t++){var n=Rt[t];n.style.left="",X&&ve(0,n),at&&V&&(n.style[V]=n.style[K]=""),h(n,it),v(n,rt)}if(!e||!tt&&e.target.parentNode===ft||e.target===ft&&be(e.propertyName)===be(Ut)){if(!Ft){var i=Kt;si(),Kt!==i&&(en.emit("indexChanged",Qe()),X&&ve(0),pe())}ee(),"inner"===kt&&en.emit("innerLoaded",Qe()),$t=!1,zn=Ln,Qt=Kt}}}function Te(e,t){if(!sn)if("prev"===e)Ee(t,-1);else if("next"===e)Ee(t,1);else if(!$t){var n=Kt%ht,i=0;if(n<0&&(n+=ht),"first"===e)i=-n;else if("last"===e)i=ht-Ot-n;else if("number"!=typeof e&&(e=parseInt(e)),!isNaN(e)){var a=e%ht;a<0&&(a+=ht),i=a-n}Kt+=i,Kt%ht!=Qt%ht&&ge(t)}}function Ee(e,t){if(!$t){var n;if(!t){e=e||R.event;for(var i=e.target||e.srcElement;i!==An&&[Cn,wn].indexOf(i)<0;)i=i.parentNode;var a=[Cn,wn].indexOf(i);a>=0&&(n=!0,t=0===a?-1:1)}if(Wt){if(Kt===Jt&&t===-1)return void Te("last",e);if(Kt===Zt&&1===t)return void Te(0,e)}t&&(Kt+=Dt*t,ge(n||e&&"keydown"===e.type?e:null))}}function Ce(e){if(!$t){e=e||R.event;for(var t,n=e.target||e.srcElement;n!==In&&!p(n,"data-nav");)n=n.parentNode;p(n,"data-nav")&&(t=Hn=[].indexOf.call(Mn,n),Te(t,e))}}function we(){Rn=setInterval(function(){Ee(null,Vn)},Xn),jn=!0}function Ne(){clearInterval(Rn),jn=!1}function Oe(e,t){g(Qn,{"data-action":e}),Qn.innerHTML=Zn[0]+e+Zn[1]+t}function De(){we(),Qn&&Oe("stop",Yn[1])}function ke(){Ne(),Qn&&Oe("start",Yn[0])}function Ae(){Un&&!jn&&(De(),Gn=!1)}function Me(){jn&&(ke(),Gn=!0)}function Pe(){jn?(ke(),Gn=!0):(De(),Gn=!1)}function Ie(){B.hidden?jn&&(Ne(),Fn=!0):Fn&&(we(),Fn=!1)}function Se(){jn&&(Ne(),qn=!0)}function We(){qn&&(we(),qn=!1)}function He(e){switch(e=e||R.event,e.keyCode){case j.LEFT:Ee(e,-1);break;case j.RIGHT:Ee(e,1)}}function Le(e){switch(e=e||R.event,e.keyCode){case j.LEFT:case j.UP:case j.PAGEUP:Cn.disabled||Ee(e,-1);break;case j.RIGHT:case j.DOWN:case j.PAGEDOWN:wn.disabled||Ee(e,1);break;case j.HOME:Te(0,e);break;case j.END:Te(ht-1,e)}}function ze(e){e.focus()}function Be(e){function n(e){return t.navContainer?e:Sn[e]}var i=B.activeElement;if(p(i,"data-nav")){e=e||R.event;var a=e.keyCode,r=[].indexOf.call(Mn,i),o=Sn.length,s=Sn.indexOf(r);switch(t.navContainer&&(o=ht,s=r),a){case j.LEFT:case j.PAGEUP:s>0&&ze(Mn[n(s-1)]);break;case j.UP:case j.HOME:s>0&&ze(Mn[n(0)]);break;case j.RIGHT:case j.PAGEDOWN:s<o-1&&ze(Mn[n(s+1)]);break;case j.DOWN:case j.END:s<o-1&&ze(Mn[n(o-1)]);break;case j.ENTER:case j.SPACE:Hn=r,Te(r,e)}}}function Re(){ye(0,ft.scrollLeft()),Qt=Kt}function je(e){return e.target||e.srcElement}function qe(e){return e.type.indexOf("touch")>=0}function Ge(e){e.preventDefault?e.preventDefault():e.returnValue=!1}function Fe(e){if(ai=0,wt=!1,ni=ii=null,!$t){e=e||R.event;var t;qe(e)?(t=e.changedTouches[0],en.emit("touchStart",Qe(e))):(t=e,Ge(e),en.emit("dragStart",Qe(e))),ni=parseInt(t.clientX),ii=parseInt(t.clientY),$n=parseFloat(ft.style[Ut].replace(Xt,"").replace(Vt,""))}}function Ue(e){if(!$t&&null!==ni){e=e||R.event;var n;if(qe(e)?n=e.changedTouches[0]:(n=e,Ge(e)),_n=parseInt(n.clientX)-ni,ei=parseInt(n.clientY)-ii,0===ai&&(ai=f(u(ei,_n),15)===t.axis),ai){qe(e)?en.emit("touchMove",Qe(e)):(oi||(oi=!0),en.emit("dragMove",Qe(e))),wt||(wt=!0);var i=$n;if(lt)if(Pt)i+=_n,i+="px";else{var a=U?_n*Ot*100/(ot*qt):100*_n/ot;i+=a,i+="%"}else i+=ei,i+="px";U&&ve(0),ft.style[Ut]=Xt+i+Vt}}}function Xe(e){if(!$t&&wt){e=e||R.event;var t;qe(e)?(t=e.changedTouches[0],en.emit("touchEnd",Qe(e))):(t=e,en.emit("dragEnd",Qe(e))),_n=parseInt(t.clientX)-ni,ei=parseInt(t.clientY)-ii;var n=Boolean(lt?_n:ei);if(ai=0,wt=!1,ni=ii=null,lt){var i=-_n*Ot/ot;i=_n>0?Math.floor(i):Math.ceil(i),Kt+=i}else{var a=-($n+ei);if(a<=0)Kt=Jt;else if(a>=Et[Et.length-1])Kt=Zt;else{var r=0;do{r++,Kt=ei<0?r+1:r}while(r<qt&&a>=Et[r+1])}}if(ge(e,n),oi){oi=!1;var o=je(e);D(o,{click:function e(t){Ge(t),k(o,{click:e})}})}}}function Ve(){ut.style.height=Et[Kt+Ot]-Et[Kt]+"px"}function Ye(){Sn=[];for(var e=Kt%ht%Ot;e<ht;)!Ht&&e+Ot>ht&&(e=ht-Ot),Sn.push(e),e+=Ot;(Ht&&Sn.length*Ot<ht||!Ht&&Sn[0]>0)&&Sn.unshift(0)}function Ke(){Pn&&!gn&&(Ye(),Sn!==Wn&&([].forEach.call(Mn,function(e,t){Sn.indexOf(t)<0?T(e):E(e)}),Wn=Sn))}function Qe(e){return{container:ft,slideItems:vt,navContainer:In,navItems:Mn,controlsContainer:An,hasControls:mn,prevButton:Cn,nextButton:wn,items:Ot,slideBy:Dt,cloneCount:jt,slideCount:ht,slideCountNew:qt,index:Kt,indexCached:Qt,navCurrentIndex:Ln,navCurrentIndexCached:zn,visibleNavIndexes:Sn,visibleNavIndexesCached:Wn,event:e||{}}}t=e({container:B.querySelector(".slider"),mode:"carousel",axis:"horizontal",items:1,gutter:0,edgePadding:0,fixedWidth:!1,slideBy:1,controls:!0,controlsText:["prev","next"],controlsContainer:!1,nav:!0,navContainer:!1,navAsThumbnails:!1,arrowKeys:!1,speed:300,autoplay:!1,autoplayTimeout:5e3,autoplayDirection:"forward",autoplayText:["start","stop"],autoplayHoverPause:!1,autoplayButton:!1,autoplayButtonOutput:!0,autoplayResetOnVisibility:!0,loop:!0,rewind:!1,autoHeight:!1,responsive:!1,lazyload:!1,touch:!0,mouseDrag:!1,nested:!1,freezable:!0,onInit:!1},t||{}),["container","controlsContainer","navContainer","autoplayButton"].forEach(function(e){"string"==typeof t[e]&&(t[e]=B.querySelector(t[e]))});var Je=R.console&&"function"==typeof R.console.warn;if(!t.container||!t.container.nodeName)return void(Je&&console.warn("Can't find container element."));if(t.container.children.length<2)return void(Je&&console.warn("Slides less than 2."));if(t.responsive){var Ze={},$e=t.responsive;for(var _e in $e){var et=$e[_e];Ze[_e]="number"==typeof et?{items:et}:et}t.responsive=Ze,Ze=null,0 in t.responsive&&(t=e(t,t.responsive[0]),delete t.responsive[0])}var tt="carousel"===t.mode;if(!tt){t.axis="horizontal",t.rewind=!1,t.loop=!0,t.edgePadding=!1;var nt="tns-fadeIn",it="tns-fadeOut",at=!1,rt=t.animateNormal||"tns-normal";Q&&J&&(nt=t.animateIn||nt,it=t.animateOut||it,at=t.animateDelay||at)}var ot,st,lt="horizontal"===t.axis,ct=B.createElement("div"),ut=B.createElement("div"),ft=t.container,dt=ft.parentNode,vt=ft.children,ht=vt.length,pt=a(dt),mt=t.responsive,yt=[],gt=!1,bt=0,xt=n();if(mt){gt=Object.keys(mt).map(function(e){return parseInt(e)}).sort(function(e,t){return e-t}),gt.forEach(function(e){yt=yt.concat(Object.keys(mt[e]))});var Tt=[];yt.forEach(function(e){Tt.indexOf(e)<0&&Tt.push(e)}),yt=Tt,H()}var Et,Ct,wt,Nt,Ot=o("items"),Dt="page"===o("slideBy")?Ot:o("slideBy"),kt=t.nested,At=o("gutter"),Mt=o("edgePadding"),Pt=o("fixedWidth"),It=o("arrowKeys"),St=o("speed"),Wt=t.rewind,Ht=!Wt&&t.loop,Lt=o("autoHeight"),zt=s(),Bt=t.lazyload,Rt=[],jt=Ht?2*ht:0,qt=tt?ht+2*jt:ht+jt,Gt=!(!Pt||Ht||Mt),Ft=!tt||!Ht,Ut=lt?"left":"top",Xt="",Vt="",Yt=o("startIndex"),Kt=Yt?function(e){return e%=ht,e<0&&(e+=ht),e=Math.min(e,qt-Ot)}(Yt):tt?jt:0,Qt=Kt,Jt=0,Zt=qt-Ot,$t=!1,_t=t.onInit,en=new A,tn=ft.id,nn=" tns-slider tns-"+t.mode,an=ft.id||i(),rn=o("disable"),on=t.freezable,sn=!!rn||!!on&&ht<=Ot,ln="inner"===kt?" !important":"",cn={click:Ee,keydown:Le},un={click:Ce,keydown:Be},fn={mouseover:Se,mouseout:We},dn={visibilitychange:Ie},vn={keydown:He},hn={touchstart:Fe,touchmove:Ue,touchend:Xe,touchcancel:Xe},pn={mousedown:Fe,mousemove:Ue,mouseup:Xe,mouseleave:Xe},mn=r("controls"),yn=r("nav"),gn=t.navAsThumbnails,bn=r("autoplay"),xn=r("touch"),Tn=r("mouseDrag"),En="tns-slide-active";if(mn)var Cn,wn,Nn,On,Dn=o("controls"),kn=o("controlsText"),An=t.controlsContainer;if(yn)var Mn,Pn=o("nav"),In=t.navContainer,Sn=[],Wn=Sn,Hn=-1,Ln=Kt%ht,zn=Ln,Bn="tns-nav-active";if(bn)var Rn,jn,qn,Gn,Fn,Un=o("autoplay"),Xn=o("autoplayTimeout"),Vn="forward"===t.autoplayDirection?1:-1,Yn=o("autoplayText"),Kn=o("autoplayHoverPause"),Qn=t.autoplayButton,Jn=o("autoplayResetOnVisibility"),Zn=["<span class='tns-visually-hidden'>"," animation</span>"];if(xn)var $n,_n,ei,ti=o("touch"),ni=null,ii=null,ai=0;if(Tn)var ri=o("mouseDrag"),oi=!1;sn&&(Dn=Pn=ti=ri=It=Un=Kn=Jn=!1),U&&(Ut=U,Xt="translate",Xt+=lt?"X(":"Y(",Vt=")"),function(){ct.appendChild(ut),dt.insertBefore(ct,ft),ut.appendChild(ft),ot=a(ut);var e="tns-outer",n="tns-inner",i=r("gutter");if(tt?lt&&(r("edgePadding")||i&&!t.fixedWidth)?e+=" tns-ovh":n+=" tns-ovh":i&&(e+=" tns-ovh"),ct.className=e,ut.className=n,ut.id=an+"-iw",Lt&&(ut.className+=" tns-ah",ut.style[X]=St/1e3+"s"),""===ft.id&&(ft.id=an),nn+=G?" tns-subpixel":" tns-no-subpixel",nn+=q?" tns-calc":" tns-no-calc",tt&&(nn+=" tns-"+t.axis),ft.className+=nn,tt&&Q){var s={};s[Q]=xe,D(ft,s)}e=n=null;for(var u=0;u<ht;u++){var f=vt[u];f.id||(f.id=an+"-item"+u),v(f,"tns-item"),!tt&&rt&&v(f,rt),g(f,{"aria-hidden":"true",tabindex:"-1"})}if(Ht||Mt){for(var d=B.createDocumentFragment(),p=B.createDocumentFragment(),m=jt;m--;){var x=m%ht,E=vt[x].cloneNode(!0);if(b(E,"id"),p.insertBefore(E,p.firstChild),tt){var C=vt[ht-1-x].cloneNode(!0);b(C,"id"),d.appendChild(C)}}ft.insertBefore(d,ft.firstChild),ft.appendChild(p),vt=ft.children}for(var w=Kt,k=Kt+Math.min(ht,Ot);w<k;w++){var f=vt[w];g(f,{"aria-hidden":"false"}),b(f,["tabindex"]),v(f,En),tt||(f.style.left=100*(w-Kt)/Ot+"%",v(f,nt),h(f,rt))}if(tt&<&&(G?(l(zt,"#"+an+" > .tns-item","font-size:"+R.getComputedStyle(vt[0]).fontSize+";",c(zt)),l(zt,"#"+an,"font-size:0;",c(zt))):[].forEach.call(vt,function(e,t){e.style.marginLeft=y(t)})),F){var A=N(t.edgePadding,t.gutter,t.fixedWidth);l(zt,"#"+an+"-iw",A,c(zt)),tt&<&&(A="width:"+O(t.fixedWidth,t.gutter,t.items),l(zt,"#"+an,A,c(zt))),(lt||t.gutter)&&(A=P(t.fixedWidth,t.gutter,t.items)+I(t.gutter),l(zt,"#"+an+" > .tns-item",A,c(zt)))}else if(ut.style.cssText=N(Mt,At,Pt),tt&<&&(ft.style.width=O(Pt,At,Ot)),lt||At){var A=P(Pt,At,Ot)+I(At);l(zt,"#"+an+" > .tns-item",A,c(zt))}if(lt||rn||(ae(),Ve()),mt&&F&>.forEach(function(e){var t=mt[e],n="",i="",a="",s="",l=o("items",e),c=o("fixedWidth",e),u=o("edgePadding",e),f=o("gutter",e);("edgePadding"in t||"gutter"in t)&&(i="#"+an+"-iw{"+N(u,f,c)+"}"),tt&<&&("fixedWidth"in t||"gutter"in t||"items"in t)&&(a="#"+an+"{width:"+O(c,f,l)+"}"),("fixedWidth"in t||r("fixedWidth")&&"gutter"in t||!tt&&"items"in t)&&(s+=P(c,f,l)),"gutter"in t&&(s+=I(f)),s.length>0&&(s="#"+an+" > .tns-item{"+s+"}"),n=i+a+s,n.length>0&&zt.insertRule("@media (min-width: "+e/16+"em) {"+n+"}",zt.cssRules.length)}),tt&&!rn&&pe(),navigator.msMaxTouchPoints&&(v(ct,"ms-touch"),D(ct,{scroll:Re}),re()),yn){var M=tt?jt:0;if(In)g(In,{"aria-label":"Carousel Pagination"}),Mn=In.children,[].forEach.call(Mn,function(e,t){g(e,{"data-nav":t,tabindex:"-1","aria-selected":"false","aria-controls":vt[M+t].id})});else{for(var H="",j=gn?"":" hidden",w=0;w<ht;w++)H+='<button data-nav="'+w+'" tabindex="-1" aria-selected="false" aria-controls="'+vt[M+w].id+j+'" type="button"></button>';H='<div class="tns-nav" aria-label="Carousel Pagination">'+H+"</div>",ct.insertAdjacentHTML("afterbegin",H),In=ct.querySelector(".tns-nav"),Mn=In.children}if(Ke(),X){var U=X.substring(0,X.length-18).toLowerCase(),A="transition: all "+St/1e3+"s";U&&(A="-"+U+"-"+A),l(zt,"[aria-controls^="+an+"-item]",A,c(zt))}g(Mn[Ln],{tabindex:"0","aria-selected":"true"}),v(Mn[Ln],Bn),D(In,un),Pn||T(In)}if(bn){var V=Un?"stop":"start";Qn?g(Qn,{"data-action":V}):t.autoplayButtonOutput&&(ut.insertAdjacentHTML("beforebegin",'<button data-action="'+V+'" type="button">'+Zn[0]+V+Zn[1]+Yn[0]+"</button>"),Qn=ct.querySelector("[data-action]")),Qn&&D(Qn,{click:Pe}),Un?(De(),Kn&&D(ft,fn),Jn&&D(ft,dn)):Qn&&T(Qn)}mn&&(An?(Cn=An.children[0],wn=An.children[1],g(An,{"aria-label":"Carousel Navigation",tabindex:"0"}),g(Cn,{"data-controls":"prev"}),g(wn,{"data-controls":"next"}),g(An.children,{"aria-controls":an,tabindex:"-1"})):(ct.insertAdjacentHTML("afterbegin",'<div class="tns-controls" aria-label="Carousel Navigation" tabindex="0"><button data-controls="prev" tabindex="-1" aria-controls="'+an+'" type="button">'+kn[0]+'</button><button data-controls="next" tabindex="-1" aria-controls="'+an+'" type="button">'+kn[1]+"</button></div>"),An=ct.querySelector(".tns-controls"),Cn=An.children[0],wn=An.children[1]),Nn=ce(Cn),On=ce(wn),de(),D(An,cn),Dn||T(An)),ti&&D(ft,hn),ri&&D(ft,pn),It&&D(B,vn),"inner"===kt?en.on("outerResized",function(){W(),en.emit("innerLoaded",Qe())}):(D(R,{resize:S}),"outer"===kt&&en.on("innerLoaded",ee)),_(),ee(),L(),z(),en.on("indexChanged",ne),"function"==typeof _t&&_t(Qe()),"inner"===kt&&en.emit("innerLoaded",Qe()),rn&&$(!0),st=!0}();var si=function(){return Ht?function(){var e=Jt,t=Zt;if(tt)if(e+=Dt,t-=Dt,Mt)e+=1,t-=1;else if(Pt){var n=At?At:0;pt%(Pt+n)>n&&(t-=1)}if(Kt>t)for(;Kt>=e+ht;)Kt-=ht;else if(Kt<e)for(;Kt<=t-ht;)Kt+=ht}:function(){Kt=Math.max(Jt,Math.min(Zt,Kt))}}(),li=function(){return tt?function(e,t){t||(t=he()),Gt&&Kt===Zt&&(t=-((Pt+At)*qt-ot)+"px"),X||!e?(pe(t),e&&C(ft)||xe()):M(ft,Ut,Xt,Vt,t,St,xe),lt||Ve()}:function(e){Rt=[];var t={};t[Q]=t[J]=xe,k(vt[Qt],t),D(vt[Kt],t),me(Qt,nt,it,!0),me(Kt,rt,nt),Q&&J&&e||xe()}}();return{getInfo:Qe,events:en,goTo:Te,play:Ae,pause:Me,isOn:st,rebuild:function(){return Z(t)},destroy:function(){if(k(R,{resize:S}),k(B,vn),zt.disabled=!0,Ht)for(var e=jt;e--;)tt&&vt[0].remove(),vt[vt.length-1].remove();var n=["tns-item",En];tt||(n=n.concat("tns-normal",nt));for(var i=ht;i--;){var a=vt[i];a.id.indexOf(an+"-item")>=0&&(a.id=""),n.forEach(function(e){h(a,e)})}if(b(vt,["style","aria-hidden","tabindex"]),vt=an=ht=qt=jt=null,Dn&&(k(An,cn),t.controlsContainer&&(b(An,["aria-label","tabindex"]),b(An.children,["aria-controls","aria-disabled","tabindex"])),An=Cn=wn=null),Pn&&(k(In,un),t.navContainer&&(b(In,["aria-label"]),b(Mn,["aria-selected","aria-controls","tabindex"])),In=Mn=null),Un&&(clearInterval(Rn),Qn&&k(Qn,{click:Pe}),k(ft,fn),k(ft,dn),t.autoplayButton&&b(Qn,["data-action"])),ft.id=tn||"",ft.className=ft.className.replace(nn,""),x(ft),tt&&Q){var r={};r[Q]=xe,k(ft,r)}k(ft,hn),k(ft,pn),dt.insertBefore(ft,ct),ct.remove(),ct=ut=ft=Kt=Qt=Ot=Dt=Ln=zn=mn=Sn=Wn=this.getInfo=this.events=this.goTo=this.play=this.pause=this.destroy=null,this.isOn=st=!1}}};return Z}(); // 2. Build carousel for products from aggregate const sliderMain = tns({ container: '.unique-identifier .my-main-carousel', items: 1, slideBy: 1, controls: true, nav: false, gutter: 10 }); // 3. Synch tabs with carousels function updateTabs(info) { let index, activeSlide; // Update slider for more than 1 product if (info && info.slideItems && info.slideItems.length) { index = info.index; activeSlide = info.slideItems[index]; } else { // Fallback for single product activeSlide = document.querySelector('.unique-identifier .glider-slide[data-product-id]'); } if (activeSlide) { const productId = activeSlide.getAttribute('data-product-id'); const productTabsAll = document.querySelectorAll('.unique-identifier .product-tabs'); const productTabsEl = document.querySelector(`.unique-identifier .product-tabs[data-product-id="${productId}"]`); productTabsAll.forEach(tab => tab.style.display = "none"); if (productTabsEl) { productTabsEl.style.display = "flex"; } } } updateTabs(sliderMain?.getInfo?.()); if (sliderMain?.events?.on) { sliderMain.events.on('indexChanged', updateTabs); } // 4. Init recomendation carousels document.querySelectorAll('.unique-identifier .products-carousel').forEach((carousel, index) => { tns({ container: carousel, items: 3, slideBy: 1, controls: true, center: false, nav: false, gutter: 10, loop: false, responsive: { 0: { items: 2 }, 768: { items: 4 } } }); }); })()
{ "value": { "StoreId": "{{ customer['storeId'] }}", "condition_text": "{{ event.params['body.current.condition.text'] }}", "condition_code": "{{ event.params['body.current.condition.code'] }}", "location_name": "{{ event.params['body.location.name'] }}", "last_updated": "{{ event.params['body.current.last_updated'] }}", "temp_c": "{{ event.params['body.current.temp_c'] }}" }, "itemKey": "{{ customer['storeId'] }}" }


{
"cover": {
"image": "https://example.com/link-to-image/",
"title": "Promo"
}
}



{
"action": "install.app",
"category": "category.automation.event",
"label": "App installed",
"params": {
"app-started": "first"
}
}
In this case the action event is the key - here it will be: install.app - it will be used to prepare the proper segment.
6. Add the **End** node and save your workflow.
## Build a segment
---
Based on event created before, prepare a segment of customers who will see the promotion in the application - people who get the specific event from the automation from previous step during last 30 days.
1. Go to {“poolUuid": “XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX"}. // uuid from the link of your code pool"


{
"bonusType": "location - POS4",
"points": "150"
}


{
"points": "100"
}
3. Click **Apply**.
(function()
{if (document.querySelector('.account-link')){ // collecting information about whether the user is logged in
document.querySelectorAll(".current")[1].addEventListener("click", function () { // condition that the campaign should be displayed after clicking the dropdown with a mass of stone
document.querySelector('.sale').style.display='block'; // event that is sent when the campaign is displayed
SR.event.trackCustomEvent('sale', {
action: "rings_show"
}, 'Secret sale');
});
}
}());
2. Apply appropriate settings that will allow messages to be displayed on selected collections.
3. Attach custom events to measure campaign results.
4. Create analytics that will allow you to check campaign performance.
## Generated events
This use case generates approximately 3 events per profile that completes the flow:
[`dynamicContent.show`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentshow) (~1), `sale` (~1), [`dynamicContent.click`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentclick) (~1).
## Read more
---
- Read more about [custom events](/developers/web/event-tracking#declarative-tracking-custom-events)
- Read more about [dynamic content campaigns](/docs/campaign/dynamiccontent)
# Step discount promotion
Elevate your customers engagement with step discounts, aligning savings with their journey, while strengthening their connection to your brand. Enable them to access higher discounts with each transaction, incentivizing loyalty and fostering more interactions. Transform their shopping experience into a journey towards greater value, connecting them with offers that resonate.
This use case describes how to create a step discount promotion for one product. The customer will receive a discount of 5, 10 and 15% respectively on the first, second and third purchase of the same product. The promotion will work only for loyalty program members (customers logged into the mobile app).
## Prerequisites
---
- Implement promotions in your [mobile application](/developers/mobile-sdk/loyalty), website or through [API](https://hub.synerise.com/api-reference/loyalty-and-engagement#operation/profileLogin)
- [Implement transactional events](/developers/web/methods-reference#tracking-transactions).
- [Import the product feed to a catalog](/use-cases/import-product-feed-to-catalog).
## Process
---
In this use case, you will go through the following steps:
1. [Prepare a segmentation](#prepare-a-segmentation) of customers who are members of the loyalty program.
2. [Create a promotion](#create-a-promotion) with step discount for one product.
## Prepare a segmentation
---
In this part of the process, create a segmentation of customers who are members of the company's loyalty program.
1. Go to

{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Number of people signed in for the videocall: `{% metricsvar metric_id:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX %}{{ metric_result }}{% endmetricsvar %}`\nNumber of people having the videocall today: `{% metricsvar metric_id:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX %}{{ metric_result }}{% endmetricsvar %}`"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "check dashboard",
"emoji": true
},
"url": "https://app.synerise.com/spa/modules/dashboards/analytics/dashboards/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
}
]
}
In the JSON body:
- Insert the ID of the metric you created in the [previous step](/use-cases/slack-integration#create-a-metric-in-synerise).
- Optionally, if you want to refer users to the analytical dashboard, for the `url` parameter, insert the link to the dashboard. If you choose not to, remove the `accessory` object from the JSON body.
3. Confirm by clicking **Apply**.
5. Add the **End** node.
6. Launch the workflow by clicking **Save&Run**.
{
"points": " {% expression %} a7d340ac-906d-49af-83a1-fbe4015bc76f {% endexpression %} ",
}
The result of such action will be such an event:

[ {%- datareference node='Campaigns sent yesterday' maxRows=100 -%}
{%- for r in datareference_result -%}
["{{ r.campaignHash }}", "{{ r.campaignTitle }}", "{{ r.campaignType }}", "{{ r.clickRate }}", "{{ r.showRate }}", "{{ r.sendingTime}}", "{{ r.sendingTimeZone}}", "{{ r.uniqueClickCount }}","{{ r.sendingTime }}", "{{ r.uniqueShowCount }}" "{{ r.uniqueCappingCount }}", "{{ r.uniqueSendCount }}", "{{ r.utm.campaign }}", "{{ r.utm.content }}", "{{ r.utm.medium }}", "{{ r.utm.source }}", "{{ r.utm.term }}"] {%- if not(loop.last) -%},{%- endif -%}
{%- endfor -%}
{%- enddatareference -%} ]
{% set productsSKU = [] %}
{% aggregate aggregate_hash %}
{% for p in aggregate_result %}
{% do productsSKU.append(p) %}
// HERE CAROUSEL ON THE LEFT SIDE
{% endfor %}
{% endaggregate %}
Iterate over the board with recently viewed products, trigger a similar campaign for each of them and create a carousel on the right.
{% for productSKU in productsSKU %}
{% set exampleProd = [] %}
{% set exampleProdUpdt = exampleProd.append(productSKU) %}
{% recommendations2 campaignId=campaign_hash products=exampleProd %}
{% for p in recommended_products2 %}
// HERE CAROUSEL ON THE RIGHT SIDE
{% endfor %} {% endrecommendations2 %} {% endfor %}
## Generated events
This use case generates approximately 7 events per profile that completes the flow:
[`dynamicContent.show`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentshow) (~1), [`dynamicContent.click`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentclick) (~1), [`recommendation.generated`](/docs/assets/events/event-reference/recommendations#recommendationgenerated) (~3), [`recommendation.view`](/docs/assets/events/event-reference/recommendations#recommendationview) (~1), [`recommendation.click`](/docs/assets/events/event-reference/recommendations#recommendationclick) (~1).
## Read more
---
- Read more about [Aggregates](/docs/crm/aggregates)
- Read more about [Jinjava inserts](/developers/inserts/insert-usage) (aggregates and recommendations)
- Read more about [Loops in Jinjava](/developers/inserts/tag)
- Read more about [Product feed preparation](/developers/product-feed)
# Switch Brands in Product Recommendations with Custom Filters
In order to diversify or monetize positions in the recommendation frame, you can use the recommendation slots. By combining using slots and recommendation filters, you can create branding campaigns. Such campaigns can be later displayed on the product page.
In this example, we will set a campaign that will return items similar to the one that is the context.
However, when the context item's brand is “BrandA”, the first two items in the recommendation frame will be from the “BrandB” brand. This can be done to better position items from a brand that you have a partnership with, as the first two positions in a recommendation frame have the highest click-through rate. To make this possible, we will utilize slots.
The first slot will contain two items and no filter will be applied, unless the context item’s brand is “BrandA”, then they will be filtered to items that are from the “BrandB” brand.
The second slot, consisting of four items, won't have filters and will display similar items to the context one.

<div class="normal-price snrs-hide">{{r.priceValue}}</div>
<div class="club-price snrs-hide">{{r.clubValue}}</div>
In this case we add two price values.
- **CSS:**
.snrs-hide{
display:none;
}
- **JS:**
var typePrice = 'club'; //
Here we collect information about the price from Data Layer or cookies.
if(typePrice ==='club'){
document.querySelector('.'+typePrice+'-price').classList.remove('snrs-hide');
}
Here the appropriate price variant is discovered.
## Generated events
This use case generates approximately 5 events per profile that completes the flow:
[`dynamicContent.show`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentshow) (~1), [`dynamicContent.click`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentclick) (~1), [`recommendation.generated`](/docs/assets/events/event-reference/recommendations#recommendationgenerated) (~1), [`recommendation.view`](/docs/assets/events/event-reference/recommendations#recommendationview) (~1), [`recommendation.click`](/docs/assets/events/event-reference/recommendations#recommendationclick) (~1).
## Read more
---
- Read more about [AI](/docs/ai-hub/recommendations-v2/recommendation-types#personalized)
- Read more about [dynamic content](/docs/campaign/dynamiccontent)
- Read more about [recommendation types](/docs/ai-hub/recommendations-v2)
# Promote discounted items to customers at risk of churn
If your customers are at risk of churn, you can increase the probability of discounted items appearing in their recommendations. Thanks to this, they are more likely to make a purchase.
This can be done by using **recommendation boosting** to promote discounted items. Boosting rules are built using the same editor as the filters, but unlike filtering, boosting does not entirely exclude items that do not meet the conditions - it only tells the AI model to assign more weight to the discount value parameter when calculating the final recommendation score (relevance to a particular customer) of an item.
## Prerequisites
---
1. **Recommended**: Become familiar with [creating recommendations](/docs/ai-hub/recommendations-v2/creating-recommendation-campaign) and [recommendation filters](/docs/ai-hub/recommendations-v2/recommendation-filters) (using the IQL query editor).
This article does not explain every step of creating a recommendation in detail.
1. The item feed must contain information about discounts.
In this example, the discount is defined as a percentage.
2. Predict churn for a group of customers.
The [Predict churn](/use-cases/churn-prediction) use case includes detailed instructions.
3. Create a segmentation of customers for whom the `score-label` parameter in the `snr.prediction.score` event from the prediction was `high` or `very high`.
This segment of customers will be needed to set up the boosting conditions.
[Learn more about creating segmentations](/docs/analytics/segmentations/creating-segmentations).
## Creating the recommendation
---
### Choose source, type, and add slots
1. Go to 

(function () {
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = function () {
return callback(window.Adform);
};
document.head.append(script);
}
function checkAdform(instance) {
if (!instance || !instance._uid) {
console.log('no uid')
} else {
var oldUid = "{% if 'adFormID' in customer|string %}{{ customer['adFormID'] }}{% else %}brak{% endif %}";
if (oldUid === 'brak' || oldUid.indexOf(instance._uid) === -1) {
SR.event.trackCustomEvent('adformid.save', {
newId: instance._uid.toString(),
});
console.log('adform uid sent -> ' + instance._uid);
}
}
}
loadScript('https://track.adform.net/serving/cookie/?adfaction=getjs;adfcookname=uid', checkAdform)
})();
3. To continue the process of configuring the dynamic content campaign, click **Next**.
9. Click **Apply**.
10. In the **Schedule** section, click **Define**.
1. As the **Display time** choose **Display immediately**.
2. Click **Apply**.
2. In the **Display Settings** section, click **Define**.
1. Specify circumstances for dynamic content to be displayed and Advanced options, according to your business needs.
2. Click **Apply**.
3. Optionally, you can define the UTM parameters and additional parameters for your dynamic content campaign.
4. Click **Activate**.
**Result:** `adformid.save` event is generated, it will contain the `adFormID` parameter with the ID value.
## Match ID Cookie on Mobile
---
The cookie matching is based on the Adveritising ID, which is a fixed ID for a given device issued by Google.
Currently, Advertising ID is not collected through our SDK. You must implement such tracking in the application. You can do this by sending a custom event.
{
"label": "custom.event",
"action": "adformid.save,
"client": {
"uuid": "xxx", // uuid from mobile app
},
"params": {
"newId": "ADVERTISEMENT_ID"
"source": "MOBILE_APP"
}
}

{% set currentValue = customer['adFormID'] %}{% set newVal = event.params.newId|string %}{% set currentValues = currentValue|split('|') %}{% do currentValues.append(newVal) %}{% set finalValues = [] %}{% if currentValue|length > 255 %}{% for x in currentValues %}{% if loop.index0 > 0 %}{% do finalValues.append(x) %}{% endif %}{% endfor %}{% else %}{% if newVal in currentValue %}{% catalog.kill(it) %}{% else %}{% set finalValues = currentValues %}{% endif %}{% endif %}{{ finalValues|join('|') }}



{% set ids = customer['adFormID']|split('|') %}
[
{% for x in ids %}
{"adformID2": "{{ x }}" ,
"OwnerID": "SyneriseDemo_female"
}
{% if loop.index != ids | count %},{% endif %}
{% endfor %}

[
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "{% set products = [] %}{% recommendations3 campaignId=VrEXZm00A7CK %}{% for p in recommended_products3 %}{% do products.append(p) %}{% endfor %}{% endrecommendations3 %}{{ products[0].title }}"
},
{
"type": "text", "text": "{{ products[0].price }}"
}
]
},
{
"type": "button",
"index": "0",
"sub_type": "url",
"parameters": [
{
"type": "url",
"text": "{{ products[0].title|replace('https://yourshop.com', '') }}"
}
]
},
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "{{ products[0].image_link }}"
}
}
]
}
]
9. Click **Apply**.
The following table explains all the inserts used in the body, button and header sections shown above.
| Section | Insert value | Insert explanation |
|--------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| body | `{% set products = [] %}{% recommendations3 campaignId=VrEXZm00A7CK %}{% for p in recommended_products3 %}{% do products.append(p) %}{% endfor %}{% endrecommendations3 %}{{ products[0].title }}` | The value of this insert is used as the value of `{{1}}`, so the name of recommended product can be displayed in the message. |
| body | `{{ products[0].price }}` | The value of this insert is used as the value of `{{2}} PLN`, to show the price of the item. |
| button | `{{ products[0].title\|replace('https://yourshop.com', '') }}` | The value of this insert is used to return product url. Here we specify the URL omitting the domain, because we define the domain in the Meta platform, as you can see in the screenshot with the [button creation](#create-a-message-template-in-the-meta-portal). |
| header | `{{ products[0].image_link }}` | The value of this insert is used to return the product image in the header. |
### Add the finishing node
---
1. Add the **End** node to the **Send Template Message** node.
2. To the **Matched** path, add the **End** node.
3. In the upper right corner, click **Save & Run**.
The dispatcher is essentially a piece of software that intelligently manages content distribution. It uses the DC template, which is like sets of rules, to determine which content to display to different users.
Behind the scenes, this dispatcher employs JavaScript. JavaScript code within the template dynamically updates the webpage based on predefined user allocations. This means that when someone visits the webpage, the dispatcher instantly decides which version of content they should see, whether it's 'A' or 'B,' according to the rules set in the DC template.
Moreover, it attaches a special 'data-test-snrs' attribute to each user's experience. This attribute acts as a marker, helping us later identify which version of content each user was exposed to. This kind of tracking is valuable for understanding how different messages perform and for tailoring future interactions based on users' previous experiences.
{
"campaign": "abandoned cart - mail",
"group": "D - Control group",
"type": "simulation of sending an e-mail"
}
#### Make sure that the event has been generated
---
This filter serves as a security measure to ensure that the **campaign.event** event was generated in the previous step.
1. Add the **Event Filter** node. In the configuration of the node:
1. Set **Check** to **for period of time**.
2. Define the time range to **10 minute**.
3. From the **Choose event** dropdown list, select the **campaign.event** event.
4. As the event parameter, select **type**.
5. From the **Choose operator** dropdown list, select **Equal (String)**.
6. As the value, enter `simulation of sending an e-mail`. The provided value must be the same as the one defined in the event body.
2. Confirm by clicking **Apply**.
{
"campaign": "abandoned cart - mail",
"group": "D - Control group",
"type": "trigger"
}
Add the **End** node to complete the path related to the customers from the control group that enter the workflow for the first time.
In the next steps, we will address the second part of the path related to the control group - the path intended for customers who have reentered the workflow.
### Customers from the control group who re-enter the workflow
---
In this part of the process, we will go through the path designed for customers reentering the workflow. The described part of the workflow is shown in the screenshot below.
{
"campaign": "abandoned cart - mail",
"group": "A",
"orderID": "{% aggregate 4c1a5eff-edae-33e9-89c2-191bb460ef46 %} {{ aggregate_result[0] }} {% endaggregate %}",
"type": "revenue prod"
}
5. Confirm by clicking **Apply**.
{
"campaign": "abandoned cart - mail",
"group": "D - Control group",
"type": "simulation of sending an e-mail follow up"
}
(function () { const metricPrevious30 = +`{% metrics %} 2f11b5f7-ddc0-4ea1-a1c2-628dd2de0d29 {% endmetrics %}`; const metricLast30 = +`{% metrics %} d798b895-19ce-44b0-b54c-06996f7898e5 {% endmetrics %}`; const imageLink = `{{ metric_additional_params["og:image"] }}`; // For example metricPrevious30 is 100, and metricLast30 is 130. That gives 30% const percent = ((metricLast30 - metricPrevious30) / metricPrevious30) * 100; // if percent is below limit or one of metric gives 0 stop the script if (Number.isNaN(percent) || percent < +`#### type: "number", id: "Percent", defaultValue: "30" !####`) return; const wrapperEl = document.querySelector(".snr-sp__main-wrapper"); const counterEl = wrapperEl.querySelector(".snr-sp__counter"); const imgEl = document.querySelector(".snr-sp__image img"); const peopleEl = document.querySelector(".snr-sp__people"); const showLabel = () => wrapperEl.classList.remove("snr-sp__hidden"); const hideLabel = () => wrapperEl.classList.add("snr-sp__hidden"); const getPeople = (num) => (num > 1 || num == 0 ? "people" : "person"); wrapperEl.addEventListener("click", hideLabel); counterEl.innerText = metricLast30 + ""; imgEl.src = imageLink; peopleEl.innerText = getPeople(metricLast30); setTimeout(() => { showLabel(); setTimeout(hideLabel, +`#### type: "number", id: "Display time in ms", defaultValue: "5000" !####`); }, +`#### type: "number", id: "Delay time in ms", defaultValue: "2000" !####`); })();

{"firstname": "test",
"lastname": "test",
"email": "user@example.com",
"newsletterAgreement": "true"
}
[
{
"email": "{{ request.body.email }}",
"agreements": {
"email": {{ request.body.newsletterAgreement }}
}
}
]
{
"points": "{{ event.params['$quantity']*100}}",
"promo": "BrandPromo"
}


<div class="in-app-wrapper"> <div class="in-app-wrapper-inner"> <div class="in-app__upper"> <div class="in-app-close"></div> <p class="in-app-title"> Hello {% customer firstname %}</p> <p class="in-app__upper--text">Your current loyalty <br> points balance is <br><strong>{% expression %} 2d4c0862-31ac-43c7-a991-97a1e5455e8a {% endexpression %} points.</strong></p> </div> <div class="in-app__lower"> <p class="in-app__lower--text changeDate"></p> <p class="in-app__lower--text">Enjoy your shopping!</p> </div> </div> </div>.in-app-wrapper * { font-family: sans-serif; } .in-app-wrapper { text-align: center; position: relative; background: #00000045; box-shadow: 0 30px 80px 0 rgba(35, 41, 54, 0.2); width: auto; height: 100vh; display: flex; flex-direction: column; flex-wrap: nowrap; align-content: center; justify-content: center; align-items: center; } .in-app-title { font-size: 20px; font-weight: normal; font-stretch: normal; font-style: normal; line-height: 1.5; letter-spacing: -0.67px; text-align: center; color: white; margin: 0; padding-top: 20px; } .in-app-subtitle { font-size: 16px; font-weight: normal; font-stretch: normal; font-style: normal; line-height: 1.43; letter-spacing: -0.47px; text-align: center; color: #13171e; } .in-app-close { position: absolute; z-index: 1; width: 50px; height: 50px; border: 0; top: 10px; right: 10px; cursor: pointer; background-color: transparent; } .in-app-close:after, .in-app-close:before { content: ''; position: absolute; height: 2px; width: 50%; top: 50%; left: 12px; margin-top: -1px; background: white; } .in-app-close:after { -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); height: 2px; margin-top: -2px; } .in-app-close:before { -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); height: 2px; margin-top: -2px; } .in-app-wrapper-inner { background: #fff; border-radius: 6px; position: relative; border-radius: 20px; margin-left: 20px; margin-right: 20px; height: 300px; width: -webkit-fill-available } .in-app__upper { background-color: #0b68ff; border-radius: 4% 4% 50% 50% / 4% 4% 10% 10%; height: 170px; } .in-app__lower { height: 200px; } .in-app__upper--text { font-size: 20px; color: white; word-spacing: 2px; } .in-app__lower--text { font-size: 17px; color: black; word-spacing: 2px; }(function () { var closeButton = document.querySelector(".in-app-close"); closeButton.addEventListener("click", function () { SRInApp.close(); SRInApp.trackCustomEvent( "inapp.custom", { action: "close", }, "Custom event from In-App message" ); }); var modalBackground = document.querySelector(".in-app-wrapper"); modalBackground.addEventListener("click", function (event) { if (event.target.getAttribute("class") == "in-app-wrapper") { SRInApp.close(); SRInApp.trackCustomEvent( "inapp.custom", { action: "click outside modal", }, "Custom event from In-App message" ); } }); var oneDay = 24 * 60 * 60 * 1000; var firstDate = new Date(); var endOfYear = new Date(2022, 12, 31); var diffDays = Math.round(Math.abs((firstDate - endOfYear) / oneDay)); var insertDate = document.querySelector(".changeDate"); if (diffDays > 30) { insertDate.innerHTML = "You still have <strong>" + diffDays + " days</strong> to <br> use your points before <br> they expire."; } else { insertDate.innerHTML = "You have only <strong>" + diffDays + " days</strong> to <br> use your points before <br> they expire!"; } })();

{
"score": "{% expression %} rfm-score-expression {% endexpression %}",
"rfmSegmentName": "{% expression %} rfm-segment-name-expression {% endexpression %}"
}
- Replace `rfm-score-expression` with the ID of [the expression that calculates the RFM score](/use-cases/rfm-analysis#create-an-expression).
- Replace `rfm-segment-name-expression` with ID of the expression you created in [this part of the process](/use-cases/google-analytics-integration#create-an-expression-that-returns-rfm-segment-name).
7. Confirm by clicking **Apply**.


(function(){ var availableMetrics = []; //Adding metric prepared in point 1.1 var firstMetricsVal = "{% socialproof %} xxx {% endsocialproof %}"; //Adding metric prepared in point 1.2 var secondMetricsVal = "{% socialproof %} xxx {% endsocialproof %}"; //Adding variant text for 1st metric one for person var textFirstMetricOnePerson = 'person is watching this product now.'; //Adding variant text for 1st metric for few people var textFirstMetricPeople = 'people are watching this product now.'; //Adding variant text for 2nd metric for person var textSecondMetricOnePerson = 'sold in the last 1 hour.'; //Adding variant text for 2nd metric for few people var textSecondMetricPeople = 'sold in the last 1 hour.'; function handleCounterText(val, variantA, variantB) { if (val.length === 1) { let finalVariant = getWordVariant(+val, variantA, variantB); return `${val} ${finalVariant}`; } else { let lastNumber = val.slice(-1); let finalVariant = getWordVariant(+lastNumber, variantA, variantB); return `${val} ${finalVariant}`; } } function getWordVariant(num, variantA, variantB) { if (num == 1) { return variantA; } else { return variantB; } } var firstMetrics = `
`; var secondMetrics = `
`; if (firstMetricsVal >= 1) { availableMetrics.push(firstMetrics); } if (secondMetricsVal >= 1) { availableMetrics.push(secondMetrics); } var wrapper = document.querySelector('#synerise-social-proof-place'); wrapper.style.position = "fixed"; var availableMetricsCounter = 0; setInterval(function() { if (document.querySelector('#synerise-social-proof') !== null&&document.querySelector('#synerise-social-proof>p')){ wrapper.removeChild(document.querySelector('#synerise-social-proof')); }; wrapper.insertAdjacentHTML('beforeend', availableMetrics[availableMetricsCounter]); if (availableMetrics.length === 1) { return; } if (availableMetricsCounter >= availableMetrics.length - 1) { availableMetricsCounter = 0; } else { availableMetricsCounter++; } }, 3000); })()(function(){ //Adding metric prepared in point 1.1 var firstMetricsVal = "{% socialproof %} xxx {% endsocialproof %}"; //Adding metric prepared in point 1.2 var secondMetricsVal = "{% socialproof %} xxx {% endsocialproof %}"; //Adding variant text for 1st metric one for person var textFirstMetricOnePerson = 'person is watching this product now.'; //Adding variant text for 1st metric for few people var textFirstMetricPeople = 'people are watching this product now.'; //Adding variant text for 2nd metric for person var textSecondMetricOnePerson = 'sold in the last 1 hour.'; //Adding variant text for 2nd metric for few people var textSecondMetricPeople = 'sold in the last 1 hour.'; function handleCounterText(val, variantA, variantB) { if (val.length === 1) { let finalVariant = getWordVariant(+val, variantA, variantB); return `${val} ${finalVariant}`; } else { let lastNumber = val.slice(-1); let finalVariant = getWordVariant(+lastNumber, variantA, variantB); return `${val} ${finalVariant}`; } } function getWordVariant(num, variantA, variantB) { if (num == 1) { return variantA; } else { return variantB; } } var firstMetrics = `
`; var wrapper = document.querySelector('#synerise-social-proof-place'); wrapper.style.position = "fixed"; if(firstMetricsVal >=1||secondMetricsVal >= 1){ if (document.querySelector('#synerise-social-proof') !== null){ wrapper.removeChild(document.querySelector('#synerise-social-proof')); }; wrapper.insertAdjacentHTML('beforeend',firstMetrics ); } })()
{ "analysis": { "title": "bestsellers [\"mobiles_tablets > mobiles\"] 30D", "description": "", "filter": { "matching": true, "expressions": [], "expression": { "name": "", "type": "EMPTY" } }, "reportMetrics": [ { "metricId": "8e03bdb3-408a-4a3b-bc96-4e5392a08735", "dateFilter": { "type": "RELATIVE", "duration": { "type": "DAYS", "value": 30 }, "offset": { "type": "DAYS", "value": 0 } }, "comparison": { "dateFilter": { "type": "ABSOLUTE" } }, "action": { "id": 71248, "name": "product.buy" }, "format": { "dataFormat": "numeric", "useSeparator": true, "compactNumbers": false, "fixedLength": 1 }, "grouping": { "type": "TOP", "top": 100 }, "groups": [ { "title": "$sku", "type": "EVENT", "format": { "dataFormat": "numeric", "useSeparator": true, "compactNumbers": false, "fixedLength": 1 }, "attribute": { "type": "PARAM", "param": "$sku", "id": "parameters:551610043" } } ], "eventName": "product.buy" } ] }, "allowNull": true }
{% set id = 0 %} {% set batch = [] %} {% for entry in event.params['body.data[0].values[0]'] %} {% set id = id|add(1) %} {% set item = {"itemKey":id|string,"value":{"no":id|string,"sku":entry.name[0], "transactions":entry.value}} %} {% do batch.append(item) %} {% endfor %} {{batch|tojson}}
{% set positions = range(1,101) %} {% set bestsellersMobileList = [] %} {% for position in positions %} {% catalogitem.bestsellersMobile(position) %} {% do bestsellersMobileList.append(catalog_result.sku) %} {% endcatalogitem %} {% endfor %} var bestsellersMobileSKUs = {{bestsellersMobileList|tojson}}; var bestsellersMobileHTML = `Bestseller
`; let bestsellersMobileURL = location.href; document.body.addEventListener('click', () => { requestAnimationFrame(() => { setTimeout(() => { bestsellersMobileURL !== location.href && showBestsellersMobile(); bestsellersMobileURL = location.href; }, 1000) }); }, true); function showBestsellersMobile() { var elements = document.querySelectorAll("a[data-sku]"); for (var i = 0; i < elements.length; i++) { if (bestsellersMobileSKUs.includes(elements[i].getAttribute("data-sku"))) { elements[i].insertAdjacentHTML('afterend', bestsellersMobileHTML); elements[i].addEventListener("click", function() { SR.event.trackCustomEvent( "dynamicContent.click", { "eventLabel": "Dynamic content click", "campaignName": "Label", "sku": this.getAttribute("data-sku"), "title": this.getAttribute("title"), "category": this.getAttribute("data-category") }, "Dynamic content click" ); }); } } } showBestsellersMobile();
{
"time": "x",
"label": "x",
"action": "x",
"client": {
"uuid": "xxx-xxx-xxx"
},
"params": {
"temp1": "x",
"temp2": "x",
"temp3": "x",
"temp4": "x"
}
}
2. When our devices are configured and they are sending data to this URL we can start to create an automation.
3. Automations allow us to convert received data into events on our device cards.
- We use business events as a trigger, which means that our automation will be triggered every time our end point receives some data. So, when one of our devices sends any data, our automation will be triggered.
- Then it will send outgoing webhooks. And here in the body of our outgoing webhook, we use Jinjava in order to get information about the values of these particular parameters.
#### Case 1: Making smart alerts about temperature deviation problems
1. Go to **Automation Hub > Workflow > New workflow.**
2. Start with the **Profile Event** trigger and in the settings of the node, select the **custom.temp** event. This automation is triggered every time this event appears on the profile card.
3. Choose **Update Profile** action. Update the device status to online because when the device is sending data, that means it's online.
4. Choose **Profile Filter**. In this step we check if the average of the last 4 measurements was between 10 and 15 degrees. Because let's assume that this is the temperature that we want to have.
5. If **Profile Filter** is **matched** add **Update Profile** node. In settings define that it should updating a temperature status into “in scale” and then the automation is ending with **End node**.
6. But when our average temperature falls below 10 degrees or moves above 15 degrees, our automation will go with a different path. If **Profile Filter** is **not matched** add some **Delay node**.
7. Next, add another **Profile Filter** in which we check if the last temperature status was “in scale”. We do it for the purpose of capping.
catalogIndexItems
storeItemType
storeIds
visibilityStatus
name
redeemQuantityPerActivation
discountValue
expireAt
description
catalogItemType
priority
discountTypediscountType
displayTo
displayFrom
redeemLimitPerClient
type
headline
startAt
storeCatalog
catalog
headerName
redeemType
If these are personalized promotions, you can set the type parameter to Personalized promotion permanently for this use case.
Read more about those parameters here.
If you wish to generate promotions across different channels, such as on receipts in addition to the app, you can configure this within the campaign using tags. For example: Promotions marked with tag CHECKOUT will display on receipts. Promotions marked with MOBILE will appear in the app. Ensure these tags are already defined in the csv file during the preparation process as `tags.0.name`. Important: The tag must first be created in Data Modeling Hub -> Tags.



Replace `your_campaign_ID` with the ID of the AI recommendation (either of the bestselling shoes suitable for asphalt or off-road). The ID of the AI campaign is contained in the URL of the recommendation.
<div class="title-and-link"> <h2>Personalized offers for you!</h2> <a href="https://app.synerise.com/ai-v2/recommendations/bWdoIUSmSfLO" target="_blank" class="border-bottom">Go to Platform</a> </div> <div id="recommendationsForYou" class="block widget block-products-list grid products-grid__layout-default products-grid__buttons-below secondary-top-right product-columns-4 product-columns-l-4 product-columns-m-2 product-columns-s-2 title--align-center quickview-bottom-left "> <div class="block-content"> <div class="products-grid grid"> <ol class="product-items widget-product-grid"> {% recommendations3 campaignId=CjmCJ4X4RfRL %} {% for p in recommended_products3 %} <li class="product-item"> <div class="product-item-info"> <div class="product-grid__image-wrapper"> <a href="{{p.productUrl}}" class="product-item-photo"> <span class="product-image-container product-image-container-5600" style="width: 489px;"> <span class="product-image-wrapper" style="padding-bottom: 100%;"> <img class="product-image-photo" src="{{p.image}}" data-original="{{p.image}}" loading="lazy" width="489" height="489" alt="{{p.name}}"></span> </span> </a> </div> </div> <div class="product-grid-overlay"></div> <div class="product-item-details"> <div class="ox-product-grid__categories"><a href="https://demoshop.synerise.com/men/watches.html" class="ox-product-grid__category-link">{{p.category.split('>')[3]}}</a></div> <strong class="product-item-name"> <a title="{{p.name}}" href="{{p.productUrl}}" class="product-item-link"> {{p.name}}</a> </strong> <div class="price-box price-final_price" data-role="priceBox" data-product-id="5600" data-price-box="product-id-5600"> <span class="price-container price-final_price tax weee"> <span id="old-price-5600-widget-product-grid" data-price-amount="619" data-price-type="finalPrice" class="price-wrapper "><span class="price">${{p.price|float(2)}}</span></span> </span> </div> </div> </div> </li> {% endfor %} {% endrecommendations3 %} </ol> </div> </div> </div>



<!-- candidates -->
{% set NEWSLETTER = {"name": "NEWSLETTER", "expression":0, "metric":0} %}
{% set MOBILE_PUSH = {"name": "MOBILE_PUSH", "expression":0, "metric":0}%}
{% set CANDIDATES = [NEWSLETTER, MOBILE_PUSH]%}
<!-- value assignment -->
{% expressionvar 37368423-6210-4ee4-bd67-5e2143544576 %}{% do
NEWSLETTER.update({"expression": expression_result})%}{%endexpressionvar%}
{metricsvar metric_id:550060f2-4feb-a6a5-46e8ad01b8ac%}{%do
NEWSLETTER.update({"metric_result})%}{% endmetricsvar %}
{% expressionvar 6e3206d9-708a-4fc5-bfe4-88fea18da90f %}{% do
MOBILE_PUSH.update({"expression_result})%}{% endexpressionvar %}
{% metricsvar metric_id:85651a3a-5163-4b1f-971a-4109e83bf753 %}{% do
MOBILE_PUSH.update ({"metric":metric_result})%}{% endmetricsvar %}
<!-- candidates by expression-->
{set expression_winners = [] %}
{% for candidat in CANDIDATES %}
{% if candidat["expression"] > candidat ["metric"] %}
{% do expression_winners.append(candidat)%}
{% endif %}
{% endfor %}
<!-- print winner-->
{% if expression_winners|count !=0 %}
{% set winner = expression_winners|sort(true, true, 'expression')|first %}
{% else %}
{% set winner = CANDIDATES|random %}
{% endif %}
{{winner}}
Everything here is based on jinjava, which produced the event at the very end of this calculation. Based on that we can actually select the segment in which we want to send the message automatically.
## Generated events
This use case does not generate any events.
## Read more
---
- [Aggregates](/docs/crm/aggregates/introduction-to-aggregates)
- [Automation Hub](/docs/automation)
- [Basic optimizer configuration](/use-cases/campaign-optimizer)
- [Build segmentation](/docs/analytics/segmentations/creating-segmentations)
- [Expressions](/docs/crm/expressions)
- [Metrics](/docs/analytics/metrics)
# Gravity Form integration
If you use the Gravity Form plugin for building custom forms in WordPress, you can use webhooks to send data from forms to Synerise. In this use case, the answers from the form are saved as the event parameters on a customer's profile.
curl --location --request POST 'ENDPOINT_URL_FROM_STEP_3' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "john.doe@example.com",
"formType": "formTitle",
"VehicleCondition": "neutral",
}'
5. In the **Incoming data** section in Synerise, click **Retrieve data**. The system waits for the incoming request for 1 minute and 30 seconds.
## Create a workflow
---
In this part of the process, you create a workflow in Synerise. The workflow starts with the business event trigger that is launched every time a customer submits the form. In response to that, on the profile of the customer who submitted the form, an event is saved with the answers in the survey in the form of parameters.
1. Go to **Automation Hub > Workflows > New workflow**.
2. Enter the name of the workflow.
3. As the first node, select **Business Event**. In the configuration of the node:
1. Select the incoming webhook you created as the first part of this use case.
2. Confirm by clicking **Apply**.
{
"action": "form.submit",
"label": "Customer submitted a survey",
"client": {
"email": "{{request.body.email}}"
},
"params": {
"formTitle": "{{request.body.formTitle}}",
"VehicleCondition": "{{request.body.VehicleCondition}}"
}
}
{% vouchervar id=uuid_of_voucher_pool %}
{% barcode code= {{voucher_result}}, gray=true, type=barcode_type, hrp=BOTTOM %}
{% endvouchervar %}














{
"itemKey": "{{client.phone}}",
"value": {
"id": "{{client.id }}",
"phone": "{{client.phone}}"
}
}
7. As the authorization method, select **by API key**.




<div class="in-app-wrapper bottom_bar"> <div class="in-app-close"> <svg viewBox="0 0 24 24" class="close-m"> <path fill="none" d="M0 0h24v24H0z"></path> <path d="M13.06 12l4.72-4.72a.75.75 0 00-1.06-1.06L12 10.94 7.28 6.22a.75.75 0 00-1.06 1.06L10.94 12l-4.72 4.72a.75.75 0 101.06 1.06L12 13.06l4.72 4.72a.75.75 0 001.06-1.06z"> </path> </svg> </div> <div class="in-app-content"> <p class="in-app-title">Do you like our products?</p> <p class="in-app-subtitle">Let us know your taste 🔥</p> </div> </div>@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&display=swap"); .in-app-wrapper.bottom_bar { margin: 15px; background-color: whitesmoke; height: 95px; border-radius: 30px; box-shadow: 0 0px 10px 0 rgba(35, 41, 54, 0.2); display: flex; flex-direction: column; flex-wrap: nowrap; align-content: center; justify-content: center; align-items: center; } .in-app-close { cursor: pointer; position: absolute; padding: 10px; top: 15px; right: 15px; } .in-app-close svg { width: 32px; height: 32px; } .in-app-content { text-align: center; } .in-app-title { margin: 0; font-weight: 600; font-size: 20px; margin-bottom: 2px; font-family: "Nunito", sans-serif; } .in-app-subtitle { margin: 0; font-size: 14px; font-family: "Nunito", sans-serif; }(function () { var inApp = document.querySelector('.in-app-wrapper.bottom_bar'); var close = inApp.querySelector('.in-app-close'); close.addEventListener('click', function () { SRInApp.close(); }) var content = inApp.querySelector('.in-app-content'); content.addEventListener('click', function () { SRInApp.close(); setTimeout(function() { SRInApp.trackCustomEvent('preferences.trigger', {}, 'Preferences trigger event'); }, 0) }) })();
<div class="in-app-close" style="width: 100%;text-align: right;width: 90%;"> <svg viewBox="0 0 24 24" class="close-m"> <path fill="none" d="M0 0h24v24H0z"></path> <path d="M13.06 12l4.72-4.72a.75.75 0 00-1.06-1.06L12 10.94 7.28 6.22a.75.75 0 00-1.06 1.06L10.94 12l-4.72 4.72a.75.75 0 101.06 1.06L12 13.06l4.72 4.72a.75.75 0 001.06-1.06z"> </path> </svg> </div> <div class="frame"></div> <div class="icons"> <svg id="hate" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.07 127.89"> <path class="a" d="M128.07,64.07c-.5,36.31-28,63.57-64,63.82S-.17,99.33,0,63.29C.17,28.25,29.23-.3,64.43,0A63.88,63.88,0,0,1,128.07,64.07ZM45.32,38.54c-2.44.36-4.63,1.12-6,3.68a6.39,6.39,0,0,0,.94,7.83A143,143,0,0,0,50.42,60.36c2.73,2.48,3.44,4.31.2,7a98.44,98.44,0,0,0-9.52,9.53c-3.62,4-3.66,7.48-.47,10.59,2.82,2.76,7.12,2.54,10.7-.79,3.05-2.83,5.91-5.86,8.85-8.8,2.58-2.57,5.16-2.53,7.73,0,2.83,2.81,5.62,5.67,8.52,8.42,3.87,3.68,8.08,4.08,11,1.15,3.23-3.21,3-6.85-.83-11C83.57,73.21,80.44,70,77.1,67c-2.37-2.13-2.71-3.65-.13-5.91,3.24-2.85,6.15-6.08,9.2-9.15,4.17-4.2,4.66-8,1.45-11.34-2.93-3-7.58-2.61-11.49,1.19-3.34,3.25-6.66,6.52-9.85,9.91-1.64,1.74-2.85,1.73-4.49,0-3.32-3.5-6.84-6.81-10.21-10.26A9,9,0,0,0,45.32,38.54Z" /> <path d="M45.32,38.54a9,9,0,0,1,6.26,2.87c3.37,3.45,6.89,6.76,10.21,10.26,1.64,1.73,2.85,1.74,4.49,0,3.19-3.39,6.51-6.66,9.85-9.91C80,38,84.69,37.52,87.62,40.57c3.21,3.34,2.72,7.14-1.45,11.34-3,3.07-6,6.3-9.2,9.15-2.58,2.26-2.24,3.78.13,5.91,3.34,3,6.47,6.24,9.53,9.52,3.87,4.16,4.06,7.8.83,11-2.95,2.93-7.16,2.53-11-1.15-2.9-2.75-5.69-5.61-8.52-8.42-2.57-2.54-5.15-2.58-7.73,0-2.94,2.94-5.8,6-8.85,8.8-3.58,3.33-7.88,3.55-10.7.79-3.19-3.11-3.15-6.6.47-10.59a98.44,98.44,0,0,1,9.52-9.53c3.24-2.72,2.53-4.55-.2-7A143,143,0,0,1,40.28,50.05a6.39,6.39,0,0,1-.94-7.83C40.69,39.66,42.88,38.9,45.32,38.54Z" /> </svg> <svg id="like" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.06 127.99"> <path class="a" d="M128.06,63.83a63.65,63.65,0,0,1-64,64.16A63.57,63.57,0,0,1,0,64a64,64,0,0,1,128.06-.13ZM96,56.53c0-5.82-3.9-13.3-10.19-16.05-6.9-3-13.67-2.67-19.37,2.82-2,1.9-3.16,1.41-4.93-.17-2.34-2.08-4.86-3.89-8.25-4.24-9.13-.92-15.31,2.3-19.11,10.25-3.89,8.11-2.42,17.27,4,23.34,7.5,7,15.22,13.88,22.77,20.89,2.06,1.92,3.76,2.27,6,.21C74.36,86.7,82,80,89.39,73.09,93.57,69.21,96.06,64.45,96,56.53Z" /> <path d="M96,56.53c.08,7.92-2.41,12.68-6.59,16.56C82,80,74.36,86.7,66.93,93.58c-2.23,2.06-3.93,1.71-6-.21-7.55-7-15.27-13.84-22.77-20.89-6.46-6.07-7.93-15.23-4-23.34,3.8-8,10-11.17,19.11-10.25,3.39.35,5.91,2.16,8.25,4.24,1.77,1.58,2.95,2.07,4.93.17,5.7-5.49,12.47-5.84,19.37-2.82C92.08,43.23,96,50.71,96,56.53Z" /> </svg> </div>@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&display=swap"); html, body { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; overflow: hidden; font-family: "Nunito", sans-serif; background-color: rgba(0, 0, 0, 0.4); } .frame { position: relative; width: 90%; height: 70%; max-width: 400px; max-height: 600px; z-index: 1; } .icons { margin-top: 3vh; user-select: none; z-index: 1; } .icons>svg { width: 10vh; height: 10vh; max-width: 60px; max-height: 60px; border-radius: 50%; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); cursor: pointer; } .icons>svg:nth-child(1) { fill: #fb4f68; margin-right: 2vh; } .icons>svg:nth-child(2) { fill: #4dca93; } .icons>svg>path:nth-child(1) { fill: #fff; } .card { background-color: #fff; position: absolute; display: flex; align-items: flex-end; justify-content: center; width: 100%; height: 100%; color: #f1f1f1; border-radius: 10px; user-select: none; cursor: pointer; overflow: hidden; background-size: cover; background-repeat: no-repeat; background-position: center; touch-action: none; } .card .is-like { width: 100%; height: 100%; position: absolute; opacity: 0; } .card .is-like::after { position: absolute; left: 50%; bottom: 30%; transform: translateX(-50%) rotate(-10deg); width: 30%; height: 13%; font-size: 3em; letter-spacing: 0.2em; font-weight: 600; border-radius: 0.15em; display: flex; align-items: center; justify-content: center; } .card .like::after { content: "👍"; } .card .nope::after { content: "👎"; } .card .bottom { width: 100%; height: 25%; background: linear-gradient(to top, #000000b8, #00000000); display: flex; flex-direction: column; align-items: flex-start; justify-content: center; padding-left: 7%; font-weight: 400; padding-bottom: 25px } .card .bottom .title>span:nth-child(1) { font-size: 2em; font-weight: 700; margin-right: 0.2em; } .card .bottom .title>span:nth-child(2) { font-size: clamp(13px, 1.5em, 25px); } .card .bottom .title>span:nth-child(2)>b { font-size: 0.6em; margin-right: 0.2em; } .card .bottom .info { margin: 0; } @media screen and (max-height: 540px) { .frame { width: 90%; height: 70%; font-size: 13px; } } @media screen and (max-height: 440px) { .frame { font-size: 8px; } } .in-app-close svg { width: 32px; height: 32px; cursor: pointer; background-color: #000; fill: #fff; } .in-app-close { max-width: 400px; margin-bottom: 16px; }let imgCount = 0; const data = {% set arr = [] %}{% recommendations3 campaignId=CAMPAIGN-ID %}{% for p in recommended_products3 %}{% set obj = {itemId: p.itemId, img: p.image_link, name: p.title, price: p.price, distance: p.category} %}{% do arr.append(obj) %}{% endfor %}{{arr|tojson}}{% endrecommendations3 %} const frame = document.body.querySelector(".frame"); data.forEach((_data) => appendCard(_data)); let current = frame.querySelector(".card:last-child"); let likeText = current.children[0]; let startX = 0, startY = 0, moveX = 0, moveY = 0; document.querySelector("#like").onclick = () => { moveX = 1; moveY = 0; complete('like'); }; document.querySelector("#hate").onclick = () => { moveX = -1; moveY = 0; complete('hate'); }; function appendCard(data) { const firstCard = frame.children[0]; const newCard = document.createElement("div"); newCard.className = "card"; newCard.setAttribute('data-sku', data.itemId); newCard.setAttribute('data-brand', data.name); newCard.setAttribute('data-title', data.distance); newCard.style.backgroundImage = `url(${data.img})`; newCard.innerHTML = `
LIKE
${data.name}
${data.price}zł
${data.distance}
`; if (firstCard) frame.insertBefore(newCard, firstCard); else frame.appendChild(newCard); imgCount++; } function initCard(card) { card.addEventListener("pointerdown", onPointerDown); } function setTransform(x, y, deg, duration) { current.style.transform = `translate3d(${x}px, ${y}px, 0) rotate(${deg}deg)`; likeText.style.opacity = Math.abs((x / innerWidth) * 2.1); likeText.className = `is-like ${x > 0 ? "like" : "nope"}`; if (duration) current.style.transition = `transform ${duration}ms`; } function onPointerDown({ clientX, clientY }) { startX = clientX; startY = clientY; current.addEventListener("pointermove", onPointerMove); current.addEventListener("pointerup", onPointerUp); current.addEventListener("pointerleave", onPointerUp); } function onPointerMove({ clientX, clientY }) { moveX = clientX - startX; moveY = clientY - startY; setTransform(moveX, moveY, (moveX / innerWidth) * 50); } function onPointerUp() { current.removeEventListener("pointermove", onPointerMove); current.removeEventListener("pointerup", onPointerUp); current.removeEventListener("pointerleave", onPointerUp); if (Math.abs(moveX) > frame.clientWidth / 2) { current.removeEventListener("pointerdown", onPointerDown); complete(String(moveX).match('-') ? 'hate' : 'like'); } else cancel(); } function complete(type) { try { const flyX = (Math.abs(moveX) / moveX) * innerWidth * 1.3; const flyY = (moveY / moveX) * flyX; setTransform(flyX, flyY, (flyX / innerWidth) * 50, innerWidth); SRInApp.trackCustomEvent('preferences.action', { actionType: type, itemId: current.getAttribute('data-sku'), title: current.getAttribute('data-title'), brand: current.getAttribute('data-brand') }, 'Preferences action'); const prev = current; const next = current.previousElementSibling; if (next) initCard(next); current = next; likeText = current.children[0]; // appendCard(data[imgCount % 4]); setTimeout(() => frame.removeChild(prev), innerWidth); } catch(error) { document.querySelector('body').style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; document.querySelector('body').innerHTML = '<h1 style="color: #fff; width: 90%; max-width: 400px;">Thank you!<br>We will reach out to you</h1>'; setTimeout(function(){ SRInApp.close(); }, 3000) } } function cancel() { setTransform(0, 0, 0, 100); setTimeout(() => (current.style.transition = ""), 100); } var close = document.querySelector('.in-app-close svg'); close.addEventListener('click', function () { SRInApp.close(); }) initCard(current);
To create an aggregate with product rejected by the customer, duplicate the aggregate created above and change the value of the parameter to `hate`.
Go to Data Modeling Hub > Catalogs.
On the list find the catalog in which you want to create the filter.
On the right side of the screen, click 
Click Define.
On the pop-up, define the conditions by clicking Choose filter. The list contains all parameters from the product feed.
Name and save the filter by clicking Save filter.
curl --location --request POST 'https://{SYNERISE_API_BASE_PATH}/automation-endpoint/endpoints/END_ID/trigger' \
--header 'Content-Type: application/json' \
--data-raw '{
"firstname": "test",
"lastname": "test",
"email": "test"
}'
6. When the endpoint receives data from the request, verify the list of variables. If the variables include those which are in the payload (in this case, firstname, lastname, and email), click **Define**. If not, click **Start again** re-send request, and wait for the results.
{
"email": "{{ request.body.email }}",
"firstname": "{{ request.body.firstname }}",
"lastname": "{{ request.body.lastname }}"
}
5. In the **Authorization** section, select **By API key**.
6. From the dropdown list, select API key that has permissions to create or update customer's data.


*
This report will return the names of most frequently bought products and its values.
This report will return the names of most frequently bought products and its values by its categories.
This report will return the names of most frequently bought products by categories and its values in a specific month.
This report will return the names of most frequently bought products by categories and its values in a specific month.
This report will return the names of most frequently visited products and its values.
This report will return the names of most frequently visited products and its values by categories.
{ "top": "{% set skus = [] %} {% set counter = [] %} {% set skusTmp = [] %} {% set skuDict = [] %} {% set sortDict = [] %} {% aggregate AGGREGATE_HASH %} {% for sku in aggregate_result|unique %}{% set k = [] %}{% for tmp in aggregate_result %}{% if sku == tmp %}{% do k.append('+') %}{% endif %}{% endfor %}{% do skuDict.append({'sku':sku,'counter':k|length }) %}{% endfor %}{% endaggregate %}{% for dictItem in skuDict %}{% do counter.append(dictItem.counter)%}{% endfor %}{% set sortCounter = counter|sort() %}{% for count in sortCounter %}{% for dictItem in skuDict %}{% if dictItem.counter == count && dictItem.sku in skusTmp == false %}{% do skusTmp.append(dictItem.sku) %}{% do sortDict.append(dictItem) %}{% endif %}{% endfor %}{% endfor %}{% set top = [] %}{% set len = sortDict|length + 1 %} {% for i in range(len) %}{% set ind = sortDict|length - i %}{% set item = sortDict[ind] %}{% if sortDict[ind].counter > 1 %}{% do top.append(sortDict[ind].sku) %}{% endif %}{% endfor %}{{ top|join(',')|trim }}" }
{% set topProducts = event.params.top %}
{% set skus = topProducts|split(',') %}
Additionally - to add the most visited products , you will have to use Jinjava and add your own CSS. If you want to display all objects that are under the key products, follow the instruction:
<!-- Opening the tag that retrieves the value from the aggregate prepared in point 1 -->
{% aggregate XXXXXXXXXXX %}
<!-- In the section {% for r in aggregate_result %} ... {% endfor %} there is access to product:retailer_part_no of the 10 most visited products -->
{% for r in aggregate_result %}
next item:
{{r}}
<!-- r is a single product:retailer_part_no and can be used as the key: {% catalog.Snrs-produktu-ogTag(r).og:XXX %} downloads additional data from a catalog built on the basis of og tags - which information about the product you want to add in the template depends on you. If you want to add the product name, photo, link, price in the e-mail - you can take it from the catalog -->
{% catalog.Snrs-produktu-ogTag(r).og:image %}
{% catalog.Snrs-produktu-ogTag(r).og:title %}
{% catalog.Snrs-produktu-ogTag(r).product:price:amount %}
{% catalog.Snrs-produktu-ogTag(r).og:url %}
{% endfor %}
<!-- Closing of the tag that gets the value from the aggregate prepared in point 1 -->
{% endaggregate %}


Check the transaction level! It decreased for more thank 50% comparing to the average from last 30 days. The metric result is: {% metricsvar metric_id:78d31e68-d886-4fcb-9ca3-b2136e74be01 %}{{ metric_result }}{% endmetricsvar %}%.
curl --request POST --url https://api.synerise.com/uauth/v2/auth/login/profile --header 'content-type: application/json' --data '{"apiKey":"64c09614-1b2a-42f7-804d-f647243eb1ab"}'curl --request GET --url 'https://api.synerise.com/analytics/%7Bnamespace%7D/segmentations?page=SOME_NUMBER_VALUE&limit=SOME_NUMBER_VALUE&search=SOME_STRING_VALUE&sortBy=SOME_STRING_VALUE' --header 'Authorization: Bearer _YOUR_JWT_TOKEN_'curl --request POST --url https://api.synerise.com/analytics/%7Bnamespace%7D/segmentations/check/by/%7BidentifierType%7D --header 'Authorization: Bearer _YOUR_JWT_TOKEN_' --header 'content-type: application/json' --data '{"identifierValue":"string","segmentationIds":["a8b2ed5c-c342-436f-a98e-eca642767926"]}'function renderPersonalizedMessage(props) {
const segmentations = props.segmentations;
return (
<div>
<h1>Hello!</h1>
{segmentations.includes("a8b2ed5c-c342-436f-a98e-eca642767926") &&
<h2>Discover our new offer!.</h2>}
</div>
);
}
**Below you will find a sequence diagram to help you better understand the communication flow between e-point CMS and Synerise.**
{% voucher assign=false %} pool-uuid {% endvoucher %}
`{{ automationPathSteps['voucherCheck'].event.params.voucherCode }}`
Note that `voucherCheck` is the name of the **Event Filter** trigger added manually.
{"code":"{{ automationPathSteps['transactionCode'].event.params.discountCode }}"}
Note that `transactionCode` is the name of the **Profile Event** trigger added manually.
In this Jinjava we reference to code that has been used. Because this code was assigned in the first workflow to the user who shares it, the voucherCode.redeemed event (which is the result of using this API request) is assigned to the profile of the person who shares the code. Such configuration is crucial for the [third workflow](#create-third-workflow) which sends out reward discount code which is based on the `voucherCode.redeemed` event.
3. Confirm by clicking **Apply**.

{% expressionvar 2bf51517-ae79-4bfb-bd37-153408c80bf5 %} {% set state = expression_result %} {% if state == 'AOV < 200 PLN' %} Buy discounted products from the online shop with an extra 10% discount! You can find your personal code below <p> {% voucher assign=false %} f017803f-4d5c-4192-aa3a-f845822490cb {% endvoucher %} {% elif state == 'AOV >= 200 PLN' %} Buy new collection products from the online shop with a 20% discount! You can find your personal code below. <p> {% voucher assign=false %} 8464b593-478c-412a-9dfb-0aab59f8ff5e {% endvoucher %} {% endif %} {% endexpressionvar %}
{
"sku":"
{% set signedProducts = [] %}
{% set receivedProducts = [] %}
{% set finalSku = [] %}
{% aggregate LAST-SKU-SIGNED %} {# aggregate collecting product sku #}
{% for sku in aggregate_result|reverse %} {# saving sku from aggregate to signedProducts variable #}
{% do signedProducts.append(sku) %}
{% endfor %}
{% endaggregate %}
{% aggregate LAST-SKU-RECEIVED %} {# aggregate collecting product sku sended to user #}
{% for sku in aggregate_result|reverse %} {# saving sku from aggregate to receivedProducts variable #}
{% do receivedProducts.append(sku) %}
{% endfor %}
{% endaggregate %}
{% for sku in signedProducts %}
{% catalogvar.catalog(sku).availability %}
{% if catalog_result == 'in stock' %} {# condition to check if products are availability#}
{% if sku in receivedProducts|join(',') %} {# condition that adds a comma to items #}
{% else %}
{% do finalSku.append(sku) %}
{% endif%}
{% endif %}
{% endcatalogvar %}
{% endfor %}
{% set counter = 0 %}
{% for sku in finalSku %} {# listing the final sku in the sku parameter #}
{% set counter = counter + 1 %}
{{sku}}
{% if counter < finalSku|length%},{% endif %}{%endfor%}"
}
As a result is a `product.backInStock` event which in the `sku` parameter contains the SKUS of products that are back in stock. The list of the products will be sent in the email, separated by commas. If the event is empty, it means that none of the products have met the back in stock conditions.
An example of a generated event:
{% set sku_array = event.params.sku %} {% set sku_from_array = sku_array|split(',') %} <ul> {% for sku in sku_from_array %} <li data-snr-ai-product-id="{% catalog.Snrs-produktu-ogTag(sku).product:retailer_part_no %}"> <a class="snrs-AI--item-link" href="{% catalog.Snrs-produktu-ogTag(sku).og:url %}" title="{% catalog.Snrs-produktu-ogTag(sku).og:title %}"> <img src="{% catalog.Snrs-produktu-ogTag(sku).og:image %}" class="products-slider__item-image snrAI-product-image snrAI-product-image-{% catalog.Snrs-produktu-ogTag(sku).og:image %}" width="90" alt="{% catalog.Snrs-produktu-ogTag(sku).og:title %}" id="snrAI-image-{% catalog.Snrs-produktu-ogTag(sku).product:retailer_part_no %}"> <h3 class="snrs-AI-product--product-name"> <span class="snrs-AI-product--name-first">{% catalog.Snrs-produktu-ogTag(sku).og:title %}</span> </h3> </a> </li> {% endfor %} </ul>
[
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "{% aggregate 17d214c4-5644-33b1-b0c6-9fab96b26b3e %}{% for item in aggregate_result %}{% catalog.store-1(item.sku).name %}{% endfor %}{% endaggregate %}"
},
{
"type": "text",
"text": "{% customer firstname %}"
}
]
},
{
"type": "button",
"index": "0",
"sub_type": "url",
"parameters": [
{
"type": "url",
"text": "{% aggregate 17d214c4-5644-33b1-b0c6-9fab96b26b3e %}{% for item in aggregate_result %}{% catalogvar.store-1(item).url %}{{ catalog_result|replace('https://yourshop.com', '') }}{% endcatalogvar %}{% endfor %}{% endaggregate %}"
}
]
},
{
"type": "header",
"parameters":
[
{
"type": "image",
"image":
{
"link": "{% aggregate 17d214c4-5644-33b1-b0c6-9fab96b26b3e %}{% for item in aggregate_result %}{% catalogvar.store-1(item).image %}{{ catalog_result }}{% endcatalogvar %}{% endfor %}{% endaggregate %}"
}
}
]
}
]
9. Click **Apply**.
The following table explains all the inserts used in the body, button and header sections shown above.
| Section | Insert value | Insert explanation |
|--------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| body | `{% aggregate 17d214c4-5644-33b1-b0c6-9fab96b26b3e %}{% for item in aggregate_result %}{% catalogvar.store-1(item).image %}{{ catalog_result }}{% endcatalogvar %}{% endfor %}{% endaggregate %}` | The value of this insert is used as the value of `{{1}}` to return a product that the customer left in the cart and did not purchase within the estimated time period. |
| body | `{% customer firstname %}` | The value of this insert is used as the value of `{{2}}`, so the name of a customer can be displayed in the message. |
| button | `{% aggregate 17d214c4-5644-33b1-b0c6-9fab96b26b3e %}{% for item in aggregate_result %}{% catalogvar.store-1(item).url %}{{ catalog_result\|replace('https://yourshop.com', '') }}{% endcatalogvar %}{% endfor %}{% endaggregate %}` | The value of this insert is used to return product url. Here we specify the URL omitting the domain, because we define the domain in the Meta platform, as you can see in the screenshot with the [button creation](#create-a-message-template-in-the-meta-portal). |
| header | `{% aggregate 17d214c4-5644-33b1-b0c6-9fab96b26b3e %}{% for item in aggregate_result %}{% catalogvar.store-1(item).image %}{{ catalog_result }}{% endcatalogvar %}{% endfor %}{% endaggregate %}` | The value of this insert is used to return the url to the product image in the header. |
### Add the finishing node
---
1. Add the **End** node to the **Send Template Message** node.
2. To the **Matched** path, add the **End** node.
3. In the upper right corner, click **Save & Run**.
## Check the use case set up on the Synerise Demo workspace
---
You can check the [aggregate](https://app.synerise.com/analytics/aggregates/17d214c4-5644-33b1-b0c6-9fab96b26b3e) and [workflow](https://app.synerise.com/automations/automation-diagram/7ea1d166-3125-4f70-ae45-659e2bfef5a3) configuration directly in Synerise Demo workspace.
If you’re our partner or client, you already have automatic access to the **Synerise Demo workspace (1590)**, where you can explore all the configured elements of this use case and copy them to your workspace.
If you’re not a partner or client yet, we encourage you to fill out the contact [form](https://demo.synerise.com/request) to schedule a meeting with our representatives. They’ll be happy to show you how our demo works and discuss how you can apply this use case in your business.
## Generated events
This use case generates approximately 7 events per profile that completes the flow:
[`product.addToCart`](/docs/assets/events/event-reference/items#productaddtocart) (~1), [`automation.clientStartPath`](/docs/assets/events/event-reference/automation#automationclientstartpath) (~1), [`automation.clientPathStep`](/docs/assets/events/event-reference/automation#automationclientpathstep) (~3), [`automation.clientEndPath`](/docs/assets/events/event-reference/automation#automationclientendpath) (~1), [`whatsapp.sendTemplateMessage`](/docs/assets/events/event-reference/integration#whatsappsendtemplatemessage) (~1).
## Read more
---
- [Aggregates](/docs/crm/aggregates)
- [Automation Hub](/docs/automation)
- [Jinjava inserts](/developers/inserts)
- [WhatsApp Send Template Message node](/docs/automation/integration/whats-app/send-template-message)
# In-app QR Code Scanner
In today’s retail environment, the boundaries between online and offline experiences are increasingly blurred. Mobile applications can bridge this gap by providing interactive tools that connect the physical store with digital personalization. One such solution is an **in-app QR code scanner** that directs customers to individualized promotions and recommendations based on their unique profile.
By enabling users to scan QR codes placed in physical locations—such as shelves, displays, or posters—brands can drive engagement, gather valuable behavioral data, and deliver a seamless omnichannel experience. Each scan automatically appends the user’s UUID (unique identifier) to the destination link, ensuring that the landing page content is fully personalized to that individual.
This use case demonstrates how to create an interactive in-app campaign featuring a built-in QR scanner that redirects customers to a personalized landing page with AI-driven recommendations. Using a **predefined template**, the setup process is quick and does not require building the scanner interface from scratch.
You will use a predefined template with the resize option, which appears permanently in the application as a top or bottom bar, providing easy access to the QR code scanner and simplifying its implementation.
The campaign targets users who interact with a brand in physical locations by scanning an in-store QR code to unlock exclusive discounts for the promoted brand and get product suggestions tailored to their preferences.
After scanning the code, the users are directed to a landing page, where they will find the personalized recommendations from 3 specific, promoted brands, costing more than 50$.
What is more, you can use this scanner in different scenarios by **letting the user scan QR codes in various placements** - in stores, on street banners, in magazines etc. Each time, you deliver personalized content and recommendations, avoiding sending the same content to everyone.




client.email,orderId,paymentInfo.method,products.finalUnitPrice.amount,products.quantity,products.sku,recordedAt,revenue.amount,value.amount,products.name test@test.pl,1234,cash,24,2,10000,2022-05-24T14:15:22Z,50,50,shoes test@test.pl,1234,cash,26,2,20000,2022-05-24T14:15:22Z,50,50,dress










SR.event.trackCustomEvent { "label": "The customer returned product",
"client": { "email": "testDoc@synerise.com" },
"action": "product.return",
"params": { "product_sku": "xxx", "orderId": "xxx",
"reason_of_return": "shoes were too small" } }
## Process
---
1. Create a [target segmentation](/use-cases/predicting-returns#create-a-target-segmentation).
2. Create a [source segmentation](/use-cases/predicting-returns#create-a-source-segmentation).
3. [Create a prediction](/use-cases/predicting-returns#create-a-prediction).
## Create a source segmentation
---
In this part of the process, create a source segmentation that contains customers who will be compared with the customers in the target segmentation. This segmentation includes customers who have made at least two returns in the last 30 days. The fastest way to make such a segmentation is to create a `product.return` event funnel.
1. Go to


{% set fieldsVar = ["code", "params", "name", "status", "images", "params", "description", "headline", "tags"] %}
{% promotions fields=fieldsVar %}
{% set count = 0 %} {%- for i in promotions_result -%}
{%- if count < noOfItems && i.tags|selectattr("name", "equalto", "home")|length > 0 -%}
{% set fieldsVar = ["code", "params", "name", "status", "images", "params", "description", "headline"] %}
{% promotions fields=fieldsVar %}
{% set count = 0 %} {%- for i in promotions_result -%} {%- if count < noOfItems -%}

<!--[if mso ]><style>sup, sub { font-size: 100% !important; } sup { mso-text-raise:10% } sub { mso-text-raise:-10% }</style> <![endif]-->
<!-- Start Gmail Promo Tab annotations code -->
<div itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="#### type: "string", groupName: "Promotion tag", groupId: "promotion-tag", defaultValue: "Synerise Demo Shop", id: "promotion-tag__company-name", label: "Company name" !####" />
<meta itemprop="logo" content="#### type: "string", groupName: "Promotion tag", groupId: "promotion-tag", defaultValue: "https://demoshop.synerise.com/media/athlete2/Demos/ca/logo-ca.png", label: "Company image", id: "promotion-tag__img-link" !####" />
</div>
<div itemscope itemtype="http://schema.org/EmailMessage">
<meta itemprop="subjectLine" content="#### type: "string", groupName: "Promotion tag", groupId: "promotion-tag", defaultValue: "Free shipping", label: "Subject line", id: "promotion-tag__subject-line" !####" />
</div>
<div itemscope itemtype="http://schema.org/DiscountOffer">
<meta itemprop="description" content="#### type: "string", groupName: "Promotion tag", groupId: "promotion-tag", defaultValue: "Free shiping", label: "Description", id: "promotion-tag__description" !####" />
<meta itemprop="discountCode" content="#### type: "string", groupName: "Promotion tag", groupId: "promotion-tag", id: "promotion-tag__code", label: "Code", defaultValue: "WELCOME20" !####" />
<meta itemprop="availabilityStarts" content="#### type: "string", groupName: "Promotion tag", groupId: "promotion-tag", defaultValue: "2024-10-15T00:00:00+00:00", label: "Start date", id: "promotion-tag__start-date" !####" />
<meta itemprop="availabilityEnds" content="#### type: "string", groupName: "Promotion tag", groupId: "promotion-tag", defaultValue: "2024-10-17T23:59:59+00:00", label: "End date", id: "promotion-tag__end-date" !####" />
</div>
<div itemscope itemtype="http://schema.org/PromotionCard">
<meta itemprop="image" content="FILL BANNER IMG" />
</div>
<!-- End Gmail Promo Tab annotations code -->
This will add the following fields in the **Config** section:
Go to Data Modeling Hub > Catalogs.
On the list, find the catalog in which you want to create the filter.
On the right side of the screen, click 
Click Define.
On the pop-up, define the conditions by clicking Choose filter. The list contains all parameters from the product feed. In our case category should contain WOMEN.
Name and save the filter by clicking Save filter.

The expression creates the following logic:
<div class="in-app-wrapper"> <div class="in-app-wrapper-inner"> <div class="in-app__upper"> <div class="in-app-close"></div> <p class="in-app-title"> Hello {% customer firstname %}</p> <p class="in-app__upper--text">We've sent you an email with the promo code.</strong></p> </div> <div class="in-app__lower"> <p class="in-app__lower--text changeDate">20% discount!</p> <p class="in-app__lower--text--second">You haven't used it yet,</br>maybe you can redeem it now?</p> </div> </div> </div>.in-app-wrapper * { font-family: sans-serif; } .in-app-wrapper { text-align: center; position: relative; background: #00000045; box-shadow: 0 30px 80px 0 rgba(35, 41, 54, 0.2); width: auto; height: 100vh; display: flex; flex-direction: column; flex-wrap: nowrap; align-content: center; justify-content: center; align-items: center; } .in-app-title { font-size: 20px; font-weight: normal; font-stretch: normal; font-style: normal; line-height: 1.5; letter-spacing: -0.67px; text-align: center; color: white; margin: 0; padding-top: 20px; } .in-app-subtitle { font-size: 16px; font-weight: normal; font-stretch: normal; font-style: normal; line-height: 1.43; letter-spacing: -0.47px; text-align: center; color: #13171e; } .in-app-close { position: absolute; z-index: 1; width: 50px; height: 50px; border: 0; top: 10px; right: 10px; cursor: pointer; background-color: transparent; } .in-app-close:after, .in-app-close:before { content: ''; position: absolute; height: 2px; width: 50%; top: 50%; left: 12px; margin-top: -1px; background: white; } .in-app-close:after { -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); height: 2px; margin-top: -2px; } .in-app-close:before { -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); height: 2px; margin-top: -2px; } .in-app-wrapper-inner { background: #fff; border-radius: 6px; position: relative; border-radius: 20px; margin-left: 20px; margin-right: 20px; width: 300px; } .in-app__upper { background-color: #0b68ff; border-radius: 4% 4% 50% 50% / 4% 4% 15% 15%; height: 170px; } .in-app__lower { height: 200px; } .in-app__upper--text { font-size: 20px; color: white; word-spacing: 2px; } .in-app__lower--text { font-size: 17px; color: black; padding-top:20px; } .in-app__lower--text--second{ font-size: 17px; color: black; margin-top:50px; }(function () { var closeButton = document.querySelector(".in-app-close"); closeButton.addEventListener("click", function () { SRInApp.close(); SRInApp.trackCustomEvent( "inapp.custom", { action: "close", }, "Custom event from In-App message" ); }); var modalBackground = document.querySelector(".in-app-wrapper"); modalBackground.addEventListener("click", function (event) { if (event.target.getAttribute("class") == "in-app-wrapper") { SRInApp.close(); SRInApp.trackCustomEvent( "inapp.custom", { action: "click outside modal", }, "Custom event from In-App message" ); } }); })();
const QUESTIONS = [
{
"type": "single",
"question": "What motivated you to download our app?",
"answers": [
"To browse products",
"To make a purchase",
"To explore exclusive offers",
"To compare prices",
{ "answer": "Other (please specify):", "options": { "isOpen": true, "limit": 20 } },
],
"shuffleAnswers": false,
"required": true
},
{
"type": "multi",
"question": "What types of products or services are you most interested in?",
"answers": [
"Fashion",
"Electronics",
"Home and Kitchen",
"Health and Beauty",
"Sports and Outdoors",
{ "answer": "Other (please specify):", "options": { "isOpen": true, "limit": 20 } }
],
"shuffleAnswers": false,
"required": true
},
{
"type": "single",
"question": "How do you prefer to shop?",
"answers": [
"I like to browse and explore new products",
"I prefer targeted recommendations based on my preferences",
"I usually know what I want and search directly",
"I’m mainly looking for deals and offers",
],
"shuffleAnswers": false,
"required": true
},
{
"type": "multi",
"question": "What factors influence your purchasing decisions the most?",
"answers": [
"Product quality",
"Price",
"Brand reputation",
"Customer reviews",
"Sustainability",
{ "answer": "Other (please specify):", "options": { "isOpen": true, "limit": 20 } }
],
"shuffleAnswers": false,
"required": true
},
{
"type": "scale",
"question": "How likely would you recommend our company to your friends and known ones?",
"length": 10,
"required": true,
},
];
5. After customising your survey, save the template.
### Select events that trigger the in-app message display
---
In this part of the process, you will define the event triggering the display of the in-app message.
1. In the **Trigger events** section, click **Define**.
2. Select **Add event...** and from the dropdown list, choose `app.firstVisit` event.
3. Click the **+ where** button and select `source`.
4. As the logical operator, select **Equal**.
5. In the text field enter **MOBILE_APP**.
5. Click **Apply**.
### Schedule the message and configure display settings
---
As the final part of the process, you will set the display settings of the in-app message such as schedule, capping, priority of the message among other in-app messages.
1. In the **Schedule** section, click **Define** and set the time when the message will be active.
2. In the **Display Settings** section, click **Define**.
1. Define the **Delay display**, **Priority index**, **Frequency limit** and/or **Capping limit**.
2. In our case, we want to display the message once per user. To do this, switch on **Capping limit**, and in the **Show maximum** section text field type `1`.
4. Click **Apply**.
5. Optionally, you can define the UTM parameters, additional parameters or test your in-app campaign.
6. Click **Activate**.
## Check the use case set up on the Synerise Demo workspace
---
You can check the [in-app message](https://app.synerise.com/communications/in-app/6d39ca33-542c-4c18-950c-0620ca5c486e) configuration directly in Synerise Demo workspace.
If you’re our partner or client, you already have automatic access to the **Synerise Demo workspace (1590)**, where you can explore all the configured elements of this use case and copy them to your workspace.
If you’re not a partner or client yet, we encourage you to fill out the contact [form](https://demo.synerise.com/request) to schedule a meeting with our representatives. They’ll be happy to show you how our demo works and discuss how you can apply this use case in your business.
## Generated events
This use case generates approximately 4 events per profile that completes the flow:
`app.firstVisit` (~1), [`inApp.show`](/docs/assets/events/event-reference/inapp#inappshow) (~1), [`inApp.click`](/docs/assets/events/event-reference/inapp#inappclick) (~1), [`form.submit`](/docs/assets/events/event-reference/web-and-app#formsubmit) (~1).
## Read more
---
- [In-app messages](/docs/campaign/in-app-messages)
- [Mobile campaigns](/docs/campaign/Mobile)
- [Using in-app template builder](/docs/campaign/in-app-messages/creating-inapp-templates/creating-inapp-template)
# Personalized menu
The navigation menu serves as a roadmap for category discovery, allowing customers to explore important site areas. However, sometimes, if the menu is not personalized, it can highlight items that are not interesting for to them. Optimizing and reorganizing the menu items based on a customer's previous visits and preferences not only eases the website navigation but also can reduce the time it takes to reach the desired categories and drive conversions more effectively.
This use case describes an example of menu personalization for a sports company's website. When the customer clicks the hamburger menu and selects a list of all sports, the list of categories is displayed in an alphabetical order. Above the list, a recommendation with six personalized categories (based on visits and interactions) is added to facilitate navigation and promote the most frequently viewed categories.

{ "data": [ {"imageLink": "https://contents.demoshop.com/s568172/k$?format=auto&?f=220x220", "itemId": "surfing", "link": "/browse/c0-sporty/c1-surfing/_/N-vfplmx", "title": "Surfing" }, { "imageLink": "https://contents.demoshop.com/s568173/k$?format=auto&?f=220x220", "itemId": "basketball", "link": "/browse/c0-sporty/c1-basketball/_/N-1krujl5", "title": "Basketball" }, { "imageLink": "https://contents.demoshop.com/s568123/k$?format=auto&?f=220x220", "itemId": "snorkeling", "link": "/browse/c0-sporty/c1-snorkeling/_/N-1hhjr0v", "title": "Snorkeling" }, { "imageLink": "https://contents.demoshop.com/s568159/k$?format=auto&?f=220x220", "itemId": "swimming", "link": "/browse/c0-sporty/c1-swimming/_/N-1tkzd7k", "title": "Swimming" }, { "imageLink": "https://contents.demoshop.com/p2097249/k$?format=auto&?f=220x220", "itemId": "bushcraft", "link": "/browse/c0-sporty/c1-bushcraft/_/N-1a5bddf", "title": "Bushcraft" }, { "imageLink": "https://contents.demoshop.com/s567762/k$?format=auto&?f=220x220", "itemId": "aquafitness", "link": "/browse/c0-sporty/c1-aquafitness/_/N-1o54845", "title": "Aquafitness" } ], "extras": { "contextItems": null, "correlationId": "5ea91ef7-64de-49ad-bb6d-7eddb8abcce9", "slots": [ { "id": 0, "itemIds": [ "surfing", "basketball", "snorkeling", "swimming", "bushcraft", "aquafitness" ], "name": "Unnamed slot"}]}}




{% aggregate 6b686d01-758f-328a-859b-91e3542c5bbe %} {{ aggregate_result[0] }} {% endaggregate %}
{% set mainShop = customer.StoreId %}
{% catalogitemv2.UC-ShopsSale11092023(mainShop) %}
Get ready for shopping at your favorite local spot, {{ catalog_result.StoreName }}! 🌟
🎁 Enjoy jaw-dropping discounts of up to 30% off on absolutely everything in-store!
📍 Find us at {{ catalog_result.Street }} in the heart of {{ catalog_result.City }}.
{% endcatalogitemv2 %}
Where:
- `StoreId` - the name of the attribute assigned to the user profile reflecting the store ID,
- `UC-ShopsSale11092023` - is the name of the created catalog,
- `StoreName`, `Street`, `City` - are the names of the columns defined in the catalog.

In this metric, you check the number of products by using in the metric formula the aggregate that returns the SKU of the products viewed within a campaign.
Additionally, narrow down the results by using the timestamp parameter. Set the time stamp to include the purchases after the first click at the product in the campaign. For that purpose, use the aggregate that calculates the timestamp of the first product visit.
This metric returns the total sum of products bought after clicking a campaign. The metric uses the expression that multiplies the product price by the product quantity.
You can duplicate the metric that calculates the number of products bought after clicking a product, change the aggregator from Count to Sum and use the expression that multiplies the price by the quantity.
This metric is based on the `transaction.charge` event (this event collects an array of products included in the transaction). The metric reuses the aggregate that returns the list of products bought after clicking the campaign to check if the order ID of the product is included in the transaction. The scope of the metric is narrowed down by the aggregate that returns the timestamp of the first visit at the product page.
This metric is based on the `transaction.charge` event (this event collects an array of products included in the transaction). The metric reuses the aggregate that returns the list of products bought after clicking the campaign to check if the order ID of the product is included in the transaction. The scope of the metric is narrowed down by the aggregate that returns the timestamp of the first visit at the product page.
If you have already created a metric for the number of transactions with the products bought after clicking a campaign, you can duplicate it and change the aggregator type and use the `totalAmount` parameter.
{% aggregate a1872106808667667%} Check before they disappear! Shoes in size {{aggregate_result [0] | float | round (0)}} {% endaggregate%}
As a result, customer will see: **Check before they disappear! Shoes in size [size number of the customer]**.
3. To create the content of the email, click **Create message**. In the email template include the Jinajava code that contains the link to the listing of products in the customers' size:
<!-- Opening the tag that retrieves the value from the aggregate prepared in point 1--> {% aggregate xxx %} <!--Creating link to website with params to filter to client's size, link below is only an example --> {% set link = "your_link_to_listing.com/listing?size="+aggregate_result[0] %} <!--Adding Synerise tracking parameters --> <a href="{% preparelink %}{{link}}{% endpreparelink%}"> <!-- Adding yours banner link --> <img src="your_image.jpg" /> </a> <!--Closing of the tag that gets the value from the aggregate prepared in point 1 --> {% endaggregate %}
{
"credential": {
"recipient": {
"name": "{{ event.params.firstName }} {{ event.params.lastName }}",
"email": "{% customer email %}",
"group_id":"{% set key = event.params.certificationId %}{% catalogitem.certificationList(key) %}{% set object = catalog_result %}{{ object.get('accredibleid')}}{% endcatalogitem %}"
}
}}


<!-- Opening the tag that retrieves the value from the aggregate prepared in step 1--> {% aggregate 6b009acf-d86a-3ac6-b299-cb3c5d64c04f %} <!-- Assign an aggregate value to skuValue --> {% set skuValue = aggregate_result[0] %} <!-- Referencing with the help of variable to the snrs-product-ogTag directory built by default from OG tags. The variable names depend on the name of the OG tags on your page --> <div class="wrapper-card"> <h2 style="text-align:center">Product Card</h2> <div class="card"> <img src="{% catalog.Snrs-produktu-ogTag(skuValue).og:image %}" alt="Product image" style="width:100%"> <h1>{% catalog.Snrs-produktu-ogTag(skuValue).product:brand %}</h1> <p class="price">$ {% catalog.Snrs-produktu-ogTag(skuValue).product:price:amount %}</p> <p>{% catalog.Snrs-produktu-ogTag(skuValue).og:title %}</p> <p><button>Add to Cart</button></p> </div> </div> <!-- Adding the skuValue variable to the array for which we will retrieve cross-sell products --> {% set itemContext = [] %} {% do itemContext.append(skuValue) %} <!-- Downloading the cross-sell campaign for the product in the table --> {% recommendations3 campaignId=tFwEUKvZLj4d itemsIds=itemContext %} <!-- Iterating loop through the received products --> {% for item in recommended_products3 %} <!-- Listing the attributes assigned to the product in the feed --> <div class="wrapper-card" style="width:25%;float:left"> <h2 style="text-align:center">Product Card</h2> <div class="card"> <img src="{{ item.imageLink }}" alt="Product image" style="width:100%"> <h1>{{ item.brand }}</h1> <p class="price">$ {{ item.priceValue }}</p> <p>{{ item.title }}</p> <p><button>Add to Cart</button></p> </div> </div> {% endfor %} {% endrecommendations3 %} <!-- Closing of the tag that gets the value from the aggregate prepared in point 1 --> {% endaggregate %}
{ "variables": [ { "name": "storeId", "value": "{ customer storeId %}" } ] }
{# Here we retrieve the results of reports from previous nodes #} {% set raport1 = automationPathSteps['data1'].event.params['body.data[0].values[0]'] %} {% set allRaports = [raport1] %} {# We iterate over retrieved reports and add arrays of skus of products from them to the skusArrays #} {% set skusArrays = [] %} {%- for raport in allRaports -%} {% set arr = [] %} {%- for entry in raport -%} {% do arr.append(entry.name[0]) %} {%- endfor -%} {% do skusArrays.append(arr) %} {%- endfor -%} {% set finalArr = [] %} {%- for skuArr in skusArrays -%} {%- for sku in skuArr -%} {%- if finalArr|length < 10 -%} {% catalogitemv2.store-1(sku) allowEmpty=True %} {% set prod = catalog_result %} {%- if prod.name and prod.brand -%} {#Here we can add conditions to check#} {%- do finalArr.append({ 'sku': sku, 'name': prod.name, 'productUrl': prod.productUrl, 'price': prod.price, 'brand': prod.brand.label }) -%} {%- endif -%} {% endcatalogitemv2 %} {%- endif -%} {%- endfor -%} {%- endfor -%} {% set data = [] %} {# Here we set the final object to send #} {%- for promotion in finalArr -%} {% do data.append({ "client": { "customId": "{% customer StoreId %}" }, "label": "Magic products", "action": "magic.products", "type": "custom", "params": promotion, }) %} {%- endfor -%} {# We convert the object to JSON and this is the body of our request #} {% if data|length > 0 %} {{ data|tojson }} {% else %} {{ [{ "client": { "customId": "{% customer StoreId %}" }, "label": "Magic products", "action": "magic.products", "type": "custom", "params": { "alert": "No products matched the conditions" } }] | tojson }} {% endif %}

{% recommendations3 campaignId=nBzlR4BQtyvD %}
{% set item = recommended_products3[0] %}
{
"label": "Webpush recommendation",
"client": {
"id": {{ customer.id }}
},
"action": "webpush.recommendation",
"params": {
"title": "{{ item.title }}",
"imageLink": "{{ item.imageLink }}",
"link": "{{ item.link }}"
}
}
{% endrecommendations3 %}
Where:
- The `campaignId=nBzlR4BQtyvD` is the Id of the recommendation campaign you created earlier [in this step](/use-cases/webpush-with-recommended-products#create-personalized-recommendation-campaign).
- All params contain information about recommended product are used in the [webpush template](/use-cases/webpush-with-recommended-products#create-a-web-push-template).
6. Click **Apply**.

{ "email": "test@test.com", "city": "", "language": "fr", "payment_cash": "1", "payment_online": "0" }, { "email": "test2@test.com", "city": "New York", "language": "en", "payment_cash": "0", "payment_online": "1" }
{% if root["city"] != "" %}{{ root["city"] }}{% else %}unknown{% endif %}


{% if root["language"] == "en" %}English{% elif root["language"] == "es"%}Spanish{% elif root["language"] == "fr"%}French{% endif %}

{% if root["payment_cash"] == "1" %}cash{% elif root["payment_cash"] == "0"%}{% endif %}
2. Click **Add rule**.
1. Click **Add column**.
2. Select the `payment_online` column.
3. Under **Edit values by**, from the dropdown list, select **Replacing**.
5. Under **with**, from the dropdown list, select **Dynamic value**.
6. In the **Type value** field, paste the following Jinja code:
{% if root["payment_online"] == "1" %}online{% elif root["payment_online"] == "0"%}{% endif %}
As a result, in the **Output data** tab, you will get an updated file:







{ "data": [
{
"event_name": "PageVisit",
"event_time": {{ (event.params.time / 1000)|int }},
"event_source_url": "{{ event.params.uri }}",
{% if event.params['product:retailer_part_no'] %}
"currency": "PLN",
"content_type": "product",
"content_ids": ["{{ event.params['product:retailer_part_no'] }}"],
"value": {{ event.params['product:price:amount']|int }},
{% endif %}
"user_data": {
"client_ip_address": "{{ event.params.ip }}",
"client_user_agent": "",
"em": ["{% if 'anonymous' in client.email %}{% else %}{{ client.email|trim|lower|hash("SHA-256") }}{% endif %}"],
"ph": ["{% if 'phone' in customer|string %}{{ client.phone|trim|lower|hash("SHA-256") }}{% endif %}"],
"fbc": "",
"fbp": ""
},
"custom_data": {}
}]
}
where:
| Line number | Description |
| --- | --- |
| 4 | Inserts event time from the trigger node and re-calculates it into seconds (this is required by Facebook). |
| 5 | Inserts URI from the trigger node as the `event_source_url` param. |
| 6-11 | If the visited page has product metadata, inserts that metadata. |
| 13 | Inserts customer's IP from the trigger node. |
| 15 | Inserts email encoded using SHA-256 algorithm, if available. |
| 16 | Inserts phone number encoded using SHA-256 algorithm, if available. |



{#CONFIG#} {% set mentions = ['abc@synerise.com','xyz@synerise.com']%} {# you can leave array empty []#} {% set title = "Message.notSent alert" %} {% set reportId = 'f3370785-f309-4b6f-b47f-bb10de377708' %} {% set env = 'microsoft-eu' %} {# microsoft-eu, microsoft-usa, google #} {% set campaignType = 'email' %} {# email, sms, mobile-push, webpush #} {#ENDCONFIG#} {% if env == 'microsoft-usa' %} {% set baseUrl = 'https://app.azu.synerise.com' %} {% elif env == 'google' %} {% set baseUrl = 'https://app.geb.synerise.com %} {% else %} {% set baseUrl = 'https://app.synerise.com' %} {% endif %} { "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "{% for mention in mentions %}<at>{{mention}}</at>{% if not loop.last %}, {% endif %}{% endfor %}", "size": "small" }, { "type": "TextBlock", "size": "Medium", "weight": "Bolder", "text": "{{title}}" }, { "type": "Table", "columns": [ { "width": 1 }, { "width": 1 }, { "width": 1 }, { "width": 1 }, { "width": 1 } ], "rows": {% set dataRow=[] %} {% set titles=['campaignName', 'info', 'extra', 'value', 'campaignId'] %} {% set cells = [] %} {% for t in titles %} {% do cells.append( { "type": "TableCell", "items": [ { "type": "TextBlock", "text": t, "wrap": true } ] } ) %} {% endfor %} {% do dataRow.append({"type": "TableRow","cells":cells}) %} {% datareference node='Get report' maxRows=12 %} {% set cells=[] %} {% for r in datareference_result %} {% set cells=[ { "type": "TableCell", "items": [ { "type": "TextBlock", "text": r.campaignName +' [[link]]('+baseUrl+'/campaigns/'+campaignType+'/preview/'+r.id+')', "wrap": true } , { "type": "TextBlock", "text": 'Automation [[link]]('+baseUrl+'/automations/workflows/automation-diagram/'+r.diagramId+')' if r.diagramId is not string_containing 'null' else '', "wrap": true } ] }, { "type": "TableCell", "items": [ { "type": "TextBlock", "text": r.info, "wrap": true } ] }, { "type": "TableCell", "items": [ { "type": "TextBlock", "text": r.extra, "wrap": true } ] }, { "type": "TableCell", "items": [ { "type": "TextBlock", "text": r.Value|int, "wrap": true } ] }, { "type": "TableCell", "items": [ { "type": "TextBlock", "text": r.id, "wrap": true } ] } ]%} {% do dataRow.append({"type": "TableRow","cells":cells}) %} {% endfor %} {% enddatareference %} {{dataRow|tojson}} } ], "actions": [ { "type": "Action.OpenUrl", "title": "Raport", "url": "{{baseUrl}}/analytics/reports/{{reportId}}" } ], "bleed": true, "msteams": { "width": "Full", "entities": [ {% for mention in mentions %} { "type": "mention", "text": "<at>{{mention}}</at>", "mentioned": { "name": "{{mention}}", "id": "{{mention}}" } } {% if not loop.last %} , {% endif %} {% endfor %} ] } }
<!-- The purpose of the following part of Jinjava is to calculate the day of the month that corresponds to the current date plus 1 hour and save it as a string.).--> {% set now = (unixtimestamp(null)|plus_time(1, 'hours'))|unixtimestamp() %} {% set now_sec = now|divide(1000) %} {% set current_date = (now_sec|int*1000)|datetimeformat('%d.%m.%y') %} {% set current_date_array = current_date|split('.') %} {% set today = current_date_array[0] %} {% set todayString = today|int|string %} <!-- The logic behind the remaining part of Jinjava determines which set of HTML elements to generate based on the value of the aggregate result and the relevant links and image data from the catalog. This part divides customers into 2 groups based on their transaction activity (customers who have or have not made any transaction in the last 180 days).--> {% aggregate insert_aggregate_ID %} {% set insert_aggregate_name = aggregate_result|last %} {%if insert_aggregate_name =='true' %} {% catalogitemv2.insert_catalog_name(todayString) %} <a href="{% preparelink %}{{ catalog_result.link1 }}{% endpreparelink %}"> <img src="{{ catalog_result.Group1banners }}"> </a> {% endcatalogitemv2 %} {% else %} {% catalogitemv2.insert_catalog_name(todayString) %} <a href="{% preparelink %}{{ catalog_result.link2 }}{% endpreparelink %}"> <img src="{{ catalog_result.Group2banners }}"> </a> {% endcatalogitemv2 %} {% endif %} {% endaggregate %}
{
"action":"client.review",
"client":{
"email":"string"
},
"params":{
"author_name":"string",
"created_at":"2021-04-28T14:09:27.000Z",
"grade":0,
"brand":"string",
"category":"string",
"group_id":"string",
"gtin":"string",
"image_url":"string",
"local_id":"string",
"name":"string",
"product_url":"string",
"public_identifier":"string"
}
}


{ "category": "coffee", "itemId": "{{ event.params.$sku }}", "predictedTime": "{% set avgTime = [] %}{% aggregate PASTE_AGGREGATE_ID_HERE %}{%set timeDiff = []%}{% for item in range(aggregate_result|length-1) %}{%set diff = aggregate_result[loop.index] - aggregate_result[loop.index-1]%}{% do timeDiff.append(diff)%}{% endfor %}{% set temp = timeDiff|sum/(aggregate_result|length-1) %}{% do avgTime.append(temp) %}{{ datetimeformat(unixtimestamp(null) + avgTime[0], '%Y-%m-%d') }}{% endaggregate %}", "predictedTimeInDays": "{{avgTime[0]/86400000}}" }
<script>
dataLayer.push({
'pid': 'product_id',
'event': 'event_name'
'sce': 'campaign_name'});
</script>
Where:
- `product_id`- recommended product ID
- `event_name`- name of the event, by which you will recognize AI recommendation's campaign
- `campaign_name`- optionally, name of the recommendation campaign. Very useful if you send to the Data Layer products from more than one AI campaign a time
{% voucher assign=false %} pool-uuid {% endvoucher %}
is an insert that assigns a code from a pool to a customer (unless one is already assigned) and [retrieves that same code for this customer every time](/developers/inserts/insert-usage#retrieving-the-same-code-every-time). The value `pool-uuid` must be replaced with the ID of your voucher pool.
5. In the **CTA button** section, customize the button text, corner radius, color, background color, and URL to which a user is redirected according to your needs.
6. In the **Close button** section, select the position of the closing cross.
7. In the **Automatically Disappear** section, you can enable the **Automatically Disappear** switch and define a timeout for this in-app message in seconds, or leave this switch disabled by default.
8. You can also enable **Use deep links** option if you are using deep links as CTA links.
9. When the template is ready, in the upper right corner click **Save as**.
10. On the pop-up:
1. In the **Template name field**, enter the name of the template.
2. From the **Template folder** dropdown list, select the folder where the template will be saved.
3. Confirm by clicking **Save**.
11. To continue the process of configuring the in-app campaign, click **Next**.
12. To save your content changes, click **Apply**.
In the screens below you can see a preview of the final in-app message configuration with and without contextual preview.

{% set tab = [] %}
{% recommendations2 campaignId=campaign_hash %}
{% for p in recommended_products2 %}
{% set count = (loop.index - 1) %}
{% for r in p.customAttributes %}
{% if r.name == 'average_rating' %}
<!-- WE SAVE RATING AND INDEX OF PRODUCT -->
{% do tab.append({rating:r.value, index: count}) %}
{% endif %}
{% endfor %}
{% endfor %}
{% for r in tab|sort(true, true, 'rating') %}
{{ r }}
<!-- SHOW PRODUCTS SORTED BY RATING -->
{{ recommended_products2[r.index].productRetailerPartNo }}<br>
{% endfor %}{% endrecommendations2 %}
## Generated events
This use case generates approximately 5 events per profile that completes the flow:
[`dynamicContent.show`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentshow) (~1), [`dynamicContent.click`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentclick) (~1), [`recommendation.generated`](/docs/assets/events/event-reference/recommendations#recommendationgenerated) (~1), [`recommendation.view`](/docs/assets/events/event-reference/recommendations#recommendationview) (~1), [`recommendation.click`](/docs/assets/events/event-reference/recommendations#recommendationclick) (~1).
## Read more
---
- Read more about [Jinjava inserts](/developers/inserts/insert-usage) (recommendations)
- Read more about [Loops in Jinjava](/developers/inserts/tag)
- Read more about [Product feed preparation](/developers/product-feed)
# Time-window event aggregates
*"How much revenue did this email drive? Did the push notification lead to a purchase? Which campaign gets credit for the sale?"* - the usual answer comes in one of two forms, and each has a drawback. You can build the metric logic and formulas by hand, but joining a click to a later purchase this way relies on approximations — proxies for the link between the two events — so the result is never fully accurate. Alternatively, you can export the raw events and wait for the BI or analytics team to join clicks to purchases for you, which arrives days later as a static spreadsheet that cannot be refreshed. A third option avoids both drawbacks: a single metric that returns the number directly.
The difference from the hand-built approach is not the number of metrics involved — it is how the link between the two events is made. A time-window event aggregate tracks a triggering event (for example, a `newsletter.click`) within a defined rolling window. When this aggregate is attached as a condition to a metric built on a base event (for example, each `transaction.charge`), it checks, for every occurrence of the base event, whether the triggering event happened first, within that window. The link between the action and the outcome is established at the event level — not approximated — without hand-built formulas and without an export to refresh. Because it runs on first-party events rather than tracking pixels, the result is not affected by cookie consent or cross-device behavior.
This builds on a familiar idea: a standard Last aggregate returns a profile's most recent value within a date range. An event aggregate applies the same Last logic at the event level — it evaluates each occurrence of the base event and looks at what happened before it — so the defined window only sets how far back it checks for the triggering event.
The key characteristic of this aggregate: it returns nothing on its own. It is a condition attached to a base event. You are not asking *"what did each customer do"* — you are asking *"of all these purchases, which ones followed the triggering action within the window."* This applies to any scenario where one action causes a later outcome inside a known time horizon: email, push, in-app, recommendations, or in-store visits.
This use case shows how to build that metric, step by step, using email attribution as the example.
## Example - Email attribution
---
**Business question:** *"How many transactions can we credit to the newsletter, and to which campaign specifically?"*
**Why this matters:** Open Rate and CTR tell you that people engaged with the email. They do not tell you that anyone bought anything. Without connecting the click to the transaction, email's contribution to revenue stays an assumption rather than a measured number. This use case provides the number, and it holds up against cookie consent, ad-blockers, and cross-device behavior, because it runs on first-party events rather than tracking pixels.
The three levels below are not three separate metrics. They are one metric, extended step by step. On its own it returns a single number (Level 1). Add a dimension and that same number splits into a table, one row per campaign (Level 2). Expose its parameter as a dynamic key and a user can enter any campaign and read its result on a dashboard (Level 3). The attribution logic stays identical at every level; what changes is only the dimension or parameter added on top. The [Process](#process) section builds it.
### Level 1 - Total
*"How much did the newsletter drive overall?"*
One number: the count of transactions that happened within 24h of any newsletter click.
**What decisions you can make based on the metric result:**
- Defend or challenge the email channel's revenue contribution in a business review.
- Track the trend over time — is email-driven revenue growing or declining?
- Set the baseline before testing a new send strategy.
### Level 2 - Per campaign
*"Which campaign gets the credit?"*
The same logic, broken out into a table — one row per campaign, showing how many post-click transactions each one drove. In this variant, the metric is used inside a report, with the campaign ID set as a dimension. The metric returns the attributed transaction count; the report is what breaks it into one row per campaign.
**What the report shows:** a table of `campaignId` (or `campaignName`) | transactions attributed.
**What decisions you can make based on the metric result:**
- Stop crediting campaigns with high open rates that drive clicks but no purchases.
- Identify the subject lines or offers that convert, not just engage.
- Reallocate send budget toward campaigns with proven downstream revenue.
### Level 3 - Self-serve
*"Let marketers check any campaign themselves."*
The same metric, exposed as a dashboard field where a user enters a campaign ID and instantly sees that campaign's attributed transactions. It uses last-click logic, so a purchase is credited only to the most recent newsletter click before it (if a customer clicks campaign X and then campaign Y, only Y receives the sale).
**What decisions you can make based on the metric result:**
- Hand campaign owners a self-service answer instead of a request to the analytics team.
- Wire the dashboard directly to email communications so the campaign context is passed automatically.
- Standardize how the whole team measures email-driven revenue.
## Prerequisites
---
The [Process](#process) below builds the total and per-campaign version step by step. All logic is configured inside Synerise using analytics features. No SQL, API, or external attribution tool is required. The same steps apply to other triggering actions — only the events and the window change. For more examples, see [Other applications](#other-applications).
- Both events must be tracked — the triggering event (for example, the `newsletter.click`) and the outcome event (for example, `transaction.charge`).
- For a breakdown, the identifying parameter must be on the event - e.g. `newsletter.click` must carry a campaign id if you want results split by campaign.
- Decide the attribution window up front (24h / 48h / 72h). This is a business decision, not a technical default — see the note in the [Process](#process) section.
- The events you want to analyze must already exist in the workspace. For building the aggregate and the metric, see [Creating event aggregates](/docs/crm/aggregates/creating-event-aggregates) and [Creating simple metrics](https://hub.synerise.com/docs/analytics/metrics/creating-simple-metrics/).









[
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "{% customer firstname %}"
},
{
"type": "text",
"text": "{% aggregate c37acfe7-08a1-345c-a3e7-da795bb6a326 %}{% for item in aggregate_result %}{% catalog.store-1(item).name %}{% endfor %}{% endaggregate %}"
},
{
"type": "text",
"text": "{% voucher %} 8c3c8fd3-e7e9-487d-ba89-0b7824f65f33 {% endvoucher %}"
}
]
},
{
"type": "button",
"parameters":
"index": "0",
"sub_type": "url", [
{
"type": "url",
"text": "{% aggregate c37acfe7-08a1-345c-a3e7-da795bb6a326 %}{% for item in aggregate_result %}{% catalogvar.store-1(item).url %}{{ catalog_result|replace('https://yourshop.com', '') }}{% endcatalogvar %}{% endfor %}{% endaggregate %}"
}
]
},
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "{% aggregate c37acfe7-08a1-345c-a3e7-da795bb6a326 %}{% for item in aggregate_result %}{% catalogvar.store-1(item).image %}{{ catalog_result }}{% endcatalogvar %}{% endfor %}{% endaggregate %}"
}
}
]
}
]
9. Click **Apply**.
The following table explains all the inserts used in the body, button and header sections shown above.
| Section | Insert value | Insert explanation |
|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| body | `{% customer firstname %}` | The value of this insert is used as the value of `{{1}}`, so the name of a customer can be displayed in the message. |
| body | `{% aggregate c37acfe7-08a1-345c-a3e7-da795bb6a326 %} {% for item in aggregate_result %} {% catalog.store-1(item).name %} {% endfor %} {% endaggregate %}` | The value of this insert is used as the value of `{{2}}` to return a product that the customer bought the last time and did not purchase again within the estimated time period. |
| body | `{% voucher %} 8c3c8fd3-e7e9-487d-ba89-0b7824f65f33 {% endvoucher %}` | The value of this insert is used as the value of `{{3}}`. In this example, we assign a voucher code from the voucher pool to an individual customer. |
| button | `{% aggregate c37acfe7-08a1-345c-a3e7-da795bb6a326 %}{% for item in aggregate_result %}{% catalogvar.store-1(item).url %}{{ catalog_result\|replace('https://yourshop.com', '') }}{% endcatalogvar %}{% endfor %}{% endaggregate %}` | The value of this insert is used to return product url. Here we specify the URL omitting the domain, because we define the domain in the Meta platform, as you can see in the screenshot with the [button creation](/use-cases/send-replenishment-message-on-whats-app#create-a-message-template-in-the-meta-portal). |
| header | `{% aggregate c37acfe7-08a1-345c-a3e7-da795bb6a326 %}{% for item in aggregate_result %}{% catalogvar.store-1(item).image %}{{ catalog_result }}{% endcatalogvar %}{% endfor %}{% endaggregate %}` | This value of insert is used to return the url to the product image in header. |

















{
"content-type": "promotion",
"background-color": "#123321",
"title": "Christmas Promotion",
"promotion": "{% promotion fields=uuid,name,discountType,discountValue,code,params,tags,type,price,images,description,expireAt %} 3a3750b0-c00b-4c57-9c5f-d55652b417a0 {% endpromotion %}"
}








const QUESTIONS = [
{
"type": "single",
"question": "What motivated you to download our app?",
"answers": [
"To browse products",
"To make a purchase",
"To explore exclusive offers",
"To compare prices",
{ "answer": "Other (please specify):", "options": { "isOpen": true, "limit": 20 } },
],
"shuffleAnswers": false,
"required": true
},
{
"type": "multi",
"question": "What types of products or services are you most interested in?",
"answers": [
"Fashion",
"Electronics",
"Home and Kitchen",
"Health and Beauty",
"Sports and Outdoors",
{ "answer": "Other (please specify):", "options": { "isOpen": true, "limit": 20 } }
],
"shuffleAnswers": false,
"required": true
},
{
"type": "single",
"question": "How do you prefer to shop?",
"answers": [
"I like to browse and explore new products",
"I prefer targeted recommendations based on my preferences",
"I usually know what I want and search directly",
"I’m mainly looking for deals and offers",
],
"shuffleAnswers": false,
"required": true
},
{
"type": "multi",
"question": "Which factors influence your purchasing decisions the most?",
"answers": [
"Product quality",
"Price",
"Brand reputation",
"Customer reviews",
"Sustainability",
{ "answer": "Other (please specify):", "options": { "isOpen": true, "limit": 20 } }
],
"shuffleAnswers": false,
"required": true
},
{
"type": "scale",
"question": "How likely are you to recommend our company to your friends and acquaintances?",
"length": 10,
"required": true,
},
];
5. After customising your survey, save the template.
### Define schedule and display settings
---
As the final part of the process, you will define the display settings of the dynamic content such as schedule, triggers and delay.
1. In the **Schedule** section, click **Define** and set the time when the message will be active.
2. In the **Display Settings** section, click **Define**.
1. In the **Triggers** section, choose **On exit**.
2. Click **Advanced options**. In our case, we want to display the survey once per user. To do this, set the **Frequency** to **Once**, and in the **Stop ddisplay** section choose option **Dynamic content was shown to a viewer x times** and type `1`.
4. Click **Apply**.
5. Optionally, you can define the UTM parameters and additional parameters for your dynamic content campaign.
6. Click **Activate**.
## Check the use case set up on the Synerise Demo workspace
---
You can check the [dynamic content campaign](https://app.synerise.com/campaigns/dynamic-content/create/1aa85a47-18dd-49e8-8179-bb6be80be4e2) configuration directly in Synerise Demo workspace.
If you’re our partner or client, you already have automatic access to the **Synerise Demo workspace (1590)**, where you can explore all the configured elements of this use case and copy them to your workspace.
If you’re not a partner or client yet, we encourage you to fill out the contact [form](https://demo.synerise.com/request) to schedule a meeting with our representatives. They’ll be happy to show you how our demo works and discuss how you can apply this use case in your business.
## Generated events
This use case generates approximately 3 events per profile that completes the flow:
[`dynamicContent.show`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentshow) (~1), [`dynamicContent.click`](/docs/assets/events/event-reference/dynamic-content#dynamiccontentclick) (~1), [`form.submit`](/docs/assets/events/event-reference/web-and-app#formsubmit) (~1).
## Read more
---
- [Dynamic content](/docs/campaign/dynamiccontent)
# In-app campaign with cross-sell products
In the world of mobile applications, engaging users effectively and generating revenue are paramount objectives. One powerful strategy for achieving both of these goals is the implementation of in-app cross-sell campaigns. These campaigns are designed to present users with complementary product suggestions precisely when they are most likely to make a purchase, thereby enhancing their experience and increasing the app's revenue potential.
A compelling example of the impact in-app cross-sell campaigns can have is demonstrated through the "product.addToCart" event. In this specific use case, when a mobile app user adds an item to their shopping cart within a mobile application, it triggers a dynamic campaign. This campaign's unique feature is its ability to display cross-selling products tailored to the specific item added to the cart, all based on event parameters such as the SKU (Stock Keeping Unit). By leveraging these parameters and the underlying code of the cross-sell recommendation campaign, the app can deliver a highly personalized and targeted experience to its users.
The primary purpose of this use case is to optimize the user experience within a mobile application by offering relevant product recommendations at a critical moment in the customer's journey – when they are actively making a purchase. By harnessing the "product.addToCart" event, this in-app campaign can provide users with product suggestions that align with their interests and the specific product they have chosen.
In this use case, we provide you with ready-to-use campaign code that you can use 1:1 in your business scenario.
## Prerequisites
---
- [Implement Synerise SDK in your mobile app](/developers/mobile-sdk).
- Implement transaction events using [SDK](/developers/web/transactions-sdk) or [API](https://hub.synerise.com/api-reference/data-management#operation/CreateATransaction).
- [Configure AI engine](/docs/settings/configuration/ai-engine-configuration/engine-configuration-for-search). Enable **Cross-sell** recommendations.
- [Implement the product.addToCart event in your mobile application](/developers/mobile-sdk/event-tracking#product-added-to-cart).
## Process
---
In this use case, you will go through the following steps:
1. [Create AI recommendations](/use-cases/in-app-bestsellers#create-ai-recommendations) with cross-sell products.
2. [Create an in-app campaign](#create-an-in-app-campaign) with the usage of an event parameter.
## Create AI recommendations
---
In this stage, you will create an AI recommendation campaign which will be used to display products in your in-app message. It will present cross-sell recommendations.
1. Go to


[{
"insertId": "{{ currentStep.actionId }}",
"json": {
"data": "{% set today = timestamp|timestamp_to_time|datetimeformat('%d.%m.%Y', tz="Europe/Warsaw" ) %}{{today}}",
"ctr": "{% metricvar d3cbeffb-4883-4621-a403-15e4bddc4650 %}",
"or": "{% metricvar 69af6a34-02d9-4a11-8436-1c8967b0abf3 %}",
"ctor": "{% metricvar 373dc09a-552d-4108-a713-e0261ac86516 %}"
}
}]
### Add the finishing node
---
1. Add the **End** node.
2. In the upper right corner, click **Save & Run**.

{% recommendations3 campaignId=aIEkwqXw0wLB %} {% for slot in slots_products3 %} {% for row in slot.rows %} <!-- Variable metadata which allows you to access the metadata catalog --> {{ row.metadata.itemId }} {{ row.metadata.imageLink }} <!-- Iterates through items in a given section --> {% for item in row.items %} {{ item.itemId }} {{ item.title }} {% endfor %} {% endfor %} {% endfor %} {% endrecommendations3 %}



{% if record.offerTitle == record.qualifiedOffer %}true{% else %}false{% endif %}
5. To save your changes, click **Apply**.
### Set up the Audience & Settings
1. Click the **Audience & Settings** tab.
2. In the **Audience** section, click **Define**.
3. Choose the schema recipients according to your needs, in our case choose **Everyone**.
4. Click **Apply**.
5. In the upper-right corner, click **Save**
## Create records
---
[Creating a record](/docs/assets/brickworks/quick-start/creating-a-record) means adding the data to the schema. It means that you fill out schema fields. In this case, these are examples of telecommunications offers - one of the three next best offer tiers - Low Cost, Advantage, or All Inclusive.
1. Go to

dd.mm.yyyy format

yyyy/mm/dd into a timestamp



{% set txn_ids = [] %}{% set txn_points = [] %}{% set txn_amounts = [] %}{% set txn_dates = [] %}{% set buy_names = [] %}{% set buy_order_ids = [] %}{% aggregate TRANSACTION_IDS_AGGREGATE_HASH %}{%- for res in aggregate_result -%}{% do txn_ids.append(res) %}{%- endfor -%}{% endaggregate %}{% aggregate TRANSACTION_POINTS_AGGREGATE_HASH %}{%- for res in aggregate_result -%}{% do txn_points.append(res) %}{%- endfor -%}{% endaggregate %}{% aggregate TRANSACTION_AMOUNTS_AGGREGATE_HASH %}{%- for res in aggregate_result -%}{% do txn_amounts.append(res) %}{%- endfor -%}{% endaggregate %}{% aggregate TRANSACTION_DATES_AGGREGATE_HASH %}{%- for res in aggregate_result -%}{% set splitDate = res|split(':') %}{% set splitDateLength = splitDate|length %}{% set finalDate = splitDate[0:splitDateLength-1]|join(':') ~ splitDate[splitDateLength-1] %}{% set finalDateFormatted = datetimeformat(finalDate|strtotime("yyyy-MM-dd'T'HH:mm:ss.SSSZ"), '%b %d, %Y') %}{% do txn_dates.append(finalDateFormatted) %}{%- endfor -%}{% endaggregate %}{% aggregate PRODUCT_NAMES_AGGREGATE_HASH %}{%- for res in aggregate_result -%}{% do buy_names.append(res) %}{%- endfor -%}{% endaggregate %}{% aggregate PRODUCT_ORDER_IDS_AGGREGATE_HASH %}{%- for res in aggregate_result -%}{% do buy_order_ids.append(res) %}{%- endfor -%}{% endaggregate %}{% set orders = [] %}{% for i in range(txn_ids | length) %}{% set order_id = txn_ids[i] %}{% set products = [] %}{% for j in range(buy_order_ids | length) %}{% if buy_order_ids[j] == order_id %}{% set _ = products.append(buy_names[j]) %}{% endif %}{% endfor %}{% set _ = orders.append({'orderId': order_id,'date': txn_dates[i],'amount': txn_amounts[i],'loyaltyPoints': txn_points[i],'products': products}) %}{% endfor %}{{ orders | reverse | tojson }}
{% brickworksgeneratevar schemaId=SCHEMA_ID recordId=RECORD_ID %}
<div class="profile-container">
<button id="close-btn" onclick="SRInApp.close()">×</button>
<div class="profile-header">
<div class="avatar">{{ brickworks_result.firstName|default('P')|truncate(1, true, '') }}</div>
<div class="header-info">
<h1 class="user-name">{{ brickworks_result.firstName|default('Profile') }}</h1>
<span class="member-badge {{ brickworks_result.loyaltyLevel | lower | replace(' ', '-') }}">{{ brickworks_result.loyaltyLevel }}</span>
</div>
</div>
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon">💰</div>
<div class="stat-value">${{ brickworks_result.transactionTotal|default('0') }}</div>
<div class="stat-label">Total Spent</div>
</div>
<div class="stat-card highlight">
<div class="stat-icon">🏆</div>
<div class="stat-value">{{ brickworks_result.loyaltyPointsTotal|default('0') }}</div>
<div class="stat-label">Loyalty Points</div>
</div>
<div class="stat-card">
<div class="stat-icon">🛍</div>
<div class="stat-value">{{ brickworks_result.numberOfTransactions|default('0') }}</div>
<div class="stat-label">Transactions</div>
</div>
</div>
{% set topVisitedCategories = brickworks_result.topVisitedCategories %}
<div class="section-card">
<div class="section-title">🔥 Top Interest</div>
<div class="interest-bar">
{%- if topVisitedCategories|length > 0 -%}
{%- for category in topVisitedCategories -%}
{% set splitCategory = category[0]|split('Default Category > ') %}
<div class="interest-item">
<span class="interest-label">{{ splitCategory[1] }}</span>
</div>
{%- endfor -%}
{%- else -%}
<p class="empty-message">Browse our products to see your interests</p>
{%- endif -%}
</div>
</div>
<div class="section-card">
<div class="section-title">🎁 Active Promotions</div>
<div class="promo-list">
{%- if brickworks_result.promotions.data|length > 0 -%}
{%- for promo in brickworks_result.promotions.data -%}
<div class="promo-item">
<div class="promo-badge">{%- if promo.discountType != "NONE" -%}{{ promo.discountValue }}{%- endif -%}{%- if promo.discountType == "PERCENT" -%}% OFF{%- elif promo.discountType == "AMOUNT" -%}$ OFF{%- else -%}PROMO{%- endif -%}</div>
<div class="promo-info">
<div class="promo-name">{{ promo.name }}</div>
<div class="promo-exp">{{ promo.description|truncate(35) }}</div>
</div>
</div>
{%- endfor -%}
{%- else -%}
<p class="empty-message">There are no promotions available for you</p>
{%- endif -%}
</div>
</div>
<div class="section-card">
<div class="section-title">📋 Past Transactions</div>
<div class="transactions-list">
{%- if brickworks_result.transactionData|length > 0 -%}
{%- for transaction in brickworks_result.transactionData -%}
<div class="tx-item">
<div class="tx-info">
<div class="tx-name">{{ transaction.products | join(', ') }}</div>
<div class="tx-date">{{ transaction.date }}</div>
</div>
<div class="tx-right">
<div class="tx-amount">{{ transaction.amount }}</div>
<div class="tx-points">+{{ transaction.loyaltyPoints }} pts</div>
</div>
</div>
{%- endfor -%}
{%- else -%}
<p class="empty-message">You haven't made any transactions yet</p>
{%- endif -%}
</div>
</div>
</div>
{% endbrickworksgeneratevar %}
In the Type and limits section of specific promotion - in the Priority field, enter a number that defines the promotion’s priority.
Priority controls the display order in the customer’s view. 1 is the highest. If multiple applicable promotions share the same priority, the one created earlier will be shown first.
```xml
{% if root["g:sale_price"] is defined %}true{% endif %}
5. Leave **Handle incomplete data** at default (**Skip row if error occurred**) to skip missing or invalid data which may occur during transformation.
7. Confirm by clicking **Apply**.
### Add the new column
7. On the **Add column** node, click **THEN**.
8. From the dropdown list, select **Add column**.
9. Click the **Add column** node.
10. In the configuration of the node:
1. In the **Add column** field, enter the name of the column. In this use case, it's `percentage_discount`.
3. From the dropdown list, select **Dynamic value**.
4. In the **Type value** box, add the Jinja code, which counts the percentage value of the discount based on `g:price` and `g:sale_price` attributes and adds it to this new column. You can use the code presented below:
{% if root["g:sale_price"] is defined %}{{ root["g:sale_price"]*100/root["g:price"] }}{% endif %}
5. Leave **Handle incomplete data** at default (**Skip row if error occurred**) to skip missing or invalid data which may occur during transformation.
7. Confirm by clicking **Apply**.
### Edit values
7. On the **Add column** node, click **THEN**.
8. From the dropdown list, select **Edit values**.
9. Click the **Edit values** node. In the configuration of the node:
1. Click **Add rule**.
2. Click **Add column**.
3. From the dropdown list, select column name, in this case it's `g:availability`.
4. Set the **Edit values** values option to:
1. **Replacing**
2. **Dynamic value**.
4. In the **Type value** field, enter the Jinja code which replaces the value of `g:availability` attribute from `1` and `0` to `in stock`/`out of stock`. You can use the code presented below:
{% if root['g:availability'] == 1 %}in stock{% else %}out of stock{% endif %}
5. Leave **Handle incomplete data** at default (**Skip row if error occurred**) to skip missing or invalid data which may occur during transformation.
7. Confirm by clicking **Apply**.
### Add the finishing node
This node lets you preview the output of the modifications to the sample data.
1. On the **Edit values** node, click **THEN**.
2. From the dropdown list, select **Data Output**.
3. To preview the results, click the **Data Output** node.














button, set **Count** as the type of the aggregate result.
4. From the **Choose event** dropdown list, select the `page.visit` event.
5. Click the **Where** button and from the **Choose parameter** dropdown list, select `uri`.
6. Select the **Contain** logical operator.
7. In the text field, next to the logical operator, enter the name of the product category.
8. Save the aggregate.
9. Create another one or more aggregates for their respective categories. Repeat the steps 1-8.
**Result**: The aggregates will count visits of individual users to the product category. The aggregates are available in
<!-- Opening the tag that retrieves the value from recommendation prepared in point 1--> {% recommendations3 campaignId=xxx %} for loop below: <!-- In the section {% for r in recommended_products3 %} a {% endfor %} there is access to all variables from a given object (products here) - which parameters you add to the template depends on you. --> {% for r in recommended_products3 %} <!-- {{ r.itemId }} {{ r.title }} {{ r.imageLink }}{{r.price.value}}{{r.salePrice.value}} - parameters taken from the recommendation. The itemId is a standard name, but others (like category, price, salePrice, title, imageLink) only depend on names defined in the feed--> {{r.itemId}} {{r.title}} {{r.imageLink}} {{r.price.value}} {{r.salePrice.value}} {% endfor %} <!-- Closing of the tag that gets the value from the recommendation prepared in point 1 --> {% endrecommendations3 %}
category,city,itemId,lat,lon,postalCode,state,streetAddress,streetName,streetNumber,title STORE,Washington,497,38.897,-77.0251,20073,District of Columbia,3960 Killdeer Terrace,Hanson,8,Nienow and Sons STORE,Brooklyn,69,40.6924,-73.9666,11205,New York,7720 Mockingbird Circle,Bultman,39231,Baumbach-Glover STORE,Los Angeles,742,33.9754,-118.417,90094,California,85774 Stone Corner Street,Washington,652,Grimes LLC STORE,Little Rock,197,34.6725,-92.3529,72209,Arkansas,97498 Bunting Road,Boyd,712,Frami Inc STORE,Salem,622,44.8685,-123.0438,97306,Oregon,40 Dorton Court,Esch,79,Roberts and Sons STORE,Brea,550,33.9187,-117.8892,92822,California,92 Di Loreto Street,Golden Leaf,16877,Hermann-Bergnaum STORE,Sacramento,966,38.3774,-121.4444,94207,California,7250 Green Ridge Center,Kensington,6,Jacobson-Cronin STORE,Baton Rouge,492,30.3795,-91.1671,70820,Louisiana,3351 Grim Lane,Del Mar,5638,Bahringer Inc STORE,Dallas,710,32.7673,-96.7776,75387,Texas,0 Talisman Junction,Glendale,480,"Rippin, Wiza and Borer"










