Skip to content

AfterInsert.Writer

Creates, updates or deletes other records, or publishes platform events, after new records are saved. Register the writes on the unit of work: they commit with the save, without DML in your loop.

Interface

apex
public class AfterInsert {
    public interface Writer extends Handler {
        Boolean writeOnAfterInsertWhen(TriggerHandler.InsertRecord record);
        void writeOnAfterInsert(TriggerHandler.InsertRecord record, TriggerHandler.UnitOfWork unitOfWork);
    }
}
  • writeOnAfterInsertWhen(record): called once per record in the chunk. Return true to write for the record.
  • writeOnAfterInsert(record, unitOfWork): called right after its predicate returns true, with this Writer’s unit of work.

Example

apex
public with sharing class ContactWriter implements AfterInsert.Writer {
    public Boolean writeOnAfterInsertWhen(TriggerHandler.InsertRecord record) {
        return record.isNotNull(Contact.AccountId);
    }

    public void writeOnAfterInsert(TriggerHandler.InsertRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        unitOfWork.toInsert(new Task(WhatId = ((Contact) record.getNewSObject()).AccountId, Subject = 'Review contact'));
    }
}
cls
public with sharing class AccountWelcomeTaskWriter implements AfterInsert.Writer, AfterInsert.ParentQuery, AfterInsert.ContinueOnError {
    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterInsert() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Account.OwnerId => TriggerHandler.ParentFields.with(User.Name, User.Email) };
    }

    public Boolean writeOnAfterInsertWhen(TriggerHandler.InsertRecord record) {
        return record.startsWith(Account.Type, 'Customer');
    }

    public void writeOnAfterInsert(TriggerHandler.InsertRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        Account accountRecord = (Account) record.getNewSObject();
        User owner = (User) record.getNewParent('Owner');

        unitOfWork.toInsert(
            new Task(
                WhatId = record.getId(),
                OwnerId = accountRecord.OwnerId,
                Subject = 'Onboarding call - ' + accountRecord.Name,
                Description = 'New customer assigned to ' + owner?.Name + ' (' + owner?.Email + '). Confirm the billing address and the primary contact.',
                ActivityDate = Date.today().addDays(3),
                Priority = 'High',
                Status = 'Not Started'
            )
        );
    }
}
cls
public with sharing class ContactOwnerAlignmentWriter implements AfterInsert.Writer, AfterInsert.ParentQuery, AfterInsert.Bypassable {
    public static Boolean isDisabled = false;

    public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterInsert() {
        return new Map<SObjectField, TriggerHandler.ParentFields>{ Contact.AccountId => TriggerHandler.ParentFields.with(Account.OwnerId).with('Owner', User.IsActive) };
    }

    public Boolean bypassOnAfterInsertWhen() {
        return ContactOwnerAlignmentWriter.isDisabled;
    }

    public Boolean writeOnAfterInsertWhen(TriggerHandler.InsertRecord record) {
        Contact contactRecord = (Contact) record.getNewSObject();
        Account accountRecord = (Account) record.getNewParent('Account');

        return accountRecord?.Owner?.IsActive == true && accountRecord.OwnerId != contactRecord.OwnerId;
    }

    public void writeOnAfterInsert(TriggerHandler.InsertRecord record, TriggerHandler.UnitOfWork unitOfWork) {
        Account accountRecord = (Account) record.getNewParent('Account');

        unitOfWork.toUpdate(new Contact(Id = record.getId(), OwnerId = accountRecord.OwnerId));
    }
}

Good to Know

  • Register, don't run DML. By default, Writers share one unit of work that merges their writes and commits once, after the last handler. A direct insert or update runs at once and skips the unit.
  • Later handlers don't see your writes. Registered records are not in the database until the unit commits. A later Writer reads the rows as they were saved.
  • The row is read-only. record.put(…) throws, and the insert fails. To change the new record, register toUpdate(new Contact(Id = record.getId(), …)). That saves it again and runs the update triggers.
  • Inserting the same object runs after insert again. Insert contexts have no RecursionGuard. Only your predicate stops the chain.
  • Writer wins. A class that also implements AfterInsert.Dispatcher runs only as a Writer.

Test

apex
@IsTest
static void writeOnAfterInsertRegistersOnboardingTask() {
    // Setup
    Id accountId = new TriggerHandler.RandomIdGenerator().get(Account.SObjectType);
    TriggerHandler.InsertRecord record = new TriggerHandler.TriggerRecord(new Account(Id = accountId, Name = 'Acme', Type = 'Customer - Direct'), null);
    RecordingUnitOfWork unitOfWork = new RecordingUnitOfWork();

    // Test
    new AccountWelcomeTaskWriter().writeOnAfterInsert(record, unitOfWork);

    // Verify
    Assert.areEqual('Onboarding call - Acme', ((Task) unitOfWork.inserted[0]).Subject, 'The task subject should name the account.');
}

RecordingUnitOfWork is a small test class that implements TriggerHandler.UnitOfWork and keeps what it receives. See Testing.