Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Send custom properties to ODP

How to send custom properties to Optimizely Data Platform (ODP) from Optimizely Configured Commerce

Extend your Optimizely Configured Commerce data by sending custom properties to Optimizely Data Platform (ODP). This lets you enrich customer profiles, events, and transactions with additional attributes, delivered through real-time sends or scheduled integration jobs.

Configured Commerce supports two pipelines for pushing data into ODP:

  • Real‑time transmission
  • Batch integration jobs (for historical backfills)

This article explains how both pipelines work, where you can wire in custom property hooks, and how to implement them cleanly.

How sending data works

Real-time

  1. An entity change generates one or more change IDs.
  2. The system runs a base SQL query for those IDs, shaping them into the ODP payload.
  3. It calls your IOdpDataCustomizationService<T>.GetDataCustomization(...) to merge extra fields.
  4. The system posts the enriched records to the ODP REST API in configurable batch sizes.

Integration jobs

  1. A scheduled job runs a full SQL extract of all target data.
  2. The system writes results as CSV and uploads them to an Amazon S3 bucket.
  3. ODP ingests the CSV file asynchronously.

You can inject custom properties in two ways without altering the base SQL, using one of the following:

  • Static preload (Mode A)
  • Dynamic factory (Mode B)

Export enrichment mechanisms (Bulk export)

Mode A: Static preload

  • Entry point – Override GetCustomizedDataQuery(filter) in your postprocessor.
  • Trigger – Returns a non-null IDictionary<Guid, IDictionary<string,string>>.
  • Behavior – Platform merges your per-GUID dictionary into each CSV row. The system replaces missing keys with DBNull.Value.
  • Best for – Fast lookup on moderate-sized data (fits in memory).
protected override IDictionary<Guid, IDictionary<string,string>> GetCustomizedDataQuery(
    CustomizationFilterParameter? filter)

Example

public class ProductOdpDataCustomizationService
    : IOdpDataCustomizationService<Product>
{
    private static readonly string[] PropertyNames = { "anyProperty1", "anyProperty2" };
    private readonly IUnitOfWorkFactory factory;

    public IDictionary<Guid, IDictionary<string,string>> GetDataCustomization(
        CustomizationFilterParameter? filter)
    {
        var result = new Dictionary<Guid, IDictionary<string,string>>();

        var uow = factory.GetUnitOfWork();
        var props = uow.GetRepository<CustomProperty>()
            .GetTableAsNoTracking()
            .Where(cp => cp.ParentTable == "Product"
                      && cp.ModifiedOn >= filter.From
                      && PropertyNames.Contains(cp.Name))
            .Select(cp => new { cp.ParentId, cp.Name, cp.Value });

        foreach (var row in props)
        {
            if (!result.TryGetValue(row.ParentId, out var dict))
            {
                dict = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
                result[row.ParentId] = dict;
            }
            dict[row.Name] = row.Value ?? string.Empty;
        }
        return result;
    }
}

Mode B: Dynamic factory

Used only if GetCustomizedDataQuery returns null.

🚧

Important

You must create a copy of JobPostprocessorBaseOdpExport and the base postprocessor for your entity, such as JobPostprocessorProductOdpExport, before using this method to extend Configure Commerce.

  • SchemaGetAdditionalColumnNames() returns a list of extra column names.
  • ValuesGetAdditionalDataFactory() returns a Func<Guid, object[]> invoked per row.
  • Behavior – Positionally maps array values to column names; short arrays will be trailing DBNull.Value.
  • Best for – Compute-heavy fields or very large datasets (minimal memory).
protected override IReadOnlyCollection<string> GetAdditionalColumnNames();
protected override Func<Guid, object[]> GetAdditionalDataFactory();

Example

public class JobPostprocessorCustomProductOdpExport
    : JobPostprocessorProductOdpExport
{
    private static readonly string[] Columns = { "anyProperty1", "anyProperty2" };
    private readonly IUnitOfWork uow;

    protected override IDictionary<Guid, IDictionary<string,string>> GetCustomizedDataQuery(...) 
        => null;

    protected override IReadOnlyCollection<string> GetAdditionalColumnNames() 
        => Columns;

    protected override Func<Guid, object[]> GetAdditionalDataFactory() 
        => productId => {
            var props = uow.GetRepository<CustomProperty>()
                .GetTableAsNoTracking()
                .Where(cp => cp.ParentTable=="Product"
                          && cp.ParentId==productId
                          && Columns.Contains(cp.Name))
                .ToList();
            var dict = props.ToDictionary(cp=>cp.Name, cp=>cp.Value ?? string.Empty,
                                         StringComparer.OrdinalIgnoreCase);
            return new object[]
            {
                dict.GetValueOrDefault("anyProperty1",""),
                dict.GetValueOrDefault("anyProperty2","")
            };
        };
}

Comparison

CriterionStatic preloadDynamic factory
Entry pointGetCustomizedDataQuery(filter)GetAdditionalColumnNames + factory
Memory footprintPreloads all extra dataComputes per row
Column uniformityMissing replaced by DBNullFixed schema
Best forFast lookup of known valuesExpensive or huge data sets

Developer extensibility

Define custom fields for both real-time and batch without touching base SQL.

public interface IOdpDataCustomizationService<T>
    : IDependency, IExtension
    where T : class, IBusinessObject
{
    IDictionary<Guid, IDictionary<string,string>>? GetDataCustomization(
        CustomizationFilterParameter? filter);
}

CustomizationFilterParameter

  • IdFilter – Optional ICollection<Guid> to enrich only specific IDs
  • From/To – Inclusive timestamp bounds

Integration points

  • Real-timeOdpUpdatedEntityEventService invokes GetDataCustomization for changed IDs.
  • Batch exportJobPostprocessorBaseOdpExport invokes it before CSV generation .

Where customization hooks run

Bulk export flow

  1. Uses SQL to read base rows.
  2. Applies customization .
    1. StaticGetCustomizedDataQuery(filter)
    2. DynamicGetAdditionalColumnNames() + GetAdditionalDataFactory()
  3. Writes enriched CSV then uploads to S3 and ODP ingestion .

Real-time API sync

  1. Detects entity changes then collects change IDs.
  2. Reruns base SQL snapshot for those IDs.
  3. Merges extra fields with IOdpDataCustomizationService.
  4. Batches client‐side and POST to ODP API.