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 orTriggerOrchestrator.bypass()already skips the handler. Returnstrueto 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 aTriggerHandler__mdtrow switch a handler off wherever it runs. See Bypassing. - Inner classes.
TriggerOrchestrator.bypass().handler(X.class)never matches an inner class. Use aTriggerHandler__mdtrow or this add-on.
