Personalizing Sitecore pages from form data, Part 2: the codeless way, without a developer
In part 1, we looked at the old way of storing form data on a Sitecore contact. A developer creates a custom facet, a model builder, a JSON file, config files, and a submit action, and deploys all of it. When the marketer wants one more field, the whole cycle starts again.
You can read this part on its own. Here, we do the same job with the Generic Personalization module, also called the codeless schema module. Sitecore released it to solve exactly this problem. We will install it, build a "dine preference" form, check the data on the contact, and personalize the home page.
Codeless schema module:
Contact area:
You can read this part on its own. Here, we do the same job with the Generic Personalization module, also called the codeless schema module. Sitecore released it to solve exactly this problem. We will install it, build a "dine preference" form, check the data on the contact, and personalize the home page.
Codeless schema module:
The module saves form answers on the contact with no custom facet code. It works because it has one facet, called FacetAreas, and the shape of that facet never changes.
Contact area:
Inside FacetAreas, the data is grouped into areas. An area is a named group, and its name is the name of the form. For example, a form called "Dine Preference" creates the area "Dine Preference". Inside an area, every value has a key (the name of the form field) and a value (the answer).
This is how a contact looks after a dine preference form:
This is how a contact looks after a dine preference form:
Data stored in contact{
"Areas": [
{
"Name": "Dine Preference",
"Keys": [
{ "Name": "Cuisine", "Value": "Italian" },
{ "Name": "Location", "Value": "Dubai" },
{ "Name": "Budget", "Value": "150" },
{ "Name": "CheckInDate", "Value": "2026-11-05 00:00" }
]
}
]
}
So when the marketer adds a new field, it is saved from the next submission. That is the whole idea. There is no new facet class, no model JSON, and no deployment. It saves whatever fields the form has.One thing to remember. The key comes from the field name, so if we rename a field, the key changes, and a rule that used the old name stops working.
Install the module with SIF
- The module installs with SIF, the Sitecore Install Framework. This is the same PowerShell tool that installs Sitecore.
- Before we start, we need a working Sitecore instance. I used Sitecore 10.5 with the XP0 topology, which means one server for the CM and one for xConnect.
- xConnect is running, and xDB is enabled because the module saves data in xDB.
- PowerShell opened as administrator, with the SIF module available. If Sitecore was installed with SIF, it is already there. The name of the CM site in IIS. Mine is sc105sc.dev.local.
- The Generic Personalization deploy package for the Sitecore version, extracted to a folder.
- My download had two web deploy packages, one for the CM and one for xConnect, plus a script called InstallModule.ps1 and SIF config files such as GP-XP0.json.
- Open PowerShell as administrator in the extracted folder and run the script
| Location | Files added |
|---|---|
CM bin |
Five DLLs: Extensions.Forms, Model.XConnect, Rules, Rules.XConnect and Services |
CM App_Config\Modules\GenericPersonalization |
Sitecore.GenericPersonalization.Services.config and Sitecore.GenericPersonalization.XConnect.Client.config |
| CM items | The "Save Contact Areas" submit action, and the "Contact Areas" rule conditions |
xConnect bin and App_Data\Models |
The model DLLs, and Sitecore.XConnect.GenericPersonalization.Model, 1.0.json |
| Automation engine | The two DLLs, plus XML files for the model, for segmentation predicates, and one that tells the engine to load the FacetAreas facet with each contact |
| Index worker and processing engine | The same model JSON in App_Data\Models |
Look at this list again. These are the same kinds of files we created by hand in part 1: a DLL, a model JSON, a CM config, and an automation engine XML. The module is also a custom model. Sitecore built it once with a generic facet, and the installer deploys it. We never write or maintain these files.
Let's create a form for capturing user "dine preference", and we will personalize it without developers creating facets
We have created the form; let's make the form fields added to the contact facet automatically on user form submission.
We can achieve this with a form submit action, "Save Contact Areas" without any custom code, which is installed from the codeless schema module.
All the form fields will be added to the user contact facet as a new Contact area (Dine Preference).
In the above form, if the marketer adds any new fields, they will be automatically added to the Contact area. We have now added 2 new fields: Name and Email.
But the above Save Contact Areas won't identify the user, so the data submitted from the form goes as anonymous.
If we need to add the form fields to be a part of the particular contact, then we need to identify the user. As an out-of-the-box submit action is not available, we need to create a custom submit action in the following path in Sitecore "/sitecore/system/Settings/Forms/Submit Actions"
and place the DLL in CM for the custom submit action below
and place the DLL in CM for the custom submit action below
Contact identification based on
public class IdentifyByEmail : SubmitActionBase<string>
{
public const string IdentifierSource = "dineDemo";
public IdentifyByEmail(ISubmitActionData data) : base(data) { }
protected override bool TryParse(string value, out string target)
{
target = value ?? string.Empty;
return true;
}
protected override bool Execute(string data, FormSubmitContext formSubmitContext)
{
// read the value of the form field named "Email"
var email = formSubmitContext.Fields
.OfType<StringInputViewModel>()
.Where(f => string.Equals(f.Name, "Email", StringComparison.OrdinalIgnoreCase))
.Select(f => (f.Value ?? string.Empty).Trim().ToLowerInvariant())
.FirstOrDefault(v => v.Length > 0);
if (string.IsNullOrEmpty(email))
{
return true; // no email, the visitor stays anonymous. Do not block the form.
}
// the full name is saved later, see the facet code below
var fullName = formSubmitContext.Fields
.OfType<StringInputViewModel>()
.Where(f => string.Equals(f.Name, "Name", StringComparison.OrdinalIgnoreCase))
.Select(f => (f.Value ?? string.Empty).Trim())
.FirstOrDefault(v => v.Length > 0);
// Sitecore's own services are resolved from its service provider
var manager = ServiceLocator.ServiceProvider.GetRequiredService<IContactIdentificationManager>();
var result = manager.IdentifyAs(new KnownContactIdentifier(IdentifierSource, email));
if (!result.Success)
{
Log.Warn("Identify By Email failed: " + result.ErrorCode + " " + result.ErrorMessage, this);
}
return true;
}
}
To verify directly in SQL, use the query below from the database sc105_Xdb.Collection.Shard0 and sc105_Xdb.Collection.Shard1
Contact with Facets SQL query
SELECT c.ContactId,
i.Source,
CONVERT(varchar(200), i.Identifier) AS Identifier,
f.FacetKey,
CONVERT(nvarchar(max), f.FacetData) AS Json
FROM xdb_collection.Contacts c
JOIN xdb_collection.ContactFacets f
ON f.ContactId = c.ContactId AND f.FacetKey = 'FacetAreas'
LEFT JOIN xdb_collection.ContactIdentifiers i
ON i.ContactId = c.ContactId AND i.IdentifierType = 1;
Saved Facets from the form
{
"@odata.type": "#Sitecore.GenericPersonalization.Model.XConnect.Facets.FacetAreasData.FacetAreas",
"Areas": [
{
"Name": "Dine Preference",
"Keys": [
{
"Name": "Name",
"Value": "Nelson Jesudoss"
},
{
"Name": "Location",
"Value": "Dubai"
},
{
"Name": "Cuisine",
"Value": "Italian"
},
{
"Name": "Budget",
"Value": "150"
},
{
"Name": "CheckInDate",
"Value": "2026-11-12 00:00"
}
]
}
]
}
When to use the old way instead of the codeless schema:
The module is simple because it saves everything in one generic shape. That is also its limit. Three things matter.
The module is simple because it saves everything in one generic shape. That is also its limit. Three things matter.
- Values are saved as text. In a custom facet, Budget is a number, so Sitecore knows 200 is bigger than 150, and a number field cannot hold the text "lots". In the module, Budget is saved as the text "150". The rule conditions help, but SQL, reports, and segments only see text. We cannot easily sort contacts by budget or add them up.
- Experience Profile does not show the contact areas. The contact page has cards for the standard facets only. The data is saved, and we can see it in SQL, but not on that screen. Showing it requires a custom card, which a developer would have to build. Also, to identify the user, we need to write the following code.
Saved Email and Name to Facet for Experience Profile
client.SetFacet(contact, EmailAddressList.DefaultFacetKey,
new EmailAddressList(new EmailAddress(email, true), "Work"));
if (!string.IsNullOrEmpty(fullName))
{
// "Nelson Jesudoss" becomes first name "Nelson" and last name "Jesudoss"
var parts = fullName.Split(new[] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries);
client.SetFacet(contact, PersonalInformation.DefaultFacetKey, new PersonalInformation
{
FirstName = parts[0],
LastName = parts.Length > 1 ? parts[1] : string.Empty
});
}
client.Submit();
- Complex data does not fit. In a custom facet, we can put an object, or a list of objects, inside the facet, for example, a list of related menus. With the module, the shape is fixed: an area with keys and one text value for each key. We can use one form per group of fields, or add a prefix to the field names, but a list of objects does not fit well.
Personalize the page:
Contact Areas Rules added by the codeless schema module; let's use it to personalize the components.
Now the marketer can build a rule. On the home page, I added a promo component and opened its personalization. The component's view has no code that checks the visitor. It always renders the banner, and the rules decide who sees it. I made two rules:
When the visitor visited the site again, the visitor’s preference loaded automatically based on the personalization rules.
Contact Areas Rules added by the codeless schema module; let's use it to personalize the components.
Now the marketer can build a rule. On the home page, I added a promo component and opened its personalization. The component's view has no code that checks the visitor. It always renders the banner, and the rules decide who sees it. I made two rules:
- A rule called "Italian lovers". Its condition is: where the current contact's contact area "Dine Preference", key "Cuisine", compares to "contains" "Italian". It has no action, so the component shows as normal.
- A rule called "Everyone else". Its condition is "where true (actions always execute)", and its action is "hide rendering".
Note:
If we have an Email field like above with data type as email in the form, this will be skipped when storing to FacetAreas, and the module writes a log warning: "[Email] field was not supported and will not be saved into FacetAreas."
Only the following fields will be stored automatically in Contact Areas. Any other fields need to be added manually by the developer using the Custom Submit action.
- Single-Line Text
- Multi-Line Text
- Checkbox
- Date
- Number
- Checkbox List
- Dropdown List
- List Box
- Radio Button List
Let's learn and grow together, happy programming 😊








Comments
Post a Comment