Why This Is Useful
Regular Apex triggers run inside the user’s transaction. Which means every bit of logic you put in one makes the save slower, and every governor limit you consume is a limit the rest of that transaction can’t use.
Change Event triggers flip that. They fire after the transaction commits, in their own asynchronous context, with their own fresh set of limits. The user’s save completes immediately and your logic runs a moment later.
They’re built on Change Data Capture, and they’re genuinely underused.
Code’s on GitHub.
Turning It On
Before you can write one, tell Salesforce to publish the events:
Setup → Change Data Capture, then move the object you care about into the Selected Entities list.
Note the limit here: on most editions you get a handful of objects selected for free, and more costs money. So pick deliberately rather than enabling everything.
The Code
//If you want to debug this you need to put a log on the Automated Process user.
//It is the only way to debug these triggers.
trigger AccountChangeEvent_Trigger on AccountChangeEvent (after insert)
{
System.debug('Change Event Trigger size ::: ' + Trigger.new.size());
//Everything you really care about lives in the ChangeEventHeader.
for (AccountChangeEvent ace : Trigger.new)
{
EventBus.ChangeEventHeader aceHeader = ace.ChangeEventHeader;
List<String> acctIds = aceHeader.getRecordIds();
String userId = aceHeader.getCommitUser();
}
}Three things to notice immediately.
1. The object is AccountChangeEvent, not Account. Salesforce generates a <Object>ChangeEvent for every object you enable.
2. The only event is after insert. That’s it. There’s no before, no update, no delete, because you’re reacting to an event being published, not to a record being saved. A record deletion still arrives as an insert of a change event.
3. Everything useful is in the ChangeEventHeader. This is the bit that catches people out, so let’s cover it properly.
The Header Is Where Everything Lives
EventBus.ChangeEventHeader header = ace.ChangeEventHeader;
header.getRecordIds(); //List<String> - which records changed
header.getChangeType(); //CREATE, UPDATE, DELETE, UNDELETE
header.getChangedFields(); //List<String> - which fields changed
header.getCommitUser(); //who made the change
header.getEntityName(); //e.g. 'Account'
header.getCommitTimestamp();getRecordIds() returns a list, not a single Id, and that’s important. Salesforce batches changes: if one save updated 200 Accounts identically, you may get one event covering all 200 record Ids.
So the bulkification lesson from episode 30 applies twice over here. You’re looping over events, and each event has multiple record Ids. Gather them all up before you query:
Set<Id> allChangedIds = new Set<Id>();
for (AccountChangeEvent ace : Trigger.new)
{
allChangedIds.addAll((List<Id>) ace.ChangeEventHeader.getRecordIds());
}
//ONE query for everything
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :allChangedIds];getChangedFields() is the other genuinely useful one. It lets you skip work when nothing relevant moved:
if (!ace.ChangeEventHeader.getChangedFields().contains('AnnualRevenue'))
{
continue;
}Debugging Them Is Genuinely Annoying
That comment at the top of the trigger is there because it cost me time.
Change Event triggers run as the Automated Process user, not as you. So you can save a record, watch the trigger clearly do something, and find absolutely nothing in your debug logs.
The fix: Setup → Debug Logs → New, and set the traced entity to the Automated Process user. Then you’ll see everything.
Same reason applies to permissions. Your trigger runs as Automated Process, which has its own access. If your logic touches something that user can’t see, it fails, and it fails quietly because there’s no user watching.
When To Use Them
Good fits: syncing to an external system, audit logging, recalculating rollups that don’t need to be instant, anything where a short delay is genuinely fine.
Bad fits: anything the user must see immediately, and anything that needs to prevent a save. You cannot call addError() here, because the record was committed before your code ran. Validation belongs in a normal before trigger.
A couple of other things worth knowing: events are retained for 72 hours, and delivery is guaranteed but ordering is not. Design accordingly.
If you just want to move slow logic out of a normal trigger, a Queueable job is usually simpler. Reach for Change Events when you genuinely want to react to data changing regardless of what caused it, including changes from the API, Data Loader, or another system entirely.
Get Coding With The Force Merch!!
We now have a redbubble store setup so you can buy cool Coding With The Force merchandise! Please check it out! Every purchase goes to supporting the blog and YouTube channel.
Get Shirts Here!
Get Cups, Artwork, Coffee Cups, Bags, Masks and more here!
Check Out More Coding With The Force Stuff!
If you liked this post make sure to follow us on all our social media outlets to stay as up to date as possible with everything!
Youtube
Patreon
Github
Facebook
Twitter
Instagram
Salesforce Development Books I Recommend
Advanced Apex Programming
Salesforce Lightning Platform Enterprise Architecture
Mastering Salesforce DevOps
Good Non-SF Specific Development Books:
Clean Code
Clean Architecture