Salesforce Apex Triggers: Automate Actions on Records

Flow Builder handles most record-based automation. But when the logic is complex, involves large data volumes, or needs precise control over timing, developers write Apex Triggers. A trigger is a block of Apex code that Salesforce executes automatically whenever a specific database event occurs on a particular object — no user click required.

What Is a Trigger?

A trigger is a piece of code that says: "When this event happens on this object, run this logic." Events include inserting a new record, updating an existing one, deleting it, or undeleting it. Triggers fire at the database level, which makes them the most reliable and powerful form of automation in Salesforce.

The Motion Sensor Analogy

  MOTION SENSOR LIGHT:
  Trigger: Someone walks into the room
  Action: Light turns on automatically

  APEX TRIGGER:
  Trigger: A new Opportunity is inserted with Stage = "Closed Won"
  Action: Create an onboarding Case, email the delivery team,
          update the Account's customer tier — all automatically

Trigger Syntax

Every trigger follows this structure:

trigger TriggerName on ObjectName (trigger_events) {
    // Your logic here
}

A Simple Trigger Example

trigger OpportunityTrigger on Opportunity (after update) {

    List<Case> casesToCreate = new List<Case>();

    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);

        // Check if Stage just changed to Closed Won
        if (opp.StageName == 'Closed Won'
            && oldOpp.StageName != 'Closed Won') {

            Case newCase = new Case();
            newCase.Subject = 'Onboarding: ' + opp.Name;
            newCase.AccountId = opp.AccountId;
            newCase.Priority = 'High';
            newCase.Status = 'New';
            casesToCreate.add(newCase);
        }
    }

    if (!casesToCreate.isEmpty()) {
        insert casesToCreate;
    }
}

Trigger Events

A trigger specifies which database events cause it to fire. Events are listed in the trigger declaration, separated by commas:

EventWhen It Fires
before insertBefore a new record is saved to the database
after insertAfter a new record is saved to the database
before updateBefore an existing record's changes are saved
after updateAfter an existing record's changes are saved
before deleteBefore a record is deleted
after deleteAfter a record is deleted
after undeleteAfter a record is restored from the Recycle Bin

Before vs. After Triggers

  • Before triggers — run before the record is written to the database. Use them to validate or modify field values on the record being saved. Changes to Trigger.new take effect without a DML statement — efficient and avoids extra operations.
  • After triggers — run after the record is committed. Use them when you need the record's final Id (assigned by the database) or when you need to update related records on other objects.

Trigger Context Variables

Inside every trigger, Salesforce provides special variables that give you access to the records being processed:

Context VariableWhat It ContainsAvailable In
Trigger.newList of new record versions (after changes)insert, update, undelete
Trigger.oldList of original record versions (before changes)update, delete
Trigger.newMapMap of Id to new record versioninsert, update, undelete
Trigger.oldMapMap of Id to old record versionupdate, delete
Trigger.isInsertTRUE if this is an insert eventAll events
Trigger.isUpdateTRUE if this is an update eventAll events
Trigger.isDeleteTRUE if this is a delete eventAll events
Trigger.isBeforeTRUE if running before the saveAll events
Trigger.isAfterTRUE if running after the saveAll events

The Handler Class Pattern

A common mistake beginners make is putting all logic directly inside the trigger file. As the logic grows, the trigger becomes impossible to maintain or test. Professional developers use the Handler Class Pattern: the trigger file stays thin and delegates all logic to a separate Apex class.

// TRIGGER FILE (thin — just routes to the handler)
trigger OpportunityTrigger on Opportunity (before insert, after update) {
    OpportunityTriggerHandler handler = new OpportunityTriggerHandler();

    if (Trigger.isBefore && Trigger.isInsert) {
        handler.beforeInsert(Trigger.new);
    }
    if (Trigger.isAfter && Trigger.isUpdate) {
        handler.afterUpdate(Trigger.new, Trigger.oldMap);
    }
}

// HANDLER CLASS (contains all the logic)
public class OpportunityTriggerHandler {

    public void beforeInsert(List<Opportunity> newOpps) {
        for (Opportunity opp : newOpps) {
            if (opp.LeadSource == null) {
                opp.LeadSource = 'Web';
            }
        }
    }

    public void afterUpdate(List<Opportunity> newOpps,
                            Map<Id, Opportunity> oldMap) {
        // complex logic here
    }
}

Benefits of this pattern:

  • Easy to unit test — test the handler class directly without simulating trigger events
  • Easy to read — each method has a clear, named purpose
  • Easy to maintain — adding logic means adding a method, not untangling a long trigger file

One Trigger Per Object Rule

Salesforce allows multiple triggers on the same object, but the order in which they fire is not guaranteed. Two triggers on Opportunity might run in any sequence — making behavior unpredictable and debugging very difficult. The professional standard is one trigger per object that routes to a handler class. All logic for that object lives in one organized handler.

Trigger Best Practices

  • Keep triggers thin — put all logic in a handler class.
  • Bulkify all logic — assume Trigger.new always contains up to 200 records, never just one.
  • Never write SOQL or DML inside a loop.
  • Use Trigger.oldMap to detect field changes accurately.
  • Write test classes that cover at least 75% of your trigger code — Salesforce requires this before deployment to Production.

Key Points

  • Triggers fire automatically on database events: before/after insert, update, delete, and undelete.
  • Before triggers modify records without extra DML; after triggers handle related record updates and require DML.
  • Context variables like Trigger.new, Trigger.old, Trigger.newMap, and Trigger.oldMap give access to record data inside the trigger.
  • Use the Handler Class Pattern — keep the trigger file thin and put logic in a separate Apex class.
  • Write one trigger per object and always bulkify logic to handle up to 200 records at a time.

Leave a Comment