Custom automation triggers

Advanced license required

Features described on this page require the Xperience by Kentico Advanced license tier.

Xperience by Kentico allows developers to create custom triggers that start marketing automation processes from application code. Custom triggers appear in the Automation Builder UI alongside the built-in triggers (Form submission, Registration, Custom activity) and can be selected by marketers when creating processes.

Use custom triggers when you need to start automation processes based on events that occur in your application, such as:

  • A purchase is completed in your digital commerce system
  • A contact is updated (e.g., in a specific field)
  • A webhook from an external application is received
  • A regularly scheduled task runs (allows you to start a process for a set of contacts matching certain criteria)

Overview

Custom triggers are classes implemented according to one of the following patterns:

  • Triggers without data or properties – basic triggers that inherit from AutomationTrigger.
  • Triggers with typed data – inherit from AutomationTrigger<TData>. Such triggers carry a typed data payload (implementing IAutomationTriggerData) that can be accessed by custom steps in the process.
  • Triggers with data and properties – inherit from AutomationTrigger<TData, TProperties>. Such triggers additionally have properties configured by marketers in the Automation Builder UI, which can be used to evaluate whether the process should start when the trigger is fired.

Key points

  • Fire triggers from code by injecting IAutomationTriggerDispatcher and calling FireTrigger<TTrigger>.
  • Triggers operate on contacts – the object you fire a trigger against must be a ContactInfo.
  • You can override the Evaluate method in trigger classes to control whether a process starts when the trigger is fired. Depending on the trigger’s base class, you can evaluate using the trigger’s data and/or properties configured by marketers.
  • Register triggers via the RegisterAutomationTrigger<TTrigger> assembly attribute.
  • Trigger classes must be stateless. When a trigger is fired, the system creates and reuses a single instance of the trigger class to evaluate and start all processes with the given trigger.

Overview of custom automation trigger components

Create custom triggers without data

To create a trigger that starts a process without using or passing additional data:

  1. Create a class inheriting from AutomationTrigger.
    • You can optionally override the Evaluate method to dynamically control whether the process starts. See Control process start with Evaluate.
    • Trigger classes must be stateless. When a trigger is fired, the system creates and reuses a single instance of the trigger class to evaluate and start all processes with the given trigger.
  2. Register the trigger via the RegisterAutomationTrigger<TTrigger> assembly attribute.
C#
Example - Purchase trigger

using CMS.Automation;
using Kentico.Xperience.Admin.Base;

using Acme.Automation;

[assembly: RegisterAutomationTrigger<PurchaseTrigger>(
    identifier: PurchaseTrigger.IDENTIFIER,
    displayName: "Purchase completed",
    Description = "Fires when a customer completes a purchase.",
    IconName = Icons.ShoppingCart)]

namespace Acme.Automation;

public class PurchaseTrigger : AutomationTrigger
{
    public const string IDENTIFIER = "Acme.PurchaseTrigger";
}

Marketers can select the trigger when designing automation processes.

All processes using the trigger start whenever you fire the trigger from your application’s code (depending on the recurrence settings of the given process).

Create custom triggers with typed data

To create a trigger that starts a process and provides structured data to steps:

  1. Create a data class implementing the IAutomationTriggerData interface.
    • Provide a stable, unique Identifier. We recommend using a unique prefix in your identifiers to prevent conflicts. The system uses the identifier internally to store and retrieve the trigger data payload within automation processes. Never change the value once the trigger is in use.
    • Add properties to hold the data you want to capture when the trigger fires.
  2. Create a trigger class inheriting from AutomationTrigger<TData>, where TData is your data class.
    • You can optionally override the Evaluate method to dynamically control whether the process starts. The method has a TData parameter providing an instance of the data class populated with values set when the trigger was fired. See Control process start with Evaluate.
    • Trigger classes must be stateless. When a trigger is fired, the system creates and reuses a single instance of the trigger class to evaluate and start all processes with the given trigger.
  3. Register the trigger via the RegisterAutomationTrigger<TTrigger> assembly attribute.
C#
Example - Purchase trigger data

using CMS.Automation;

namespace Acme.Automation;

public class PurchaseTriggerData : IAutomationTriggerData
{
    // Stable, unique identifier of the trigger data type
    public string Identifier => "Acme.PurchaseTriggerData";

    public string OrderId { get; init; }

    public decimal TotalAmount { get; init; }

    public string Currency { get; init; }
}
C#
Example - Purchase trigger with data

using System.Threading;
using System.Threading.Tasks;

using CMS.Automation;
using Kentico.Xperience.Admin.Base;

using Acme.Automation;

[assembly: RegisterAutomationTrigger<HighValuePurchaseTrigger>(
    identifier: HighValuePurchaseTrigger.IDENTIFIER,
    displayName: "Purchase over 100",
    Description = "Fires when a customer completes a purchase with an order total of 100 or more.",
    IconName = Icons.ShoppingCart)]

namespace Acme.Automation;

public class HighValuePurchaseTrigger : AutomationTrigger<PurchaseTriggerData>
{
    public const string IDENTIFIER = "Acme.HighValuePurchaseTrigger";

    public const decimal MINIMUM_PURCHASE_AMOUNT = 100.00m;

    public override Task<bool> Evaluate(
        AutomationTriggerContext context,
        PurchaseTriggerData triggerData,
        CancellationToken cancellationToken)
    {
        // In this example, the trigger fires only if the purchase total meets a minimum requirement
        return Task.FromResult(triggerData.TotalAmount >= MINIMUM_PURCHASE_AMOUNT);
    }
}

Marketers can select the trigger when designing automation processes.

All processes using the trigger start whenever you fire the trigger from your application’s code (depending on the recurrence settings of the given process). When firing the trigger, prepare a trigger data payload and use the AutomationTriggerDispatch constructor overload that accepts trigger data.

The trigger data is JSON-serialized and stored with the automation process state. Custom steps within the process can read the trigger data via AutomationProcessContext.GetTriggerData<T>. See Access trigger data.

Create custom triggers with properties

To create a trigger that allows marketers to configure the process start conditions in the Automation Builder UI:

  1. Create a properties class implementing the IAutomationTriggerProperties interface.
  2. Define each property by adding a C# property in the class.
  3. Assign editing components to define the visual interface for properties in the trigger’s configuration dialog.
    C#
    Example - Purchase trigger properties
    
     using CMS.Automation;
     using Kentico.Xperience.Admin.Base.FormAnnotations;
    
     namespace Acme.Automation;
    
     public class PurchaseTriggerProperties : IAutomationTriggerProperties
     {
         // Minimum order total to start the automation process
         [DecimalNumberInputComponent(Label = "Minimum order total", Order = 10)]
         [RequiredValidationRule]
         public decimal MinimumTotal { get; set; } = 100.00m;
     }
     
  4. (Optional) Create a data class implementing the IAutomationTriggerData interface.
  5. Create a trigger class inheriting from AutomationTrigger<TData, TProperties>, where TProperties is your properties class and TData your data class.
    • If you wish to have a trigger with configurable properties, but without trigger data, create a dummy class implementing IAutomationTriggerData (with only the required Identifier member) to use as the TData parameter.
    • Trigger classes must be stateless. When a trigger is fired, the system creates and reuses a single instance of the trigger class to evaluate and start all processes with the given trigger.
  6. Override the Evaluate method to control whether the process starts based on the configured properties.
    • The method receives the properties instance populated with values from the trigger’s configuration.
    • The method has a TData parameter providing an instance of the data class populated with values set when the trigger was fired.
    • See Control process start with Evaluate.
  7. Register the trigger via the RegisterAutomationTrigger<TTrigger> assembly attribute.
C#
Example - Purchase trigger with properties

using System.Threading;
using System.Threading.Tasks;

using CMS.Automation;
using Kentico.Xperience.Admin.Base;

using Acme.Automation;

[assembly: RegisterAutomationTrigger<PurchaseWithMinimumTotalTrigger>(
    identifier: PurchaseWithMinimumTotalTrigger.IDENTIFIER,
    displayName: "Purchase with minimum total",
    Description = "Fires when a customer completes a purchase with an order total greater than a specified value.",
    IconName = Icons.ShoppingCart)]

namespace Acme.Automation;

public class PurchaseWithMinimumTotalTrigger : AutomationTrigger<PurchaseTriggerData, PurchaseTriggerProperties>
{
    public const string IDENTIFIER = "Acme.PurchaseWithMinimumTotalTrigger";

    public override Task<bool> Evaluate(
        AutomationTriggerContext context,
        PurchaseTriggerProperties triggerProperties,
        PurchaseTriggerData triggerData,
        CancellationToken cancellationToken)
    {
        // In this example, the trigger fires only if the purchase total
        // is greater than a value specified in the trigger properties
        return Task.FromResult(triggerData.TotalAmount >= triggerProperties.MinimumTotal);
    }
}

Marketers can select the trigger when designing automation processes and configure its properties directly in the Automation Builder UI.

When you fire the trigger from your application’s code, the trigger’s Evaluate method runs for all processes using the trigger and their property configuration. If the method returns true, the given process starts (also depending on the recurrence settings of the process).

Control process start with Evaluate

Every trigger base class exposes an overridable Evaluate method that returns Task<bool>. Return true to start the automation process when the trigger fires, or false to skip it. The default implementation always returns true, so the process starts whenever the trigger fires.

The Evaluate signature depends on the trigger’s base class:

Base class

Evaluate parameters

AutomationTrigger

  • AutomationTriggerContext context
  • CancellationToken cancellationToken

AutomationTrigger<TData>

  • AutomationTriggerContext context
  • TData triggerData
  • CancellationToken cancellationToken

AutomationTrigger<TData, TProperties>

  • AutomationTriggerContext context
  • TProperties properties
  • TData triggerData
  • CancellationToken cancellationToken

You can implement Evaluate logic based on the trigger data, the properties of specific trigger instances, or other general conditions.

To access the contact the trigger was fired against, call the GetProcessedObject extension method (from the CMS.ContactManagement namespace) on AutomationTriggerContext. The method returns a ContactInfo object representing the contact.

C#
Example - Conditional trigger based on contact properties

using System.Threading;
using System.Threading.Tasks;

using CMS.Automation;
using CMS.ContactManagement;

namespace Acme.Automation;

public class KnownEmailContactTrigger : AutomationTrigger
{
    public override async Task<bool> Evaluate(AutomationTriggerContext context, CancellationToken cancellationToken)
    {
        // Gets the contact the trigger was fired against
        ContactInfo contact = await context.GetProcessedObject(cancellationToken);

        // Starts the process only for contacts that have a known email address
        return !string.IsNullOrEmpty(contact.ContactEmail);
    }
}

Evaluate runs in the background before the process starts. Keep it lightweight – it is called for every fired trigger.

Register custom triggers

Register custom triggers using the RegisterAutomationTrigger<TTrigger> assembly attribute. We recommend placing the attribute at the top of the trigger’s source file.

Specify the following parameters:

  • identifier – a unique, stable string identifier for the trigger. We recommend using a unique prefix in your identifiers to prevent conflicts. For example, include your company’s name as a prefix (Acme.PurchaseTrigger). Never change this value once the trigger is deployed and used in automation processes.
    For easy access and reuse, store the identifier value in the trigger class within an IDENTIFIER constant.
  • displayName – the name displayed in the Automation Builder trigger selection dialog.

You can also set the following optional properties:

  • IconName – the icon displayed for the trigger tile in the builder. Use icons from the set of icons starting with the xp- prefix, referenced by the constants in the Kentico.Xperience.Admin.Base.Icons class.
  • Description – hover text displayed as a tooltip in the Automation Builder trigger selection dialog.
C#
Trigger registration example

using CMS.Automation;
using Kentico.Xperience.Admin.Base;

[assembly: RegisterAutomationTrigger<PurchaseTrigger>(
    identifier: PurchaseTrigger.IDENTIFIER,
    displayName: "Purchase completed",
    Description = "Fires when a customer completes a purchase.",
    IconName = Icons.ShoppingCart)]

Once registered, the trigger appears in the trigger selection dialog when creating or editing an automation process in the Automation Builder.

Deleting or unregistering a trigger that is used by an existing automation process breaks the process. If a process contains an “unknown” trigger, the Automation Builder UI will not be available to change the trigger. The process cannot be recovered until the trigger is restored and registered.

Fire triggers from code

To fire custom triggers, inject the IAutomationTriggerDispatcher service and call its FireTrigger<TTrigger> method, where TTrigger is a trigger class inheriting from one of the AutomationTrigger base class variants.

FireTrigger<TTrigger> accepts an AutomationTriggerDispatch parameter, which describes a single trigger fire request. Construct the AutomationTriggerDispatch object with the following parameters:

  • ContactInfo contact – the contact for which the triggered automation processes will start.
  • IAutomationTriggerData triggerData – use the constructor overload with this parameter for triggers with data. The triggerData instance must match the trigger’s declared data type.
C#
Firing a trigger with data

using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Threading.Tasks;

using CMS.Automation;
using CMS.ContactManagement;

using Acme.Automation;

public class CheckoutController(IAutomationTriggerDispatcher triggerDispatcher) : Controller
{
    // Creates a new order when a purchase is completed
    public async Task<IActionResult> CreateOrder(CancellationToken cancellationToken)
    {
        // ...

        // Gets the current contact
        ContactInfo currentContact = ContactManagementContext.GetCurrentContact();

        // Fires a trigger with populated trigger data
        await triggerDispatcher.FireTrigger<HighValuePurchaseTrigger>(
            new AutomationTriggerDispatch(currentContact, new PurchaseTriggerData
            {
                OrderId = "ORD-2026-00451",
                TotalAmount = 149.99m,
                Currency = "USD"
            }), cancellationToken); 

        // ...
    }
}

Where to fire triggers

You can fire triggers from any code within your Xperience application. Common locations are:

Chaining automation processes

If you wish to start an automation process from another process, we recommend that you use custom activities – log a custom activity using the built-in Log custom activity step and then select the Custom activity trigger for the target processes.

It is possible to fire triggers from the code of custom automation actions, but the activity approach avoids the need to develop automation components and also ensures a reliable record of the event in the activity log for the related contacts.

Trigger firing behavior

  • FireTrigger<TTrigger> enqueues the trigger for background processing and returns immediately without blocking your application code. Triggered automation processes then run in a background thread.
  • Fired triggers are held in a bounded in-memory queue shared by all custom triggers and the built-in Form trigger. If the queue becomes saturated – for example, when a large number of triggers are fired before the processes finish or reach a Wait step – additional triggers are dropped and the system logs a warning. Increase the CMSAutomationTriggerQueueCapacity application setting if this occurs.
  • If no automation process is configured to start from the trigger, the call is a silent no-op.
  • The dispatcher throws an InvalidOperationException when:
    • The trigger type specified in the TTrigger generic is not registered via RegisterAutomationTrigger<TTrigger>.
    • Trigger data is supplied via AutomationTriggerDispatch for a trigger that does not declare a data type.
    • The supplied trigger data is not an instance of the trigger’s declared data type.

Example - Fire from an event handler

The following example demonstrates how to fire an automation trigger from an object event handler for contact updates.

C#
Firing a trigger from a contact update handler

using System.Threading;
using System.Threading.Tasks;

using CMS.Automation;
using CMS.ContactManagement;
using CMS.DataEngine;
using Kentico.Xperience.Admin.Base;

using Acme.Automation;

[assembly: RegisterAutomationTrigger<ContactUpdatedTrigger>(
    identifier: ContactUpdatedTrigger.IDENTIFIER,
    displayName: "Contact updated",
    Description = "Fires when an existing contact is updated.",
    IconName = Icons.UserCheckbox)]

namespace Acme.Automation;

public class ContactUpdatedTrigger : AutomationTrigger
{
    public const string IDENTIFIER = "Acme.ContactUpdatedTrigger";
}

// Event handler triggered after a contact object is updated
// Fires a custom automation trigger for the updated contact
public class ContactUpdatedTriggerHandler(IAutomationTriggerDispatcher triggerDispatcher) : IInfoObjectEventHandler<InfoObjectAfterUpdateEvent<ContactInfo>>
{
    // Invoked for synchronous contact updates (Set operation)
    public void Handle(InfoObjectAfterUpdateEvent<ContactInfo> infoObjectEvent)
    {
        ContactInfo contact = infoObjectEvent.InfoObject;

        // Fires the trigger for the updated contact (blocks on the async call)
        triggerDispatcher
            .FireTrigger<ContactUpdatedTrigger>(new AutomationTriggerDispatch(contact))
            .GetAwaiter().GetResult();
    }

    // Invoked by the asynchronous contact updates (SetAsync operation)
    public async Task HandleAsync(InfoObjectAfterUpdateEvent<ContactInfo> infoObjectEvent, CancellationToken cancellationToken)
    {
        ContactInfo contact = infoObjectEvent.InfoObject;

        // Fires the trigger for the updated contact
        await triggerDispatcher.FireTrigger<ContactUpdatedTrigger>(
            new AutomationTriggerDispatch(contact), cancellationToken);
    }
}

Example - Fire from a checkout controller

The following example demonstrates how to fire an automation trigger from a controller that handles the checkout process and order creation in a digital commerce project.

See Create custom triggers with typed data for the code of the fired trigger and the used trigger data.

C#
Firing a trigger from a controller

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Threading.Tasks;

using CMS.Automation;
using CMS.Commerce;
using CMS.ContactManagement;
using CMS.DataEngine;

using Kentico.Membership;

using Acme.Automation;

namespace Acme.Automation;

public class CheckoutController(
    IAutomationTriggerDispatcher triggerDispatcher,
    UserManager<ApplicationUser> userManager,
    IOrderCreationService<OrderData, PriceCalculationRequest, PriceCalculationResult, AddressDto> orderCreationService,
    IInfoProvider<OrderInfo> orderInfoProvider) : Controller
{
    // Creates a new order when a purchase is completed
    public async Task<IActionResult> CreateOrder(CancellationToken cancellationToken)
    {
        // Prepare order data from your checkout form
        var orderData = new OrderData
        {
            // Identifies the authenticated user for the order, if one exists
            BuyerIdentifier = BuyerIdentifier.FromMemberId(
                (await userManager.GetUserAsync(User))?.Id ?? 0),
            OrderNumber = "ORD-2026-00451",
            LanguageName = "en",
            BillingAddress = new AddressDto
            {
                FirstName = "John",
                LastName = "Doe",
                Email = "john@example.com",
                Line1 = "123 Main St",
                City = "New York",
                Zip = "10001",
                CountryID = 1,
                StateID = 1
            },
            ShippingAddress = null,
            OrderItems = new[]
            {
                new OrderItem
                {
                    ProductIdentifier = new ProductIdentifier { Identifier = 123 },
                    Quantity = 2
                }
            }
        };

        // Creates the order
        var orderId = await orderCreationService.CreateOrder(orderData, cancellationToken);
        var order = await orderInfoProvider.GetAsync(orderId, cancellationToken);

        // Gets the current contact
        ContactInfo currentContact = ContactManagementContext.GetCurrentContact();

        // Fires a trigger with trigger data populated from the order
        await triggerDispatcher.FireTrigger<HighValuePurchaseTrigger>(
            new AutomationTriggerDispatch(currentContact, new PurchaseTriggerData
            {
                OrderId = order.OrderNumber,
                TotalAmount = order.OrderGrandTotal,
                Currency = "USD"
            }), cancellationToken); 


        // Redirects users to a confirmation page
        return RedirectToAction("Confirmation", new { orderId });
    }
} 

Time-based triggers

While most automation triggers are implemented as a reaction to a specific event in your application, you can also fire triggers on a recurring schedule for a specific set of contacts. Use the following approach:

  1. Create a scheduled task.

  2. Configure the task schedule to control how often the trigger fires.

    Run scheduled tasks that fire triggers at most once per day. More frequent intervals are not recommended. On projects with a large number of contacts, each run may enqueue a large amount of background automation work and can lead to high application load.
  3. In the task’s Execute method:

    1. Load the contacts for which you want to fire the trigger (e.g., contacts with low engagement, contacts classified as hot leads).
    2. Fire the trigger for each selected contact.
C#
Example - Contact birthday trigger

using CMS.Automation;
using CMS.ContactManagement;
using CMS.DataEngine;
using CMS.Scheduler;
using Kentico.Xperience.Admin.Base;

using System;
using System.Collections.Generic;

using System.Threading;
using System.Threading.Tasks;

using Acme.Automation;

[assembly: RegisterScheduledTask(identifier: "Acme.ContactBirthdayAutomationTask", typeof(ContactBirthdayAutomationTask))]

[assembly: RegisterAutomationTrigger<ContactBirthdayTrigger>(
    identifier: ContactBirthdayTrigger.IDENTIFIER,
    displayName: "Contact birthday",
    Description = "Fires once per day for contacts whose birthday is on the given day.",
    IconName = Icons.Gift)]

namespace Acme.Automation;

public class ContactBirthdayTrigger : AutomationTrigger
{
    public const string IDENTIFIER = "Acme.ContactBirthdayTrigger";
}

public class ContactBirthdayAutomationTask(IAutomationTriggerDispatcher triggerDispatcher,
                                           IInfoProvider<ContactInfo> contactInfoProvider) : IScheduledTask
{
    public async Task<ScheduledTaskExecutionResult> Execute(ScheduledTaskConfigurationInfo task, CancellationToken cancellationToken)
    {

        DateTime today = DateTime.Today;

        // Loads contacts whose birthday (day and month) matches today
        IEnumerable<ContactInfo> contactsWithBirthday = await contactInfoProvider.Get()
                .WhereNotNull(nameof(ContactInfo.ContactBirthday))
                .WhereEquals($"DAY([{nameof(ContactInfo.ContactBirthday)}])".AsExpression(), today.Day)
                .WhereEquals($"MONTH([{nameof(ContactInfo.ContactBirthday)}])".AsExpression(), today.Month)
                .GetEnumerableTypedResultAsync(cancellationToken: cancellationToken);
        
        // Fires the trigger for each applicable contact
        foreach (ContactInfo contact in contactsWithBirthday)
        {
            await triggerDispatcher.FireTrigger<ContactBirthdayTrigger>(
                new AutomationTriggerDispatch(contact), cancellationToken);
        }

        return ScheduledTaskExecutionResult.Success;
    }
}

Best practices

Most best practices for custom actions also apply to custom triggers – error handling, data protection (GDPR), property guidelines, and dependency injection.

Keep triggers stateless and lightweight

  • When a trigger is fired, the system creates and reuses a single instance of the trigger class to evaluate and start all processes with the given trigger. Do not store process-specific state in trigger class members.
  • Keep any Evaluate override lightweight – it runs for every fired trigger before the process starts. Only retrieve data required for the evaluation conditions and avoid any operations that modify or create data.

Trigger evaluation timeout

Custom triggers have a maximum evaluation time of 2 minutes. If a trigger does not evaluate within this limit, it will not start (equivalent to a false result), and the system activates the CancellationToken passed to the Evaluate method.

Trigger data guidelines

  • Keep data payloads small – include only identifiers and values that downstream actions need.
  • Provide a stable, unique Identifier on your IAutomationTriggerData types and never change it.
  • Use the init accessor for trigger data properties to ensure immutability.
  • All DateTime values are serialized and deserialized as UTC.
  • Avoid storing personal or sensitive data (names, emails, API keys) directly in trigger data – store identifiers and look up details in downstream actions.
  • If trigger data cannot be deserialized (e.g., after a schema change or after the trigger is unregistered), downstream actions reading it receive null.

Updating and versioning triggers

  • Never change the identifier of a registered trigger or trigger data class – doing so breaks all processes that reference it.
  • Adding new optional properties to a trigger data class is safe – they receive default values for existing trigger configurations.
  • Removing or renaming properties in trigger data breaks deserialization for processes started before the change.
    • If you need to remove or replace a property without breaking existing configurations, you can remove the property’s UI form component attribute to hide it in the configuration dialog.

To version a trigger safely:

  1. Create a new trigger class with a new identifier (e.g., with a _V2 suffix), along with a new data and properties class if required.
  2. Keep the old version registered and functional.
  3. Instruct marketers to only use the new trigger version, for example by updating the old version’s display name and description.
  4. Once all processes using the old trigger are disabled or deleted, the old trigger class can be removed.