Installs into .claude/skills of the current project.
Are you the author of 1096 Trigger Actions Framework A0eb4799?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tools-only-1096-trigger-actions-framework-a0eb4799)
<!-- Parent: sf-apex/SKILL.md -->
# Trigger Actions Framework (TAF) Guide
## Overview
The Trigger Actions Framework provides a metadata-driven approach to trigger management, enabling:
- One trigger per object
- Ordered execution of actions
- Bypass mechanisms (global, transaction, permission-based)
- Support for both Apex and Flow actions
## Installation
Install from: https://github.com/mitchspano/trigger-actions-framework
## Basic Setup
### 1. Create the Trigger
One trigger per object, delegating all logic to the framework:
```apex
trigger AccountTrigger on Account (
before insert, after insert,
before update, after update,
before delete, after delete,
after undelete
) {
new MetadataTriggerHandler().run();
}
```
### 2. Enable the Object
Create an `sObject_Trigger_Setting__mdt` record:
- Label: Account Trigger Setting
- Object API Name: Account
- Bypass Execution: unchecked
### 3. Create Action Classes
Each action class handles one specific behavior:
```apex
public class TA_Account_SetDefaults implements TriggerAction.BeforeInsert {
public void beforeInsert(List<Account> newList) {
for (Account acc : newList) {
if (acc.Industry == null) {
acc.Industry = 'Other';
}
}
}
}
```
### 4. Register the Action
Create a `Trigger_Action__mdt` record:
- Object: Account
- Apex Class Name: TA_Account_SetDefaults
- Order: 1
- Before Insert: checked
---
## Action Interfaces
### Before Triggers
```apex
// Before Insert
public class MyAction implements TriggerAction.BeforeInsert {
public void beforeInsert(List<SObject> newList) { }
}
// Before Update
public class MyAction implements TriggerAction.BeforeUpdate {
public void beforeUpdate(List<SObject> newList, List<SObject> oldList) { }
}
// Before Delete
public class MyAction implements TriggerAction.BeforeDelete {
public void beforeDelete(List<SObject> oldList) { }
}
```
### After Triggers
```apex
// After Insert
public class MyAction implements TriggerAction.AfterInsert {
public void afterInsert(List<SObject> newList) { }
}
// After Update
public class MyAction implements TriggerAction.AfterUpdate {
public void afterUpdate(List<SObject> newList, List<SObject> oldList) { }
}
// After Delete
public class MyAction implements TriggerAction.AfterDelete {
public void afterDelete(List<SObject> oldList) { }
}
// After Undelete
public class MyAction implements TriggerAction.AfterUndelete {
public void afterUndelete(List<SObject> newList) { }
}
```
---
## Common Patterns
### Setting Default Values (Before Insert)
```apex
public class TA_Account_SetDefaults implements TriggerAction.BeforeInsert {
public void beforeInsert(List<Account> newList) {
for (Account acc : newList) {
acc.Industry = acc.Industry ?? 'Other';
acc.NumberOfEmployees = acc.NumberOfEmployees ?? 0;
}
}
}
```
### Validation (Before Insert/Update)
```apex
public class TA_Account_ValidateData implements TriggerAction.BeforeInsert, TriggerAction.BeforeUpdate {
public void beforeInsert(List<Account> newList) {
validate(newList);
}
public void beforeUpdate(List<Account> newList, List<Account> oldList) {
validate(newList);
}
private void validate(List<Account> accounts) {
for (Account acc : accounts) {
if (acc.AnnualRevenue != null && acc.AnnualRevenue < 0) {
acc.AnnualRevenue.addError('Annual Revenue cannot be negative');
}
}
}
}
```
### Related Record Updates (After Insert/Update)
```apex
public class TA_Account_UpdateContacts implements TriggerAction.AfterUpdate {
public void afterUpdate(List<Account> newList, List<Account> oldList) {
Map<Id, Account> oldMap = new Map<Id, Account>(oldList);
Set<Id> changedAccountIds = new Set<Id>();
for (Account acc : newList) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.BillingCity != oldAcc.BillingCity) {
changedAccountIds.add(acc.Id);
}
}
if (!changedAccountIds.isEmpty()) {
updateContactAddresses(changedAccountIds);
}
}
private void updateContactAddresses(Set<Id> accountIds) {
List<Contact> contacts = [
SELECT Id, AccountId, MailingCity
FROM Contact
WHERE AccountId IN :accountIds
WITH USER_MODE
];
Map<Id, Account> accounts = new Map<Id, Account>([
SELECT Id, BillingCity
FROM Account
WHERE Id IN :accountIds
WITH USER_MODE
]);
for (Contact con : contacts) {
con.MailingCity = accounts.get(con.AccountId).BillingCity;
}
update contacts;
}
}
```
### Async Processing (After Insert/Update)
```apex
public class TA_Account_ProcessAsync implements TriggerAction.AfterInsert {
public void afterInsert(List<Account> newList) {
Set<Id> accountIds = new Map<Id, Account>(newList).keySet();
System.enqueueJob(new AccountProcessingQueueable(accountIds));
}
}
```
---
## Bypass Mechanisms
### Global Bypass (Metadata)
In `sObject_Trigger_Setting__mdt`:
- Set `Bypass_Execution__c = true` to disable all triggers for object
### Transaction Bypass (Apex)
```apex
// Bypass specific object
TriggerBase.bypass(Account.SObjectType);
// Bypass specific action
MetadataTriggerHandler.bypass('TA_Account_SetDefaults');
// Clear bypasses
TriggerBase.clearAllBypasses();
MetadataTriggerHandler.clearAllBypasses();
```
### Permission-Based Bypass
In `Trigger_Action__mdt`:
- `Bypass_Permission__c`: Users with this permission skip the action
- `Required_Permission__c`: Only users with this permission run the action
---
## Recursion Prevention
### Using TriggerBase
```apex
public class TA_Account_PreventRecursion implements TriggerAction.AfterUpdate {
public void afterUpdate(List<Account> newList, List<Account> oldList) {
List<Account> toProcess = new List<Account>();
for (Account acc : newList) {
// Check if already processed in this transaction
if (!TriggerBase.idToNumberOfTimesSeenAfterUpdate.get(acc.Id).equals(1)) {
continue;
}
toProcess.add(acc);
}
if (!toProcess.isEmpty()) {
processAccounts(toProcess);
}
}
}
```
---
## Flow Actions
### Setup
1. Create an Autolaunched Flow
2. Add variables:
- `record` (Input, Record type)
- `recordPrior` (Input, Record type, for Update triggers)
3. Create `Trigger_Action__mdt`:
- Apex Class Name: `TriggerActionFlow`
- Flow Name: `Your_Flow_API_Name`
### Entry Criteria
Define formula criteria to control when the flow executes:
```
{!record.Status__c} = 'Submitted' && {!recordPrior.Status__c} != 'Submitted'
```
---
## Testing
### Test Action Classes
```apex
@isTest
private class TA_Account_SetDefaultsTest {
@isTest
static void testBeforeInsert() {
Account acc = new Account(Name = 'Test');
Test.startTest();
insert acc;
Test.stopTest();
Account result = [SELECT Industry FROM Account WHERE Id = :acc.Id];
Assert.areEqual('Other', result.Industry, 'Default industry should be set');
}
@isTest
static void testBulkInsert() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 251; i++) {
accounts.add(new Account(Name = 'Test ' + i));
}
Test.startTest();
insert accounts;
Test.stopTest();
List<Account> results = [SELECT Industry FROM Account WHERE Id IN :accounts];
for (Account acc : results) {
Assert.areEqual('Other', acc.Industry);
}
}
}
```
### Test with Bypass
```apex
@isTest
static void testWithBypass() {
// Bypass the action
MetadataTriggerHandler.bypass('TA_Account_SetDefaults');
Account acc = new Account(Name = 'Test');
insert acc;
Account result = [SELECT Industry FROM Account WHERE Id = :acc.Id];
Assert.isNull(result.Industry, 'Industry should not be set when bypassed');
// Clear bypass for other tests
MetadataTriggerHandler.clearBypass('TA_Account_SetDefaults');
}
```
---
## Naming Convention
```
TA_[ObjectName]_[ActionDescription]
Examples:
- TA_Account_SetDefaults
- TA_Account_ValidateData
- TA_Contact_UpdateAccountRollup
- TA_Opportunity_SendNotification
```
---
## Execution Order
Actions execute in the order defined by the `Order__c` field in `Trigger_Action__mdt`.
Recommended ordering:
1. Validation (10-20)
2. Default values (30-40)
3. Field calculations (50-60)
4. Related record queries (70-80)
5. Related record updates (90-100)
6. Async/external calls (110+)