Skip to content

AfterUpdate.Bypassable

Skips an after update Writer or Dispatcher for the whole chunk when a condition holds, such as a static flag, a batch job or a custom permission.

Interface

apex
public class AfterUpdate {
    public interface Bypassable {
        Boolean bypassOnAfterUpdateWhen();
    }
}
  • bypassOnAfterUpdateWhen(): called once per chunk, before parents load; not called when metadata or TriggerOrchestrator.bypass() already skips the handler. Returns true to skip this handler for this chunk.

Example

apex
public with sharing class ContactWriter implements AfterUpdate.Writer, AfterUpdate.Bypassable {
    public static Boolean isDisabled = false;

    public Boolean bypassOnAfterUpdateWhen() {
        return ContactWriter.isDisabled;
    }

    public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChanged(Contact.AccountId);
    }

    public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        unitOfWork.toInsert(new Task(WhatId = ((Contact) record.getNewSObject()).AccountId, Subject = 'Review contact'));
    }
}
cls
public with sharing class AccountOwnerTransferWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery, AfterUpdate.PriorParentQuery, AfterUpdate.RelatedQuery, AfterUpdate.Bypassable {
    public static Boolean isDisabled = false;

    public Boolean bypassOnAfterUpdateWhen() {
        return isDisabled || System.isBatch();
    }

    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Account.OwnerId => TriggerHandler.ParentFields.with(User.Name) };
    }

    public Map<SObjectField, TriggerHandler.ParentFields> queryPriorParentsOnAfterUpdate() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Account.OwnerId => TriggerHandler.ParentFields.with(User.Name) };
    }

    public Map<String, AfterUpdate.RecordsProvider> queryRelatedOnAfterUpdate() {
        return new Map<String, AfterUpdate.RecordsProvider>{ 'openOpportunities' => new AccountOpenOpportunitiesProvider() };
    }

    public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChanged(Account.OwnerId);
    }

    public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        Account accountRecord = (Account) record.getNewSObject();
        User previousOwner = (User) record.getOldParent('Owner');
        User newOwner = (User) record.getNewParent('Owner');
        String handoverNote = Date.today().format() + ' - accountRecord handed over from ' + previousOwner?.Name + ' to ' + newOwner?.Name;

        for (SObject relatedRecord : record.getRelated('openOpportunities').getAllWhereKeyEquals(record.getId())) {
            Opportunity opportunityRecord = (Opportunity) relatedRecord;

            unitOfWork.toUpdate(
                new Opportunity(Id = opportunityRecord.Id, OwnerId = accountRecord.OwnerId, Description = this.notedDescription(handoverNote, opportunityRecord.Description))
            );
        }
    }

    private String notedDescription(String handoverNote, String description) {
        return String.isBlank(description) ? handoverNote : handoverNote + '\n' + description;
    }

    private with sharing class AccountOpenOpportunitiesProvider implements AfterUpdate.RecordsProvider {
        public List<SObject> query(TriggerHandler.UpdateRecords records) {
            return [
                SELECT Id, AccountId, Description
                FROM Opportunity
                WHERE AccountId IN :records.getIds() AND IsClosed = FALSE
            ];
        }

        public String keyOf(SObject record) {
            return ((Opportunity) record).AccountId;
        }
    }
}

Good to Know

  • Whole chunk, not single records. To skip single records, return false from the predicate.
  • Reset static flags in finally. A static flag lasts for the whole transaction, nested saves included.
  • Outside the error handling. An exception here is not logged, ContinueOnError does not apply, and the update fails.
  • Other switches work in every context. TriggerOrchestrator.bypass().handler(X.class) and a TriggerHandler__mdt row switch a handler off wherever it runs. See Bypassing.
  • Inner classes. TriggerOrchestrator.bypass().handler(X.class) never matches an inner class. Use a TriggerHandler__mdt row or this add-on.