Skip to content
Back to skills

1096 Trigger Actions Framework A0eb4799

ASecurity

<!-- Parent: sf-apex/SKILL.md -->

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
businesstestinggitapi

Works with

  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 1096-trigger-actions-framework_a0eb4799 --agent claude-code

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.

Security grade badge for 1096 Trigger Actions Framework A0eb4799
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1096-trigger-actions-framework-a0eb4799/badge)](https://www.skillsdirectory.com/skills/tools-only-1096-trigger-actions-framework-a0eb4799)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
<!-- 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+)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.