> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fusioncore.us/llms.txt
> Use this file to discover all available pages before exploring further.

# Trigger Framework

> Build, register, order, and selectively disable metadata-driven Apex triggers in the fusionCore Platform package.

fusionCore routes all Apex trigger logic through a single, metadata-driven framework that lives in the Platform package under the `FCORE_BASE` namespace. Instead of writing logic in trigger files, you write small handler classes — **trigger units** — and register each one against an object through Custom Metadata. The framework decides which units run, in what order, and whether to run at all.

This design gives you a few things for free:

* A single, predictable execution path for every object's automation.
* Per-object, per-handler, per-user, and org-wide on/off switches without code changes.
* A clean separation between automation logic and validation logic.

The framework dispatches events as part of trigger execution. For the publish/subscribe layer that runs alongside it, see [Event Framework](/developer-guide/event-framework). For shared helpers referenced below, see [Utility Functions](/developer-guide/utility-functions).

## Writing a Trigger

A trigger file contains exactly one line. It constructs a `FCORE_BASE.TriggerExecutor` for the object's `SObjectType` and calls `execute()`. Never put logic in the trigger file itself — the executor delegates to your registered trigger units.

```java theme={null}
trigger OrderTrigger on Order (before insert, before update, before delete,
        after insert, after update, after delete, after undelete) {
    new FCORE_BASE.TriggerExecutor(Order.SObjectType).execute();
}
```

`FCORE_BASE.TriggerExecutor` exposes:

* `TriggerExecutor(SObjectType)` — constructor; pass the object's `SObjectType`.
* `execute()` — runs every enabled trigger unit registered for that object, in order, for the current trigger context.

Wire up all the trigger contexts your registered units might use. The executor only invokes the contexts each unit overrides, so listing every context in the trigger signature is safe and keeps future units from being silently skipped.

## Writing a Trigger Unit

A trigger unit is an Apex class that extends the abstract class `FCORE_BASE.TriggerUnit`. By convention its name ends in `TU`. Override only the contexts your unit needs — every context method is a `global virtual void` with an empty default implementation.

```java theme={null}
global without sharing class WalletItemRemovedTU extends FCORE_BASE.TriggerUnit {
    public override void beforeUpdate(List<SObject> records, Map<Id, SObject> oldRecordsByIds) {
        // automation logic
    }
}
```

### Available Contexts

| Method                                                                  | When it runs                                          |
| ----------------------------------------------------------------------- | ----------------------------------------------------- |
| `bulkBefore()`                                                          | Once per `before` pass, before any per-context method |
| `bulkAfter()`                                                           | Once per `after` pass, before any per-context method  |
| `beforeInsert(List<SObject> records)`                                   | Before insert                                         |
| `beforeUpdate(List<SObject> records, Map<Id, SObject> oldRecordsByIds)` | Before update                                         |
| `beforeDelete(List<SObject> records)`                                   | Before delete                                         |
| `afterInsert(List<SObject> records)`                                    | After insert                                          |
| `afterUpdate(List<SObject> records, Map<Id, SObject> oldRecordsByIds)`  | After update                                          |
| `afterDelete(List<SObject> records)`                                    | After delete                                          |
| `afterUndelete(List<SObject> records)`                                  | After undelete                                        |

Use `bulkBefore()` and `bulkAfter()` to do shared, query-once setup (for example, loading related records) that the per-context methods then consume — this keeps your unit bulk-safe.

<Tip>
  Keep each trigger unit focused on one concern. Several small units on one object are easier to order, test, and disable independently than one large unit that handles every context.
</Tip>

## Registering a Trigger Unit

Registration uses two Custom Metadata types. When you reference them from Apex or in metadata files, the API names carry the `FCORE_BASE__` namespace prefix.

### `FCORE_BASE__Trigger__mdt` — one record per object

This record enables the framework for an object and is the anchor that trigger units link to.

| Field                            | Purpose                                                                                          |
| -------------------------------- | ------------------------------------------------------------------------------------------------ |
| `FCORE_BASE__Object_API_Name__c` | API name of the object the trigger runs on (for example, `Order` or `FCORE_PAY__Wallet_Item__c`) |
| `FCORE_BASE__Is_Enabled__c`      | Per-object master switch; when `false`, no trigger units run for this object                     |

### `FCORE_BASE__Trigger_Unit__mdt` — one record per handler

This record registers a single trigger unit class against an object's `Trigger__mdt` record.

| Field                        | Purpose                                                                                        |
| ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `FCORE_BASE__Apex_Class__c`  | Fully qualified class name, including namespace (for example, `FCORE_PAY.WalletItemRemovedTU`) |
| `FCORE_BASE__Trigger__c`     | Lookup to the object's `Trigger__mdt` record                                                   |
| `FCORE_BASE__Order__c`       | Execution order for this unit on the object                                                    |
| `FCORE_BASE__Is_Enabled__c`  | Per-handler switch; when `false`, this unit is skipped                                         |
| `FCORE_BASE__Description__c` | Free-text note describing what the unit does                                                   |

A `Trigger_Unit__mdt` record looks like this:

```xml theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
    <label>Wallet Item Removed</label>
    <values>
        <field>FCORE_BASE__Apex_Class__c</field>
        <value xsi:type="xsd:string">FCORE_PAY.WalletItemRemovedTU</value>
    </values>
    <values>
        <field>FCORE_BASE__Trigger__c</field>
        <value xsi:type="xsd:string">FCORE_PAY__Wallet_Item__c</value>
    </values>
    <values>
        <field>FCORE_BASE__Order__c</field>
        <value xsi:type="xsd:double">20.0</value>
    </values>
    <values>
        <field>FCORE_BASE__Is_Enabled__c</field>
        <value xsi:type="xsd:boolean">true</value>
    </values>
    <values>
        <field>FCORE_BASE__Description__c</field>
        <value xsi:type="xsd:string">Cleans up downstream records when a wallet item is removed.</value>
    </values>
</CustomMetadata>
```

### Ordering

Trigger units for one object run in ascending `FCORE_BASE__Order__c` — a unit with `Order` `10` runs before a unit with `Order` `20`. Leave gaps between values (10, 20, 30, …) so you can slot a new unit between two existing ones later without renumbering everything.

## Turning Triggers Off

Bypass is layered. Each layer is independent, and the framework checks them in this sequence — any one of them stops a unit from running:

| Layer               | Where                                                                      | Effect                                                                                                    |
| ------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Globally            | `FCORE_BASE__Org_Setting__mdt.Triggers_Enabled__c` on the `Default` record | Master switch; when `false`, no trigger units run anywhere. Useful during data migration.                 |
| Per user or profile | `FCORE_BASE__Org_Setting__mdt.Disable_Automations_For_User_Profile_Ids__c` | Comma-separated User and Profile Ids (for example, an integration user) for which automations are skipped |
| Per object          | `FCORE_BASE__Trigger__mdt.Is_Enabled__c`                                   | Disables every unit registered for one object                                                             |
| Per handler         | `FCORE_BASE__Trigger_Unit__mdt.Is_Enabled__c`                              | Disables one specific unit                                                                                |
| Programmatically    | `FCORE_BASE.TriggerExecutor.TRIGGER_CONTEXT`                               | Disables an object or a class for the current transaction only                                            |

### Disabling for the Current Transaction

`FCORE_BASE.TriggerExecutor.TRIGGER_CONTEXT` is a `TriggerContext` you can use to suppress automation programmatically — for example, around a data fix in an anonymous block or a one-off script. These changes apply only to the current transaction; re-enable as soon as the protected DML is done.

```java theme={null}
// Skip all trigger units for one object during a data fix.
// The argument is the Trigger__mdt record name (the object API name).
FCORE_BASE.TriggerExecutor.TRIGGER_CONTEXT.disableSObject('FCORE_PAY__Wallet_Item__c');
update walletItems;
FCORE_BASE.TriggerExecutor.TRIGGER_CONTEXT.enableSObject('FCORE_PAY__Wallet_Item__c');

// Or skip a single handler class; every other unit on the object still runs.
// The argument is the Apex class name.
FCORE_BASE.TriggerExecutor.TRIGGER_CONTEXT.disableClass('FCORE_PAY.WalletItemRemovedTU');
```

`TRIGGER_CONTEXT` exposes these methods, each taking a `String`:

* `disableSObject(String)` / `enableSObject(String)` — toggle every unit for one object.
* `disableClass(String)` / `enableClass(String)` — toggle one unit by class name.
* `disableValidations()` / `enableValidations()` — toggle validation units as a group (see below).

## Validation Triggers

Validation-only logic belongs in its own kind of unit. A validation unit extends `FCORE_BASE.ValidationTriggerUnit` — an abstract subclass of `TriggerUnit` — and registers through the same `Trigger_Unit__mdt` records as any other unit. By convention its name ends in `VTU`.

Keeping validations in their own units means you can switch them off as a group without touching the automation units that should keep running.

* **Metadata gate:** `FCORE_BASE__Org_Setting__mdt.Global_Validations_Enabled__c` — when `false`, every `ValidationTriggerUnit` is skipped.
* **Runtime gate:** `FCORE_BASE.TriggerExecutor.TRIGGER_CONTEXT.disableValidations()` / `enableValidations()` — suppress validation units for the current transaction (for example, during a data migration).

A validation unit raises its errors with the shared validation helpers rather than calling `addError()` directly:

```java theme={null}
global without sharing class RenewalPathRequiredFieldsVTU extends FCORE_BASE.ValidationTriggerUnit {
    public override void beforeInsert(List<SObject> records) {
        for (SObject record : records) {
            if (record.get('FCORE_MEM__Renewal_Path__c') == null) {
                record.addError(
                    FCORE_BASE.ValidationUtils.buildRequiredFieldIsMissingMessage('Renewal Path')
                );
            }
        }
    }
}
```

Because this logic lives in a `ValidationTriggerUnit`, both `Global_Validations_Enabled__c` and `disableValidations()` skip it automatically — you do not need to add your own enabled check. See [Utility Functions](/developer-guide/utility-functions) for the full set of `ValidationUtils` helpers.

### Prefer a Validation Trigger Unit Over a Declarative Validation Rule

When you need new validation logic, build it as a `ValidationTriggerUnit` rather than a declarative Salesforce Validation Rule. A validation trigger unit gives you two things a Validation Rule cannot:

* **Ordering** — `FCORE_BASE__Trigger_Unit__mdt.FCORE_BASE__Order__c` positions your validation relative to every other trigger unit on the object, including other validations.
* **Programmatic disable** — `FCORE_BASE.TriggerExecutor.TRIGGER_CONTEXT.disableValidations()` and the per-handler `FCORE_BASE__Trigger_Unit__mdt.FCORE_BASE__Is_Enabled__c` field can turn the check off for a single transaction or a single handler, without a metadata deployment.

A declarative Validation Rule only has its own `Active` checkbox — it cannot be reordered against other automation, and disabling it for one transaction (for example, during a data migration) means deactivating it org-wide first.

### Gating a Declarative Validation Rule

If a declarative Validation Rule is still the right tool — for example, a simple field check on a standard object with no accompanying Apex — gate it on `Global_Validations_Enabled__c` too, so it can be disabled the same way as validation trigger units. Every validation rule shipped with fusionCore follows this pattern (for example `PricebookEntry.Block_Standard_Price_Change`).

Add the gate as another `AND` condition if your formula already uses the `AND()` function:

```
AND(
    $CustomMetadata.FCORE_BASE__Org_Setting__mdt.FCORE_BASE__Default.FCORE_BASE__Global_Validations_Enabled__c = TRUE,
    <your rule condition>
)
```

Or with the `&&` operator if your formula is written with infix logic:

```
$CustomMetadata.FCORE_BASE__Org_Setting__mdt.FCORE_BASE__Default.FCORE_BASE__Global_Validations_Enabled__c && (<your rule condition>)
```

<Note>
  Unlike a `ValidationTriggerUnit`, a declarative Validation Rule does not gate on `Global_Validations_Enabled__c` automatically — you must add the condition yourself. The flag also only gates the rule at runtime; it has no effect on ordering. If ordering matters, build the check as a `ValidationTriggerUnit` instead.
</Note>
