Skip to content

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. Return true to 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, toDelete and toPublish take 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 toUpdate of records of this object runs the update triggers again. Qualify with isChanged or isChangedTo, so the nested run skips the record.
  • Nothing is saved inside the handler. A toInsert record 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.Dispatcher runs 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.');
}