The July 28, 2026 refresh of Xperience by Kentico, released as version 31.7.0, completes the developer extensibility model for marketing automation. Custom actions already allowed projects to add their own operations to a process. The refresh adds custom triggers and custom conditions, giving developers first-class extension points for how a process starts, what it does, and how it branches.
Each extension appears as a native component in the Automation Builder. Developers implement and register the behavior in the Xperience application, while marketers place the resulting trigger, action, or condition into a process and configure its exposed properties. The visual process can finally describe the real business workflow instead of relying on technical custom activities and code running out of sight.
Kentico also released the "kentico-digital-experience" plugin in the KentiCopilot repository for AI-assisted development of automation components. A July 31 update added trigger and condition skills alongside the existing custom action skill.
The limitations of custom activity workarounds
Before first-class custom components covered the entire process, custom activities had to carry much of the integration burden. They were useful because an application could log a project-specific event and an automation process could react to it. However, an activity often became a technical message rather than a meaningful record of contact behavior.
Starting a process from a custom application event required the application to log a custom activity and the marketer to select the built-in Custom activity trigger. Before custom actions were introduced, running custom logic in the middle of a process was similarly indirect: the process logged another custom activity, separate code listened for it, and that listener performed the actual operation. Custom actions removed the second workaround, while custom starts remained activity-based until version 31.7.0.
As a result, a process diagram did not tell the whole story. A step named "Log custom activity" might really mean "Synchronize this contact with the CRM" or "Request a loyalty status update." Editors had to know which activities were technical signals, what external code handled them, and whether that code completed successfully. A mismatched activity code name or configuration could also break the connection between the process and its hidden implementation.
Project-specific decisions were the largest gap. Custom activities could invoke application code, but they could not choose which path a contact should follow. Checks involving a CRM, billing system, loyalty service, or scoring model therefore remained outside the Automation Builder. Custom conditions bring these decisions into the process as reusable, configurable steps. They can evaluate the contact, trigger data, or an external service and select a path. When a decision depends on earlier work, a custom action can save typed data to the process context and a following condition can read it and select the appropriate path. This keeps the decision visible to marketers and the business logic in application code.
The July refresh removes these indirections. Custom activities remain valuable when an event is genuinely part of a contact's activity history, but they no longer need to act as the universal transport mechanism for all custom automation behavior.
The three extension contracts
The automation customization API now covers the full process lifecycle, with three extension points available:
- Custom trigger – inherits from AutomationTrigger , AutomationTrigger<TData> , or AutomationTrigger<TData, TProperties> , registered via RegisterAutomationTrigger<TTrigger>
- Custom action – inherits from AutomationAction or AutomationAction<TProperties> , registered via RegisterAutomationAction<TAction>
- Custom condition – inherits from AutomationCondition or AutomationCondition<TProperties> , registered via RegisterAutomationCondition<TCondition>
All three component types live in CMS.Automation. Their registration attributes give the component a stable identifier and display name, with optional icon and description metadata for the builder. Configurable variants use a properties class decorated with Xperience administration editing-component attributes.
The distinction between the contracts is intentional. Triggers respond to events and decide whether a process starts. Actions own side effects. Conditions read context and return a branch decision. Keeping those responsibilities separate makes an automation process easier to test and reason about.
Implementing a custom trigger
A custom trigger starts automation from application code. Typical dispatch locations include an object event handler, checkout controller, webhook endpoint, integration service, or scheduled task.
A trigger can be as small as a class inheriting from AutomationTrigger. More useful integrations can carry typed event data through IAutomationTriggerData and expose per-process configuration through IAutomationTriggerProperties.
The example used throughout the next sections follows one connected customer journey:
- A known visitor views the detail page for a vehicle model.
- The visitor later completes a test drive at a dealership, and the dealership records it in the CRM.
- A CRM webhook fires a custom Xperience trigger for the mapped contact.
- The built-in
Contact has visited a page in the last X dayscondition checks whether the contact recently viewed that model online. - A custom condition checks whether the CRM opportunity is still eligible for a personal offer.
- The true branch runs a custom action that creates the offer. Either false branch can finish the process without creating one.
The trigger carries the CRM activity and vehicle identifiers into the process. Its configurable model code lets marketers create separate processes for different vehicle campaigns.
using System; using System.Threading; using System.Threading.Tasks; using CMS.Automation; using Kentico.Xperience.Admin.Base; using Kentico.Xperience.Admin.Base.FormAnnotations; using Acme.Automation; [assembly: RegisterAutomationTrigger<TestDriveCompletedTrigger>( identifier: TestDriveCompletedTrigger.IDENTIFIER, displayName: "Test drive completed", Description = "Starts a follow-up process after a dealership records a test drive.", IconName = Icons.UserCheckbox)] namespace Acme.Automation; public sealed class TestDriveCompletedData : IAutomationTriggerData { public string Identifier => "Acme.TestDriveCompletedData"; public string CrmActivityId { get; init; } = string.Empty; public string VehicleModelCode { get; init; } = string.Empty; public string DealerCode { get; init; } = string.Empty; public DateTimeOffset CompletedAt { get; init; } } public sealed class TestDriveTriggerProperties : IAutomationTriggerProperties { [TextInputComponent(Label = "Vehicle model code", Order = 10)] [RequiredValidationRule] public string VehicleModelCode { get; set; } = string.Empty; } public sealed class TestDriveCompletedTrigger : AutomationTrigger<TestDriveCompletedData, TestDriveTriggerProperties> { public const string IDENTIFIER = "Acme.TestDriveCompletedTrigger"; public override Task<bool> Evaluate( AutomationTriggerContext context, TestDriveTriggerProperties properties, TestDriveCompletedData triggerData, CancellationToken cancellationToken) { return Task.FromResult(string.Equals( triggerData.VehicleModelCode, properties.VehicleModelCode, StringComparison.OrdinalIgnoreCase)); } }
The properties class becomes the trigger configuration UI in the Automation Builder. Different processes can use the same trigger for different models. When the CRM event arrives, Xperience evaluates every process that uses the trigger against that process's saved model code and recurrence settings.
Trigger classes must be stateless. Xperience creates one trigger instance and reuses it across evaluations, so dispatch-specific data belongs in the typed trigger payload rather than instance fields.
Defining a trigger is only half of the implementation. The CRM webhook must map its contact identifier to a Xperience ContactInfo object and dispatch the trigger. The resolver in the following example is application-specific; it encapsulates the identity mapping established when the visitor became a known contact.
using System; using System.Threading; using System.Threading.Tasks; using CMS.Automation; using CMS.ContactManagement; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Acme.Automation; [ApiController] [Authorize(AuthenticationSchemes = "CrmWebhook")] [Route("integrations/crm/test-drives")] public sealed class TestDriveWebhookController( ICrmContactResolver contactResolver, IAutomationTriggerDispatcher triggerDispatcher) : ControllerBase { [HttpPost("completed")] public async Task<IActionResult> Completed( TestDriveCompletedWebhook webhook, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(webhook.CrmContactId) || string.IsNullOrWhiteSpace(webhook.CrmActivityId) || string.IsNullOrWhiteSpace(webhook.VehicleModelCode) || string.IsNullOrWhiteSpace(webhook.DealerCode) || webhook.CompletedAt == default || webhook.CompletedAt > DateTimeOffset.UtcNow.AddMinutes(5)) { return BadRequest(); } ContactInfo? contact = await contactResolver.FindContact( webhook.CrmContactId, cancellationToken); if (contact is null) { return NotFound(); } await triggerDispatcher.FireTrigger<TestDriveCompletedTrigger>( new AutomationTriggerDispatch( contact, new TestDriveCompletedData { CrmActivityId = webhook.CrmActivityId, VehicleModelCode = webhook.VehicleModelCode, DealerCode = webhook.DealerCode, CompletedAt = webhook.CompletedAt.ToUniversalTime() }), cancellationToken); return Accepted(); } } public sealed record TestDriveCompletedWebhook( string CrmContactId, string CrmActivityId, string VehicleModelCode, string DealerCode, DateTimeOffset CompletedAt);
Trigger data is serialized with the process state, so downstream custom steps can retrieve it through AutomationProcessContext.GetTriggerData<T>() without another CRM query simply to recover which test drive started the process. The payload contains identifiers and event facts rather than the visitor's name, email address, or a copied CRM record.
"FireTrigger" enqueues the request and returns immediately, before processing starts. The bounded in-memory queue is shared by custom triggers and the built-in Form trigger. If the queue fills, additional triggers are dropped and Xperience logs a warning. High-volume implementations should monitor this behavior and adjust CMSAutomationTriggerQueueCapacity only after understanding their process throughput. A business-critical CRM event should first be persisted and deduplicated in a durable integration inbox, with a background worker dispatching the trigger. For scheduled tasks that fire triggers for sets of contacts, Kentico recommends running at most once per day to avoid excessive load.
Implementing a custom condition
A custom condition adds project-specific branching. It can inspect the contact, trigger data, stored process data, or information from an external system, then return `true` or `false`.
The recent website visit does not require custom code. In the Automation Builder, place the built-in Contact has visited a page in the last X days condition after the trigger and configure it with the vehicle detail page and campaign window. Each model-specific process pairs that page selection with the same model code configured on the trigger.
The custom condition should answer a question that Xperience cannot evaluate from its own contact data. The following example asks the CRM whether the opportunity associated with the test drive remains eligible for a personal offer. The application-specific service can centralize rules such as requiring an open opportunity and excluding a completed sale or an offer that was already issued.
using System.Threading; using System.Threading.Tasks; using CMS.Automation; using Kentico.Xperience.Admin.Base; using Microsoft.Extensions.Logging; using Acme.Automation; [assembly: RegisterAutomationCondition<CrmAllowsPersonalOfferCondition>( identifier: CrmAllowsPersonalOfferCondition.IDENTIFIER, displayName: "CRM allows personal offer", Description = "Checks whether the CRM opportunity is eligible for an offer.", IconName = Icons.CheckCircle)] namespace Acme.Automation; public sealed class CrmAllowsPersonalOfferCondition( ICrmTestDriveService crmTestDriveService, ILogger<CrmAllowsPersonalOfferCondition> logger) : AutomationCondition { public const string IDENTIFIER = "Acme.CrmAllowsPersonalOffer"; public override async Task<bool> Evaluate( AutomationProcessContext context, CancellationToken cancellationToken) { TestDriveCompletedData triggerData = await context.GetTriggerData<TestDriveCompletedData>(cancellationToken); if (triggerData is null) { return false; } try { return await crmTestDriveService.IsEligibleForPersonalOffer( triggerData.CrmActivityId, cancellationToken); } catch (CrmUnavailableException exception) { logger.LogError( exception, "Could not evaluate offer eligibility for CRM activity {CrmActivityId}.", triggerData.CrmActivityId); return false; } } }
Registered conditions appear under Conditions next to the built-in Rule-based condition. Connect the true path of the built-in page-visit condition to CRM allows personal offer, then connect its true path to the Create test-drive offer action implemented in the next section. Both false paths should use a safe fallback such as the end of the process. An unavailable CRM follows the false branch, so that branch must never send an offer or perform another eligibility-sensitive action.
Conditions should be read-only and idempotent because Xperience may evaluate them more than once. Do not update contacts, store process data, or trigger external side effects from Evaluate. If a decision requires mutation or expensive preparation, perform it in a preceding action, store the result as process data, and let the condition read that result.
Implementing a custom action
A custom action performs work when a contact reaches its step. This is the correct extension point for side effects such as synchronizing a CRM record, enriching contact data, sending an internal notification, calling an external API, or publishing an analytics event.
The action overrides Execute and receives an AutomationProcessContext. The context provides the processed contact, process metadata, trigger data, and typed data saved by earlier steps.
The following action belongs on the true branch of the custom CRM condition shown in the previous section, after the built-in page-visit condition. It creates a personal offer through an application-specific offer service, using the CRM activity and vehicle model carried by the trigger. Marketers choose the offer template and validity period without configuring the external service itself.
using System.Threading; using System.Threading.Tasks; using CMS.Automation; using CMS.ContactManagement; using Kentico.Xperience.Admin.Base; using Kentico.Xperience.Admin.Base.FormAnnotations; using Microsoft.Extensions.Logging; using Acme.Automation; [assembly: RegisterAutomationAction<CreateTestDriveOfferAction>( identifier: CreateTestDriveOfferAction.IDENTIFIER, displayName: "Create test-drive offer", Description = "Creates a personal offer after a relevant test drive.", IconName = Icons.StarFull)] namespace Acme.Automation; public sealed class TestDriveOfferProperties : IAutomationActionProperties { [TextInputComponent(Label = "Offer template code", Order = 10)] [RequiredValidationRule] public string OfferTemplateCode { get; set; } = string.Empty; [NumberInputComponent(Label = "Valid for days", Order = 20)] [MinimumIntegerValueValidationRule(1)] [MaximumIntegerValueValidationRule(60)] public int ValidForDays { get; set; } = 14; } public sealed class CreateTestDriveOfferAction( ITestDriveOfferService offerService, ILogger<CreateTestDriveOfferAction> logger) : AutomationAction<TestDriveOfferProperties> { public const string IDENTIFIER = "Acme.CreateTestDriveOffer"; public override async Task Execute( TestDriveOfferProperties properties, AutomationProcessContext context, CancellationToken cancellationToken) { TestDriveCompletedData triggerData = await context.GetTriggerData<TestDriveCompletedData>(cancellationToken); if (triggerData is null) { logger.LogWarning( "Test-drive trigger data is missing in process {ProcessName}.", context.Process.DisplayName); return; } ContactInfo contact = await context.GetProcessedObject(cancellationToken); await offerService.EnqueuePersonalOffer( contact.ContactID, triggerData.CrmActivityId, triggerData.VehicleModelCode, triggerData.DealerCode, properties.OfferTemplateCode, properties.ValidForDays, cancellationToken); } }
Once registered, the component appears under Steps in the selection dialog. The editor sees "Create test-drive offer" and can select a business-owned template and validity period.
Actions can also prepare data for later steps. A data class implementing IAutomationProcessData can be saved with SetProcessData and read later with GetProcessData<T>. This pattern is useful when one action calculates a score or obtains a status that several later conditions need. It also keeps conditions free of mutations and duplicated integration calls.
Designing configurable components
Properties turn a developer-defined component into a reusable builder tool. Implement the matching interface—IAutomationTriggerProperties, IAutomationActionProperties, or IAutomationConditionProperties—and decorate public get/set properties with editing components from Kentico.Xperience.Admin.Base.FormAnnotations.
The property model supports text, number, checkbox, date and time, rich text, and drop-down inputs. It also supports validation rules, conditional visibility, categories, default values, and dynamic drop-down options. This means a single integration component can serve multiple processes without hard-coded campaign settings.
The registration metadata deserves the same care. Display names should describe the operation in business language. Descriptions should explain when to use the component, and icons should make similar types easier to scan. Version 31.7.0 also improves the Select step type dialog by grouping step types into Steps and Conditions, pinning search, and listing built-in and custom types together.
KentiCopilot support for automation components
The KentiCopilot "kentico-digital-experience" plugin was released to support AI-assisted implementation of marketing automation components. Its initial custom action skill was followed on July 31 by skills for custom trigger and condition development. Together, they guide coding assistants through the same contracts described above: selecting the correct base class, defining optional properties and typed data, registering the component, and wiring trigger dispatch into application code.
The plugin source and usage information are available in the KentiCopilot digital experience plugin.
Migrating custom activity workarounds
Existing processes do not need to be rewritten immediately. Start by distinguishing genuine marketing activities from technical relay messages. An event that belongs in the contact's activity history can remain a custom activity. Xperience also recommends custom activities when one automation process intentionally starts another because the activity provides a useful record of that transition.
Branch merging is one example: separate branches can each log the same custom activity, which starts a shared follow-up process and gives those branches a common continuation.
For technical relay activities, the migration target follows directly from the responsibility. An activity logged by application code only to start automation becomes a custom trigger and a dispatcher call. An activity logged by a process only to invoke an integration becomes a custom action backed by an injected service. A decision previously moved outside the process becomes a custom condition, optionally reading typed data prepared by an earlier action.
A direct model for custom automation
The July 2026 refresh gives Xperience projects a direct programming model for custom automation. Application events dispatch typed triggers. Actions perform side effects through injected services. Conditions make read-only decisions from process context. Registration and property classes turn each implementation into a configurable Automation Builder component.
Developers no longer need to force every integration through custom activity logging, and marketers no longer need to interpret generic steps backed by hidden handlers. The code and the process now use the same three concepts: start on this event, perform this operation, and choose the next path.
Bluesoft is a development company specialising in bespoke web solutions, e-commerce platforms and digital applications. For more than 17 years, we have been a Kentico Gold Partner and are among the most experienced implementation teams in the region.
We also deliver projects on Kontent.ai and Umbraco, supporting mid-size and enterprise companies such as Škoda Auto, Sazka and E.ON. Our solutions regularly receive Kentico Site of the Year awards, confirming the quality and long-term reliability of our work.
As part of BiQ Group, we bring together more than 590 specialists and have successfully delivered more than 2,100 projects.
👉 Contact us via our contact form and our team will get back to you.








































