﻿#### 

For our demo scenario, we create a custom outcome event “**Demo Purchase**” that will be triggered when a customer buys a product. We will store information about customer purchase within this event. In the next posts, we will use our outcome model to generate the initial data model for Cortex (projection model).

![Sitecore outcome event](https://www.brimit.com/-/media/images/blog/cortexpart2-1.png?la=en&amp;hash=B4604AC9294C24B85247064627774402)

Product price indicates a major value for all our processes. We need to mark our outcome to have ‘Monetary Value Applicable’:

![Monetary Value Applicable](https://www.brimit.com/-/media/images/blog/cortexpart2-2.png?la=en&amp;hash=12DC08C0338D9DD34CD20605EF306B9C)

Next, we create the corresponding class with some additional properties:

- CustomerId - stores contact identifier
- InvoiceId - stores purchase identifier
- Quantity - stores count of products that customer purchased

```

public class PurchaseOutcome: Outcome
{
    public int Quantity { get; set; }
    public int InvoiceId { get; set; }
    public int CustomerId { get; set; }

    public PurchaseOutcome(Guid definitionId, DateTime timestamp, string currencyCode, decimal monetaryValue, int invoiceId, int quantity, int customerId) 
        : base(definitionId, timestamp, currencyCode, monetaryValue)
    {
        this.Quantity = quantity;
        this.CustomerId = customerId;
        this.InvoiceId = invoiceId;
    }

    // ID of "Demo Purchase" item
    public static Guid PurchaseEventDefinitionId { get; } = new Guid("F841E7C8-A12D-4376-94CD-296A37EDABCC");
}
```

To make our PurchaseOutcome visible for xConnect and xConnect jobs, we need to build a custom model where we register our new event type PurchaseOutcome.

```
public static class XdbPurchaseModel 
{
    public static XdbModel Model { get; } = BuildModel();

    private static XdbModel BuildModel()
    {
        XdbModelBuilder modelBuilder = new XdbModelBuilder("PurchaseOutcome", new XdbModelVersion(1, 0));

        modelBuilder.ReferenceModel(Sitecore.XConnect.Collection.Model.CollectionModel.Model);
        modelBuilder.DefineEventType<PurchaseOutcome>(false);
        return modelBuilder.BuildModel();
    }
}
```

Run the following code to generate json file:

```
var fileName = XdbPurchaseModel.Model.FullName + ".json";
            var json = XdbModelWriter.Serialize(model);
            System.IO.File.WriteAllText(fileName, json);
```

Copy generated json file to Model folder of the xConnect and Model folder of xConnect jobs:

1. xconnect\_instance\App\_Data\Models\
2. xconnect\_instance\App\_Data\jobs\continuous\ProcessingEngine\App\_Data\Models\
3. xconnect\_instance\App\_Data\jobs\continuous\AutomationEngine\App\_Data\Models\
4. xconnect\_instance\App\_Data\jobs\continuous\IndexWorker\App\_data\Models\

**Important**: We also need to register XdbPurchaseModel in xConnect client configuration of the Processing Engine job. If we don`t do it - the projection will not work. To do it we need to patch “xconnect\_instance\App\_Data\jobs\continuous\ProcessingEngine\App\_Data\Config\Sitecore\XConnect\sc.XConnect.Client.xml” config:

```

<Settings>
    <Sitecore>
        <XConnect.Client>
            <Services>
                <Client.Configuration>
                    <Type>Sitecore.XConnect.Client.Services.Configuration.XConnectModelConfigurator, Sitecore.XConnect.Client.Services.Configuration</Type>
                   <As>Sitecore.XConnect.Client.Services.Configuration.IXConnectModelConfigurator, Sitecore.XConnect.Client.Services.Configuration</As>
                    <LifeTime>Transient</LifeTime>
                    <Options>
                        <!-- The xDB models to use with the xConnect client configuration. -->
                        <Models>
                            <DefaultModel>
                                <TypeName>Sitecore.XConnect.Collection.Model.CollectionModel, Sitecore.XConnect.Collection.Model</TypeName>
                                <PropertyName>Model</PropertyName>
                            </DefaultModel>	    <CustomModel>
<TypeName>Demo.Foundation.ProcessingEngine.Models.XdbPurchaseModel, Demo.Foundation.ProcessingEngine</TypeName>
                                <PropertyName>Model</PropertyName>
                            </CustomModel>
                        </Models>
                    </Options>
                </Client.Configuration>
            </Services>
        </XConnect.Client>
    </Sitecore>
</Settings>
```

To make our model “visible” for Automation Engine we also need to add it to Automation Engine xConnect client configuration:

“xconnect\_instance\App\_Data\jobs\continuous\AutomationEngine\App\_Data\Config\sitecore\sc.XdbPurchase.Model.xml” (xml file name should be in format sc.CustomName.Model.xml):

```
   <Settings>
   <Sitecore>
    <XConnect>
      <Services>
        <XConnect.Client.Configuration>
          <Options>
            <Models>
              <XdbPurchaseModel>
                <TypeName>Demo.Foundation.ProcessingEngine.Models.XdbPurchaseModel, Demo.Foundation.ProcessingEngine</TypeName>
              </XdbPurchaseModel>
            </Models>
          </Options>
        </XConnect.Client.Configuration>
      </Services>
    </XConnect>
  </Sitecore>
</Settings>
```

Finally, If we want to use our custom **PurchaseOutcome** model within the xdb tracker,  we need to create an event conversion pipeline processor and add our model configuration to the xConnect schema of our website instance. 

```
public class ConvertPurchaseOutcome : ConvertToXConnectEventProcessorBase<OutcomeData>
{
        protected override Event ConvertToEvent(OutcomeData entity)
        {
            var quantity = int.Parse(entity.CustomValues["Quantity"] as string);
            var invoiceId = int.Parse(entity.CustomValues["InvoiceId"] as string);
            var contactId = int.Parse(entity.CustomValues["CustomerId"] as string);

            var purchase = new PurchaseOutcome(PurchaseOutcome.PurchaseEventDefinitionId, entity.Timestamp,  entity.CurrencyCode, entity.MonetaryValue,invoiceId,quantity, contactId);

            return purchase;
        }

        protected override bool CanProcess(Sitecore.Analytics.Model.Entity entity)
        {
            if (entity is OutcomeData)
            {
                OutcomeData outcomeData = (OutcomeData)entity;
                return (outcomeData.OutcomeDefinitionId == PurchaseOutcome.PurchaseEventDefinitionId);
            }

            return false;
        }
}
```

Add new config “Foundation.ProcessingEngine.config” to website instance:

```
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <pipelines>
            <convertToXConnectEvent>
                <processor patch:before="processor[@type='Sitecore.Analytics.XConnect.DataAccess.Pipelines.ConvertToXConnectEventPipeline.ConvertOutcomeDataToOutcome, Sitecore.Analytics.XConnect']" type="Demo.Foundation.ProcessingEngine.Processors.ConvertPurchaseOutcome, Demo.Foundation.ProcessingEngine"/>
            </convertToXConnectEvent>
        </pipelines>

      <xconnect>
        <runtime type="Sitecore.XConnect.Client.Configuration.RuntimeModelConfiguration,Sitecore.XConnect.Client.Configuration">
          <schemas hint="list:AddModelConfiguration">
            <!-- value of 'name' property must be unique -->
            <schema name="purchaseoutcomemodel" type="Sitecore.XConnect.Client.Configuration.StaticModelConfiguration,Sitecore.XConnect.Client.Configuration" patch:after="schema[@name='collectionmodel']">
              <param desc="modeltype">Demo.Foundation.ProcessingEngine.Models.XdbPurchaseModel, Demo.Foundation.ProcessingEngine</param>
            </schema>
          </schemas>
        </runtime>
      </xconnect>
    </sitecore>
</configuration>
```

Now we can trigger our custom event in code when user makes a purchase:

```
var customer = Tracker.Current.Session.Contact.Identifiers.First(x => x.Source == "demo");
       
var ev = Tracker.MarketingDefinitions.Outcomes [PurchaseOutcome.PurchaseEventDefinitionId];
var outcomeData = new Sitecore.Analytics.Data.OutcomeData(ev, "USD", 100);
outcomeData.CustomValues.Add("Quantity", "5");
outcomeData.CustomValues.Add("CustomerId", customer.Identifier);     outcomeData.CustomValues.Add("InvoiceId", "10001");
Tracker.Current.CurrentPage.RegisterOutcome(outcomeData);
```

Or we can also register our custom outcome programmatically. (Note: if you want to register outcome with webvisit event, pass addWebVisit=true parameter and replace channel and page Ids with necessary values): 

```
public async Task<bool> Add(Customer purchase, bool addWebVisit = false)
    {
        using (XConnectClient client = SitecoreXConnectClientConfiguration.GetClient())
        {
            try
            {

                IdentifiedContactReference reference = new IdentifiedContactReference(IdentificationSource, purchase.CustomerId.ToString());
                var customer = await client.GetAsync(
                    reference,
                    new ContactExpandOptions(
                        PersonalInformation.DefaultFacetKey,
                        EmailAddressList.DefaultFacetKey,
                        ContactBehaviorProfile.DefaultFacetKey
                    )
                    {
                        Interactions = new RelatedInteractionsExpandOptions
                        {
                            StartDateTime = DateTime.MinValue,
                            EndDateTime = DateTime.MaxValue,
                            Limit = 100
                        }
                    }
                );

                if (customer == null)
                {
                    var email = "demo" + Guid.NewGuid().ToString("N") + "@gmail.com";

                    customer = new Contact(new ContactIdentifier(IdentificationSource, purchase.CustomerId.ToString(), ContactIdentifierType.Known));

                    var preferredEmail = new EmailAddress(email, true);
                    var emails = new EmailAddressList(preferredEmail, "Work");

                    client.AddContact(customer);
                    client.SetEmails(customer, emails);

                    var identifierEmail = new ContactIdentifier(IdentificationSourceEmail, email, ContactIdentifierType.Known);
                    
                    client.AddContactIdentifier(customer, identifierEmail);
                    client.Submit();
                }

                // existing 'Other 3rd party' channelId
                var channel = Guid.Parse("DF9900DE-61DD-47BF-9628-058E78EF05C6");
                var interaction = new Interaction(customer, InteractionInitiator.Brand, channel, "demo app");

                if (addWebVisit)
                {
                    //Add Device profile
                    DeviceProfile newDeviceProfile = new DeviceProfile(Guid.NewGuid()) { LastKnownContact = customer };
                    client.AddDeviceProfile(newDeviceProfile);
                    interaction.DeviceProfile = newDeviceProfile;

                    //Add fake Ip info
                    IpInfo fakeIpInfo = new IpInfo("127.0.0.1") { BusinessName = "Home" };
                    client.SetFacet(interaction, IpInfo.DefaultFacetKey, fakeIpInfo);

                    // Add fake webvisit
                    // Create a new web visit facet model
                    var webVisitFacet = new WebVisit
                    {
                        Browser =
                            new BrowserData
                            {
                                BrowserMajorName = "Chrome",
                                BrowserMinorName = "Desktop",
                                BrowserVersion = "22.0"
                            },
                        Language = "en",
                        OperatingSystem =
                            new OperatingSystemData { Name = "Windows", MajorVersion = "10", MinorVersion = "4" },
                        Referrer = "www.google.com",
                        Screen = new ScreenData { ScreenHeight = 1080, ScreenWidth = 685 },
                        SearchKeywords = "sitecore",
                        SiteName = "website"
                    };

                    // Populate data about the web visit
                    // HomePage ID
                    var itemId = Guid.Parse("110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9");
                    var itemVersion = 1;

                    // First page view
                    var datetime = purchase.Invoices.FirstOrDefault() == null
                        ? DateTime.Now
                        : purchase.Invoices.First().TimeStamp.ToUniversalTime();
                    PageViewEvent pageView = new PageViewEvent(datetime,
                        itemId, itemVersion, "en")
                    {
                        ItemLanguage = "en",
                        Duration = new TimeSpan(3000),
                        Url = "/home"
                    };
                    // client.SetFacet(interaction, WebVisit.DefaultFacetKey, webVisitFacet);
                    interaction.Events.Add(pageView);
                    client.SetWebVisit(interaction, webVisitFacet);
                }

                foreach (var invoice in purchase.Invoices)
                {
                    var outcome = new PurchaseOutcome(PurchaseOutcome.PurchaseEventDefinitionId, invoice.TimeStamp, invoice.Currency, invoice.Price, invoice.Number, invoice.Quantity, purchase.CustomerId);
                    interaction.Events.Add(outcome);
                }

                client.AddInteraction(interaction);

                await client.SubmitAsync();

                return true;
            }
            catch (XdbExecutionException ex)
            {
                Log.Error(ex.Message, ex, this);
                return false;
            }
        }
    }
```

Last but not least, we need to create our custom contact facet to store calculated RFM values and predicted cluster. 

```
 [Serializable]
    [FacetKey(DefaultFacetKey)]
    public class RfmContactFacet : Facet
    {
        public const string DefaultFacetKey = "RfmContactFacet";

        public RfmContactFacet()
        {

        }
        public int R { get; set; }
        public int F { get; set; }
        public int M { get; set; }
        public double Recency { get; set; }
        public int Frequency { get; set; }
        public double Monetary { get; set; }

        public int Cluster { get; set; }
    }
```

To make our RfmContactFacet visible for the xConnect and xconnect jobs, again we need to build custom model where we register our new facet.

```
public static class XdbPurchaseContactModel
    {
        public static XdbModel Model { get; } = BuildModel();

        private static XdbModel BuildModel()
        {
            XdbModelBuilder modelBuilder = new XdbModelBuilder("ContactModel", new XdbModelVersion(1, 0));

            modelBuilder.ReferenceModel(Sitecore.XConnect.Collection.Model.CollectionModel.Model);
            modelBuilder.DefineFacet<Contact, RfmContactFacet>(RfmContactFacet.DefaultFacetKey);

            return modelBuilder.BuildModel();
        }
    }
```

Generate json file:

```
var fileName = XdbPurchaseContactModel.Model.FullName + ".json";
var json = XdbModelWriter.Serialize(model);
System.IO.File.WriteAllText(fileName, json);
```

Copy this generated json file to Model folder of xConnect and Model folder of xConnect jobs:

1. xconnect\_instance\App\_Data\Models\
2. xconnect\_instance\App\_Data\jobs\continuous\ProcessingEngine\App\_Data\Models\
3. xconnect\_instance\App\_Data\jobs\continuous\AutomationEngine\App\_Data\Models\
4. xconnect\_instance\App\_Data\jobs\continuous\IndexWorker\App\_data\Models\

Here are some links that will help you understand custom outcome programming better:

[https://doc.sitecore.com/developers/90/sitecore-experience-platform/en/create-a-custom-outcome-model.html](https://doc.sitecore.com/developers/90/sitecore-experience-platform/en/create-a-custom-outcome-model.html "Sitecore custom model")

[https://doc.sitecore.com/developers/90/sitecore-experience-platform/en/triggering-custom-events.html](https://doc.sitecore.com/developers/90/sitecore-experience-platform/en/triggering-custom-events.html "Sitecore Triggering events")

[https://doc.sitecore.com/developers/90/sitecore-experience-platform/en/convert-an-outcome.html](https://doc.sitecore.com/developers/90/sitecore-experience-platform/en/convert-an-outcome.html "Sitecore outcome convert")

Table of contents [Dive into Sitecore Cortex and Machine Learning - Introduction](https://www.brimit.com/blog/dive-sitecore-cortex-machine-learning-introduction "Intro")

Read next [Part 3 - Processing engine Projection models and Datasets](https://www.brimit.com/blog/cortex-ml-3-processing-engine-projection-models-datasets "Sitecore Processing engine Projection models and Datasets")

##### Do you need help with your Sitecore project?

![](~/media/CD37043DFD1F491D8E92958A11959F75.ashx?la=en&amp;hash=D08B252DD3B41C09B588007541E5D22F)

[VIEW SITECORE SERVICES](https://www.brimit.com/expertise/sitecore-cms)

###### Author

[!\[sergey-200\](https://www.brimit.com/-/jssmedia/feature/blogs/authors/sergey-200.jpg?h=197&amp;iar=0&amp;w=200&amp;hash=0D636570F87F1C13C39E06EB19D9DF06)
Sergey Baranov
Sitecore MVP/ Senior Sitecore Developer](https://www.brimit.com/blog/author?authors=Sergey%20Baranov)

#### More on Sitecore

[!\[How Vercel Will Help You Save Effort When Deploying Sophisticated Sitecore Projects\](https://www.brimit.com/-/jssmedia/project/brimit/blog/2024/vercel_cover-image.png)
#Guides#How-toDXPE-commerce
##### How Vercel Will Help You Save Effort When Deploying Sophisticated Sitecore Projects
Optimize and accelerate the development and deployment of complex multisite Sitecore projects.
Alexei Vershalovich on July 17, 2024](https://www.brimit.com/blog/how-vercel-will-help-you-save-effort-when-deploying-sophisticated-sitecore-projects)

[!\[Training Up Tomorrow's Sitecore MVPs: a Mentoring Success Story\](https://www.brimit.com/-/jssmedia/project/brimit/blog/2023/sitecore-mentoring---cover-image.png)
#How-toDXP
##### Training Up Tomorrow's Sitecore MVPs: a Mentoring Success Story
How to participate in the Sitecore Mentor program and help younger colleagues jump-start a career in Sitecore development.
Sergey Baranov on October 2, 2023](https://www.brimit.com/blog/training-up-tomorrows-sitecore-mvps)

[!\[Going Headless. Part 2: When a Headless CMS Is Your Best Bet (if you have Sitecore)\](https://www.brimit.com/-/jssmedia/project/brimit/blog/2022/headless/adobestock_456986731.jpg)
#How-toDXPE-commerce
##### Going Headless. Part 2: When a Headless CMS Is Your Best Bet (if you have Sitecore)
Discover how a headless CMS can benefit organizations that use Sitecore.
Daniil Raschupkin, Palina Trokhautsava on September 15, 2022](https://www.brimit.com/blog/going-headless-part-2-when-a-headless-cms-is-your-best-bet-if-you-have-sitecore)

![](https://bat.bing.net/action/0?ti=187017043&amp;tm=gtm002&amp;Ver=2&amp;mid=2562600f-0c59-413f-917b-2912410f2332&amp;bo=2&amp;gtm_tag_source=1&amp;pi=0&amp;lg=en-US&amp;sw=800&amp;sh=600&amp;sc=24&amp;nwd=1&amp;tl=Sitecore%20Cortex%20and%20ML%3A%20Part%202%20-%20Adding%20custom%20events,%20facets%20and%20models&amp;kw=sitecore,%20Cortex,%20Machine%20Learning,%20ML,%20custom%20events,%20facets%20&amp;p=https%3A%2F%2Fwww.brimit.com%2Fblog%2Fcortex-ml-2-adding-custom-events-facets-models&amp;r=&amp;lt=283&amp;evt=pageLoad&amp;sv=2&amp;asc=D&amp;cdb=AQAY&amp;rn=779250)