Overview

The ApexTrigger metadata type represents an Apex trigger that executes before or after Data Manipulation Language (DML) operations (insert, update, delete, undelete) on a standard object, custom object (__c), platform event (__e), or Change Data Capture event (*ChangeEvent).

In a Salesforce DX project, each trigger is stored in force-app/main/default/triggers/ as a pair of files: TriggerName.trigger (the executable Apex definition) and TriggerName.trigger-meta.xml (the Metadata API descriptor). Use the metadata descriptor to bind the trigger’s apiVersion and toggle its status between Active and Inactive during bulk data migrations or emergency hotfixes. Always keep the trigger body minimal and delegate business logic to an Apex Trigger Handler Pattern class.

DML Operation (Insert / Update / Delete / Undelete) Before Triggers (UsageBeforeInsert / Update / Delete) System & Custom Validation Rules Save to Database (Not Committed) After Triggers (UsageAfterInsert / Update / Delete / Undelete) Assignment, Auto-Response & Record-Triggered After Flows Commit Transaction to Database

In the Salesforce Order of Execution shown above, ApexTrigger components fire at two critical checkpoints: Before Triggers modify field values on Trigger.new without extra DML statements before validation rules run, and After Triggers read system-generated Id values and update related records after database save.

Prerequisites

  • Permissions: “Author Apex” and “Modify Metadata Through Metadata API Functions” permissions.
  • Target Object: The target sObject, Platform Event, or Change Event must exist in the org.
  • API Version: Verified against API version 68.0 (Winter ’27).

ApexTrigger XML Metadata Structure

The TriggerName.trigger-meta.xml file contains three configurable properties:

XML Element Type Required Valid Values & Architect Guidance
apiVersion double ✅ Yes API version (e.g., 68.0). Determines SOQL behavior and governor limit enforcement rules.
status ApexCodeUnitStatus ✅ Yes Active (runs on DML), Inactive (compiled but disabled), or Deleted (removes trigger in managed package upgrades).
packageVersions PackageVersion[] No Specifies the namespace, majorNumber, and minorNumber of referenced managed packages.

Tip

While you can set <status>Inactive</status> in TriggerName.trigger-meta.xml to disable a trigger during a data load, changing XML metadata in Production requires a deployment and test run. For zero-deployment control during data loads, build a hierarchy Custom Setting or Custom Metadata bypass switch inside your Apex Trigger Handler Pattern.

Querying ApexTrigger via SOQL

The ApexTrigger object is available in standard SOQL and Tooling API, exposing boolean flags (UsageBeforeInsert, UsageAfterUpdate, etc.) for every DML event a trigger subscribes to.

1. Audit All Triggers and Their Subscribed DML Events on an Object

Run this standard SOQL query to inspect every trigger registered on Account or any custom object:

SOQL
SELECT Id, Name, TableEnumOrId, Status, ApiVersion, IsValid,
       UsageBeforeInsert, UsageBeforeUpdate, UsageBeforeDelete,
       UsageAfterInsert, UsageAfterUpdate, UsageAfterDelete, UsageAfterUndelete,
       LengthWithoutComments
FROM ApexTrigger
WHERE TableEnumOrId = 'Account'
ORDER BY Name ASC

2. Detect Objects Violating the “One Trigger Per Object” Best Practice

Having multiple active triggers on the same object results in non-deterministic execution order. Use this query to find offending objects across your org:

SOQL
SELECT TableEnumOrId, COUNT(Id) activeTriggerCount
FROM ApexTrigger
WHERE Status = 'Active'
  AND NamespacePrefix = null
GROUP BY TableEnumOrId
HAVING COUNT(Id) > 1

3. Find Invalid or Inactive Triggers in Production

Identify triggers that failed recompilation after a schema change or were left inactive after a migration:

SOQL
SELECT Name, TableEnumOrId, Status, IsValid, LastModifiedBy.Name, LastModifiedDate
FROM ApexTrigger
WHERE Status = 'Inactive' OR IsValid = false
ORDER BY LastModifiedDate DESC

Example

The following example shows a complete InvoiceTrigger metadata pair (InvoiceTrigger.trigger and InvoiceTrigger.trigger-meta.xml) paired with a bulk-safe unit test that satisfies Production deployment coverage rules.

force-app/main/default/triggers/InvoiceTrigger.trigger-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>68.0</apiVersion>
    <status>Active</status>
</ApexTrigger>
force-app/main/default/triggers/InvoiceTrigger.trigger
trigger InvoiceTrigger on Account (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete
) {
    new AccountTriggerHandler().run();
}
force-app/main/default/classes/InvoiceTriggerTest.cls
@IsTest
private class InvoiceTriggerTest {

    @IsTest
    static void executesTriggerOnBulkInsert() {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(Name = 'Billing Account ' + i));
        }

        Test.startTest();
        insert accounts;
        Test.stopTest();

        Integer countInserted = [SELECT COUNT() FROM Account WHERE Name LIKE 'Billing Account %'];
        Assert.areEqual(200, countInserted, 'All 200 accounts should be processed by the trigger');
    }
}

Deploy only the trigger and run its specific test class in Production:

Terminal
sf project deploy start \
  --metadata ApexTrigger:InvoiceTrigger \
  --test-level RunSpecifiedTests \
  --tests InvoiceTriggerTest \
  --target-org Production

Limits and Gotchas

  • Zero-Coverage Deployment Failure: Even if your org has 95% overall Apex test coverage, deploying an active ApexTrigger with 0% test coverage causes the Production deployment to fail immediately.
  • 200-Record Batch Chunks: Salesforce invokes an ApexTrigger with at most 200 records per chunk (Trigger.new size ≤ 200). For Platform Event triggers (__e), the maximum batch size is 2,000 event messages per invocation (configurable down to 1 via PlatformEventSubscriberConfig).
  • Shared Governor Limits: Every trigger on an object shares the single transaction’s governor limits (100 synchronous SOQL queries, 150 DML statements, 10,000毫秒 CPU time) with all other triggers, record-triggered Flows, and Apex classes in the save order.
  • No Wildcard Filtering by Object: In package.xml, <members>*</members> under <name>ApexTrigger</name> retrieves every trigger in the org; you cannot write <members>Account.*</members>.

Frequently asked questions

How do I deactivate an ApexTrigger in Production without deleting it?

Change the status element in the TriggerName.trigger-meta.xml file from Active to Inactive and deploy it via sf project deploy start. In Production, running local or specified Apex tests is still required.

What is the code coverage requirement for deploying an ApexTrigger to Production?

Every active ApexTrigger included in a Production deployment must have at least 1% (non-zero) test coverage, in addition to the overall 75% org-wide Apex code coverage requirement.

How can I find all objects that have multiple active triggers using SOQL?

Query the standard ApexTrigger object grouped by TableEnumOrId where Status = 'Active' and use HAVING COUNT(Id) greater than 1.

Can an ApexTrigger be subscribed to a Platform Event or Change Data Capture channel?

Yes. An ApexTrigger on a Platform Event (__e) or Change Event (*ChangeEvent) runs asynchronously under the Automated Process user (unless overridden by PlatformEventSubscriberConfig) and supports only the after insert event.

What is the difference between ApexTrigger and ApexClass status values?

ApexTrigger supports three status values in its metadata XML: Active, Inactive, and Deleted. ApexClass only supports Active and Deleted.

Comments

Questions, corrections and tips are welcome. Comments are GitHub Discussions, so you sign in with GitHub.