The Salesforce Apex trigger framework for clean, scalable, and maintainable automation.
- Custom settings allow you to enable or disable triggers globally or for specific sObjects.
- Custom registry lets you register handlers through configuration rather than code.
- Fine-grained control over handler execution using
context.next()
,context.stop()
, andTriggers.skips
.
Environment | Installation Link | Version |
---|---|---|
Production, Developer | ![]() |
ver 2.0 |
Sandbox | ![]() |
ver 2.0 |
Support: If you find this library helpful, please consider sharing it on Twitter or recommending it to your friends and colleagues.
Completely rewritten from v1.0. Retains the best features, removes unnecessary complexity, and introduces new capabilities.
- 1. Setting
- 2. Handler Registration
- 3. Handler Usage
- 4. Execution Control
- 5. Tests
- 6. APIs
- 7. License
The Registry Has Priority
setting determines whether handlers registered via custom metadata types take precedence over those registered directly in Apex code. For more details about bypass flags, see the table below:
Bypass Triggers | Bypass SObjects | Description |
---|---|---|
false | Empty | By default, no trigger handlers are bypassed. |
true | Empty | All trigger handlers registered through this framework are bypassed. |
true | Account Contact Opportunity |
Only trigger handlers for the specified SObjects are bypassed. |
Field Name | Data Type | Description |
---|---|---|
SObject | Text | Required. The API name of the SObject to which the handler applies. |
Trigger Event | Picklist | Required. Defaults to Any Event . When set to Any Event , the handler is applied to all implemented events. |
Handler Class | Text | Required. The name of the Apex class that implements the handler logic. |
Execution Order | Number | Required. Determines the sequence in which handlers are executed. |
Is Active | Checkbox | Indicates whether the handler is enabled or disabled. |
To create a trigger handler, define a class that implements the corresponding handler interfaces.
// Explicitly declare the required interfaces for clarity.
public class AccountTriggerHandler implements Triggers.BeforeInsert, Triggers.BeforeUpdate {
// Implement Triggers.BeforeInsert
public void beforeInsert(Triggers.Context context) {
}
// Implement Triggers.BeforeUpdate
public void beforeUpdate(Triggers.Context context) {
}
}
Handlers registered via the registry are loaded automatically. Each handler is associated with the correct SObject and trigger event based on its configuration.
trigger AccountTrigger on Account (before update, after update) {
Triggers.prepare().execute();
}
Handlers can be registered using either class types or class names. Using class names is often preferred for flexibility. You can bind handlers to all events for simpler control, or to specific events for more granular management.
trigger AccountTrigger on Account (before update, after update) {
Triggers.prepare()
.bind(AccountTriggerHandler1.class) // handlers bound to any event
.bind('AccountTriggerHandler2')
.beforeUpdate() // handlers bound to a specific event
.bind(AccountTriggerHandler3.class)
.bind('AccountTriggerHandler4')
.execute();
}
Note: You can register trigger handlers using both custom metadata and Apex code at the same time. By default, handlers registered in Apex code have higher priority. To give priority to handlers registered via custom metadata, enable the Registry Has Priority
setting as described above.
All static properties from the Trigger
class are now accessible via the context
object. Always use context
to access trigger properties, such as context.oldList
and context.newList
.
public class AccountTriggerHandler implements Triggers.BeforeInsert {
public void beforeInsert(Triggers.Context context) {
if (context.isBefore && context.isInsert) {
for (Account account : (List<Account>) context.newList) {
// Your logic here
}
}
}
}
Use Triggers.states
to manage state objects. This is a singleton, meaning it is shared across all triggers within the same transaction. State classes are automatically initialized the first time they are accessed.
public class AccountTriggerHandler implements Triggers.BeforeInsert {
public void beforeInsert(Triggers.Context context) {
// Retrieve and update a state instance as needed.
CounterState counter = (CounterState) Triggers.states.get(CounterState.class);
counter.increment();
}
}
You can also set a state object in advance, which is useful for initializing values:
// Make sure to check if the state already exists.`
if (!Triggers.states.contains(CounterState.class)) {
Triggers.states.put(CounterState.class, new CounterState(100));
}
State classes must implement the Triggers.State
interface.
public class CounterState implements Triggers.State {
public Integer count { get; private set; }
public CounterState() {
this.count = 0;
}
public CounterState(Integer count) {
this.count = count;
}
public void increment() {
this.count++;
}
}
You can also remove state objects to free up resources:
// Remove a specific state.
Triggers.states.remove(CounterState.class);
// Or clear all states.
Triggers.states.clear();
It is a common requirement to determine whether specific fields have changed between the old and new lists in a trigger context. You can use Apex LINQ to simplify this process.
public class AccountTriggerHandler implements Triggers.BeforeUpdate {
public void beforeUpdate(Triggers.Context context) {
Q.Differ differ = new AccountDiffer();
List<Account> changedAccounts = (List<Account>) Q.of(context.newList)
.diff(differ, context.oldList).toList();
// Implement your business logic here.
}
public class AccountDiffer implements Q.Differ {
public Boolean changed(Object arg1, Object arg2) {
Double revenue1 = ((Account) arg1).AnnualRevenue;
Double revenue2 = ((Account) arg2).AnnualRevenue;
return revenue1 != revenue2;
}
}
}
You can skip specific handlers in your Apex code as shown below:
// Skip the AccountTriggerHandler during this operation.
Triggers.skips.add(AccountTriggerHandler.class);
insert accounts;
// Restore the handler after the operation.
Triggers.skips.remove(AccountTriggerHandler.class);
// Or simply clear all skipped handlers.
Triggers.skips.clear();
Method | Return Type | Description |
---|---|---|
add(type handlerType) |
void | Skip a handler. |
add(List<type> handlerTypes) |
void | Skip multiple handlers. |
contains(type handlerType) |
Boolean | Check if a handler is being skipped. |
remove(type handlerType) |
void | Restore a previously skipped handler. |
clear() |
void | Restore all skipped handlers. |
public class AccountTriggerHandler implements Triggers.BeforeInsert {
public void beforeInsert(Triggers.Context context) {
// Optionally call context.next() to execute all subsequent handlers.
context.next();
// This is useful if you need to perform logic after all subsequent handlers run.
// Optionally call context.stop() to prevent any further handlers from executing.
context.stop();
}
}
You can centralize exception handling for all subsequent handlers by implementing a dedicated error handler. This ensures that any exceptions thrown by handlers executed after context.next()
are caught and managed in a single location. For example:
public class ErrorTriggerHandler implements Triggers.BeforeInsert, Triggers.AfterInsert {
public void beforeInsert(Triggers.Context context) {
execute(context);
}
public void afterInsert(Triggers.Context context) {
execute(context);
}
private void execute(Triggers.Context context) {
try {
context.next();
} catch (Exception ex) {
// Handle exceptions from subsequent handlers here
throw ex; // rethrow to revoke the transaction
}
}
}
Settings can be modified for testing purposes only, as the following two methods are private and marked with @TestVisible
.
// Use this method to override the global settings.
// @param hasPriority
// @param bypassTriggers
// @param bypassSObjects
Triggers.setSettings(true, true, 'Account\nContact\nOpportunity');
// Use this method to override the handler registry.
Triggers.setRegistry(
Account.SObjectType,
new List<TriggerHandlerRegistry__mdt>{
new TriggerHandlerRegistry__mdt(
TriggerEvent__c = 'ANY_EVENT',
HandlerClass__c = 'TriggersTest.AccountTriggerHandler01'
// The following fields are optional:
// SObject__c = 'Account',
// ExecutionOrder__c = 10,
// IsActive__c = true
)
}
);
The following method is private but marked as @TestVisible
, allowing it to be used in test methods to provide mock records for the old and new lists. This eliminates the need to perform DML operations to trigger the handlers.
@isTest
static void test_AccountTriggerHandler_BeforeUpdate {
List<SObject> oldList = new List<Account> {
new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'Old Name 1'),
new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'Old Name 2'),
new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'Old Name 3')}
List<SObject> newList = new List<Account> {
new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'New Name 1'),
new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'New Name 2'),
new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'New Name 3')}
Triggers.prepare(TriggerOperation.Before_Update, oldList, newList)
.beforeUpdate().bind('AccountTriggerHandler').execute();
}
Interface | Method to Implement |
---|---|
Triggers.BeforeInsert |
void beforeInsert(Triggers.Context context); |
Triggers.AfterInsert |
void afterInsert(Triggers.Context context); |
Triggers.BeforeUpdate |
void beforeUpdate(Triggers.Context context); |
Triggers.AfterUpdate |
void afterUpdate(Triggers.Context context); |
Triggers.BeforeDelete |
void beforeDelete(Triggers.Context context); |
Triggers.AfterDelete |
void afterDelete(Triggers.Context context); |
Triggers.BeforeUndelete |
void afterUndelete(Triggers.Context context); |
Property | Type | Description |
---|---|---|
context.sObjectType |
SObjectType | The current SObjectType. |
context.isExecuting |
Boolean | Trigger.isExecuting |
context.isBefore |
Boolean | Trigger.isBefore |
context.isAfter |
Boolean | Trigger.isAfter |
context.isInsert |
Boolean | Trigger.isInsert |
context.isUpdate |
Boolean | Trigger.isUpdate |
context.isDelete |
Boolean | Trigger.isDelete |
context.isUndelete |
Boolean | Trigger.isUndelete |
context.oldList |
List<SObject> | Trigger.old |
context.oldMap |
Map<Id, SObject> | Trigger.oldMap |
context.newList |
List<SObject> | Trigger.new |
context.newMap |
Map<Id, SObject> | Trigger.newMap |
context.operationType |
TriggerOperation | Trigger.operationType |
context.size |
Integer | Trigger.size |
Method | Return Type | Description |
---|---|---|
context.next() |
void | Call the next handler. |
context.stop() |
void | Stop execute any following handlers. A bit like the the stop in process builders. |
BSD 3-Clause License