Custom automation triggers
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 (implementingIAutomationTriggerData) 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
IAutomationTriggerDispatcherand callingFireTrigger<TTrigger>. - Triggers operate on contacts – the object you fire a trigger against must be a
ContactInfo. - You can override the
Evaluatemethod 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.
Create custom triggers without data
To create a trigger that starts a process without using or passing additional data:
- Create a class inheriting from
AutomationTrigger.- You can optionally override the
Evaluatemethod 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.
- You can optionally override the
- Register the trigger via the
RegisterAutomationTrigger<TTrigger>assembly attribute.
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:
- Create a data class implementing the
IAutomationTriggerDatainterface.- 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.
- Provide a stable, unique
- Create a trigger class inheriting from
AutomationTrigger<TData>, whereTDatais your data class.- You can optionally override the
Evaluatemethod to dynamically control whether the process starts. The method has aTDataparameter 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.
- You can optionally override the
- Register the trigger via the
RegisterAutomationTrigger<TTrigger>assembly attribute.
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; }
}
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:
- Create a properties class implementing the
IAutomationTriggerPropertiesinterface. - Define each property by adding a C# property in the class.
- Assign editing components to define the visual interface for properties in the trigger’s configuration dialog.
- See Step properties and Property guidelines for detailed information. Trigger properties provide the same UI form component options as custom step properties.
C#Example - Purchase trigger propertiesusing 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; } - (Optional) Create a data class implementing the
IAutomationTriggerDatainterface.- See Create custom triggers with typed data for more information.
- Create a trigger class inheriting from
AutomationTrigger<TData, TProperties>, whereTPropertiesis your properties class andTDatayour 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 requiredIdentifiermember) to use as theTDataparameter. - 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.
- If you wish to have a trigger with configurable properties, but without trigger data, create a dummy class implementing
- Override the
Evaluatemethod 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
TDataparameter providing an instance of the data class populated with values set when the trigger was fired. - See Control process start with Evaluate.
- Register the trigger via the
RegisterAutomationTrigger<TTrigger>assembly attribute.
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 |
|
|
|
|
|
|
|
|
|
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.
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 anIDENTIFIERconstant.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 theKentico.Xperience.Admin.Base.Iconsclass.Description– hover text displayed as a tooltip in the Automation Builder trigger selection dialog.
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. ThetriggerDatainstance must match the trigger’s declared data type.
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:
- Event handlers reacting to application-level events (e.g., contact updated)
- Custom middleware, integration code or controllers
- API endpoints that receive external webhooks
- Scheduled tasks (regularly start a process for a set of contacts matching certain criteria)
- See: Time-based triggers
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
CMSAutomationTriggerQueueCapacityapplication 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
InvalidOperationExceptionwhen:- The trigger type specified in the
TTriggergeneric is not registered viaRegisterAutomationTrigger<TTrigger>. - Trigger data is supplied via
AutomationTriggerDispatchfor a trigger that does not declare a data type. - The supplied trigger data is not an instance of the trigger’s declared data type.
- The trigger type specified in the
Example - Fire from an event handler
The following example demonstrates how to fire an automation trigger from an object event handler for contact updates.
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.
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:
Create a scheduled task.
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.In the task’s
Executemethod:- Load the contacts for which you want to fire the trigger (e.g., contacts with low engagement, contacts classified as hot leads).
- Fire the trigger for each selected contact.
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
Evaluateoverride 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
Identifieron yourIAutomationTriggerDatatypes and never change it. - Use the init accessor for trigger data properties to ensure immutability.
- All
DateTimevalues 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
identifierof 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:
- Create a new trigger class with a new identifier (e.g., with a
_V2suffix), along with a new data and properties class if required. - Keep the old version registered and functional.
- Instruct marketers to only use the new trigger version, for example by updating the old version’s display name and description.
- Once all processes using the old trigger are disabled or deleted, the old trigger class can be removed.