AfterUpdate.Writer
Changes other records after the update: children, the parent, a follow-up task. Register the writes on the unit of work, and the library commits them with the save.
Interface
apex
public class AfterUpdate {
public interface Writer extends Handler {
Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record);
void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork);
}
}writeOnAfterUpdateWhen(record): called once per record in the chunk; a record that has used up its recursion budget is skipped without a call. Returntrueto write for the record.writeOnAfterUpdate(record, unitOfWork): called right after its predicate returns true, with this Writer’s unit of work.
Example
apex
public with sharing class ContactWriter implements AfterUpdate.Writer {
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 OpportunityAccountTypeWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery, AfterUpdate.RecursionGuard {
public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
return new Map<SObjectField, TriggerHandler.ParentFields>{ Opportunity.AccountId => TriggerHandler.ParentFields.with(Account.Type) };
}
public Integer maxRecursionDepthOnAfterUpdate() {
return 1;
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
Account parentAccount = (Account) record.getNewParent('Account');
return record.isChangedTo(Opportunity.StageName, 'Closed Won') && parentAccount != null && parentAccount.Type != 'Customer - Direct';
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
Opportunity newOpportunity = (Opportunity) record.getNewSObject();
unitOfWork.toUpdate(new Account(Id = newOpportunity.AccountId, Type = 'Customer - Direct'));
}
}cls
public with sharing class OpportunityWinTaskWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery, AfterUpdate.ContinueOnError {
public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
return new Map<SObjectField, TriggerHandler.ParentFields>{ Opportunity.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChangedTo(Opportunity.StageName, 'Closed Won');
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
Opportunity newOpportunity = (Opportunity) record.getNewSObject();
Account parentAccount = (Account) record.getNewParent('Account');
unitOfWork.toInsert(
new Task(
WhatId = record.getId(),
OwnerId = newOpportunity.OwnerId,
Subject = 'Onboarding kick-off: ' + (parentAccount?.Name ?? newOpportunity.Name),
Description = 'Won on ' + Date.today().format() + '. Agree the implementation plan with the customer.',
ActivityDate = Date.today().addDays(7),
Priority = 'High',
Status = 'Not Started'
)
);
}
}Good to Know
- Register, do not run DML.
toInsert,toUpdate,toUpsert,toDeleteandtoPublishtake one record each. By default, the unit commits after the last handler, in system mode without sharing. Direct DML runs at once and bypasses the unit. - Register new instances. The trigger rows are read-only. Build a new record with the Id and only the fields to change, such as
new Account(Id = accountId, Type = 'Customer - Direct'). - Gate on a change. A
toUpdateof records of this object runs the update triggers again. Qualify withisChangedorisChangedTo, so the nested run skips the record. - Nothing is saved inside the handler. A
toInsertrecord has no Id yet in the action or the Finalizer. For user mode, partial success or commit results, add OwnUnitOfWork. - Writer wins. A class that also implements
AfterUpdate.Dispatcherruns only as a Writer.
Test
apex
@IsTest
static void writeOnAfterUpdateWhenStageChangedToClosedWon() {
// Setup
TriggerHandler.UpdateRecord record = new TriggerHandler.TriggerRecord(new Opportunity(StageName = 'Closed Won'), new Opportunity(StageName = 'Negotiation/Review'));
// Test
Boolean result = new OpportunityWinTaskWriter().writeOnAfterUpdateWhen(record);
// Verify
Assert.isTrue(result, 'The record should qualify.');
}