Introduction to Triggers

My last post, A Simple Trigger Framework, introduced a framework for triggers but didn’t cover the fundamentals of them. This post does.

What’s A Trigger?

Apex code that runs whenever a record is

  • Inserted
  • Updated
  • Deleted
  • Undeleted – A record that’s been restored from the recycling bin.

It runs whether the records are saved from the UI, through automation, or any APIs. It does not run when records are queried from the database.

Use Cases

  • Complex Business Logic Not Viable Declaratively
  • Validation – More Complex than Validation Rules
  • Defaulting Data
  • Data Aggregation

Complex Business Logic Not Viable Declaratively

Whenever possible, implement functionality using declarative means like process builder, flows, validation rules, etc so it’s easier to maintain and faster to develop.

With that said, declarative options aren’t always viable so Apex via a Trigger can implement it. For example, process builder and workflow rules are only available when a record is inserted or updated but not on deletion.

Another instance that falls into this category is when declarative functionality is too complex. Process builder and flows are great for simple to moderately complex functionality but when they get really complex, say 100+ elements in a flow which is arbitrary, then it may be easier to implement with Apex code because code’s instructional density is much higher. Put differently, you can express the same instructions with much less code than in a flow or process builder and it’s likely to run faster too. A hybrid approach is possible too because one can invoke Apex code from a process builder or flow and one can invoke a flow or process builder from apex so it’s not an all-or-nothing approach.

Validation

Whenever possible, implement validation using validation rules. They’re good for validating against fields on the record and parent records. They can also access custom settings, custom metadata, user, role, and profile information BUT they can’t however validate against child records. They also can’t validate on delete.

A trigger can validate on delete and essentially implement any kind of validation necessary. For example, one can implement the requirement that an order can be deleted but only if it’s empty meaning it has no order line items.

Defaulting Data

Triggers are also commonly used to populate fields on records before they’re inserted or updated. This seems to usually be conditional where if certain complex criteria is met then the value should be X, Y or Z. It could also be that the computed value was complicated to compute and required much code to do so a trigger was needed.

Data Aggregation

Data aggregation involves computing some value(s) from one or more child records and storing them on a parent record.

A common example is a Rollup Summary field where one can display a count or sum on a parent record based on its child records. Rollup summaries are great for Master-Detail relationships but they don’t support Lookup relationships.

To recalculate the aggregated data onto the parent record whenever a child record is inserted, updated, and deleted, a trigger is used to recalculate the data and store it on the parent records. This is a common scenario which is why Andy Fawcett and other contributors created the Declarative Lookup Rollup Summary which allows admins to install the package and create rollup summaries on Master-Details and Lookups and do additional aggregate operations. One of my favorite is the “Concatenate” operation which lets one generate a delimited list of values and store them in a field on the parent record. That’s really useful for reporting and filtering without having to always query the child records or looking at the related records.

Another example is accounts some times have memberships or subscriptions as child records but they only have one “current membership” that should be seen on the account. One way to solve this is to create a “Current Membership” or “Current Subscription” lookup on the account to that corresponding child record and then have a trigger maintain that when membership and subscription records are inserted, updated, or deleted. One could then use account formula fields to read the key membership or subscription data elements and perform other calculations if needed. This would be a “real-time” way of keeping this data synchronized.

Trigger Syntax

<trigger_name> – The name of the trigger. I typically name it “Object>Trigger” so if I’m adding an Account Trigger it’s name is “AccountTrigger”.

<Object_Name> – The object’s API Name so Salesforce knows which object this is for.

<event_list> – The comma separated list of events on which this trigger should run. For example, “before update, before insert, after update, after insert”.

Trigger Syntax Documentation

Events

  • Before Insert
  • Before Update
  • Before Delete
  • After Insert
  • After Update
  • After Delete
  • After Undelete

The events instruct Salesforce when to run the trigger. They are split into two categories, “Before” and “After”. These relate to when the records are saved but not yet committed to the database. The Before events run before the records are saved to the database while the After events run after they’ve been saved to the database.

Whenever possible, run your code in the Before event because it’ll run sooner. The Before events are also where validation and defaulting and setting field values are done most often. Setting field values on before insert and before update is preferred because it doesn’t cause the trigger to fire again. Since setting the fields on after insert or after update won’t cause the fields to be updated since they’ve already been saved, but not yet committed, to the database, one has to update the records again which may cause the trigger to fire again.

When the before insert event runs, the records aren’t saved to the database yet so they don’t have record Ids yet. This can be problematic when you want to create child records when a record gets created. The solution is to create these records on after insert instead.

In the Before Update and After update methods, one has access to the record before it was updated and to the updated one also which allows the record to be compared and see which fields change. One best practice is to only take some action when a field or certain fields change and meet some criteria instead of always doing something when a record is updated because it saves on computing and makes everything faster.

After Undelete and After Delete are rarely used in my experience. Before Insert, Before Update, After Insert, and After Update are the most commonly used events. Before Delete typically is used to prevent a record from being deleted or to do a cascade delete because the child object has a lookup and doesn’t want to prevent deletion because of locking issues.

Trigger Events Documentation

Trigger Context Object

Salesforce provides a “Trigger” Apex Object that has various static properties that one uses in a trigger to determine which event is being executed and the records being inserted, updated, or deleted.

Trigger Context Properties

  • isExecuting – A boolean that indicates if the current code is running within a trigger. Helpful for determining what actions can be done when. For example, apex callouts can’t be done from within a trigger so you may start an async operation to do it.
  • isInsert – A boolean that indicates if this is an insert event.
  • isUpdate – A boolean that indicates if this is an update event.
  • isDelete – A boolean that indicates if this is a delete event.
  • isBefore – A boolean that indicates if this is a Before event.
  • isAfter – A boolean that indicates if this is an After event.
  • isUndelete – A boolean that indicate if this is an Undelete event.
  • new – A list of new records. On Before Insert, these records don’t have Ids. On After Insert, they do. On Before Update and After Update, these records are the ones with the latest values. This collection doesn’t contain anything on After Delete or Before Delete.
  • newMap – A map of records whose key is the record id and whose value is the SObject record. Not applicable on Before Insert since the records don’t have Ids yet. In the update events, it contains the records with the latest field updates.
  • old – A list of records. On the update events, it contains the records before they were updated. On the delete events, it contains the records that were deleted. It’s not applicable to the insert events.
  • oldMap – A map of records whose key is the record id and whose value is the SObject record. Used most often in the update events so one can fetch the original records and compare them to the updated ones to determine what changed. Not applicable on the Insert events.
  • size – An integer describing how many records are in the batch.

To determine if the trigger is running in a particular event, one has to inspect two context variables, “isBefore or isAfter” and the desired “isInsert, isUpdate, isDelete” properties.

Trigger Context Documentation

Triggers Are Bulkified

When a trigger runs, it may be executing on up to 200 records at a time. Other databases have triggers that run once for each record inserted, updated, or deleted. In Salesforce, records are chunked into batches of 200 and then the trigger runs on each chunk of records. For example, if 1,000 records were inserted, a trigger would run 5 times, once for each set of 200 records.

Insert Validation Example

Let’s finally take a look at an example.

This example validates on before insert that if the account is inserted with a name of “bad”, the record isn’t saved and one sees “Bad Name” as the error message. In a normal account page, this error would show up at the top. If the error should show on a field use the field’s addError method with the error name as an argument. For instance, newAcct.Name.addError(‘Bad Name’); would show the Bad Name error by the account name instead.

Disclaimer: This simple, contrived example shows how many triggers are written in the Salesforce documentation and throughout the web but it’s not the preferred way of doing it because it leads to Spaghetti code. With a lot of checks to see which event is being executed and having to do things in bulk, the code often becomes very hard to read which usually leads to bugs and higher maintenance costs. See below for a better way using the Simple Trigger Framework.

Update Validation Example

Let’s take a look at an update example now.

This example loops through each updated account on before update and if the name changed and contains “Luke”, the account is not saved and the error “Choose a better name” is shown at the top.

Disclaimer: This simple, contrived example shows how many triggers are written in the Salesforce documentation and throughout the web but it’s not the preferred way of doing it because it leads to Spaghetti code. With a lot of checks to see which event is being executed and having to do things in bulk, the code often becomes very hard to read which usually leads to bugs and higher maintenance costs. See below for a better way using the Simple Trigger Framework.

Simple Framework Insert Validation Example

This is a better way to implement the Account Insert Validation example. See my Simple Trigger Framework for why it’s better.

Best Practices

Declarative First

As much as possible, implement functionality through declarative means. For example, workflow rules and process builders are triggered when records are inserted or updated and their actions can be used to handle most common, custom functionality. For more complex things, a process builder can be paired with a Flow so the Flow does even more heavy lifting.

Declarative First

Not a typo. This is how imperative this is.

If code really must be used, consider that a hybrid solution could be used. For example, process builders and Flows can invoke Apex code for the really complex pieces. A process builder could be used as the firing mechanism which lets an admin control, declaratively, when the code runs. Similarly, code can invoke Flows also so one can delegate certain things to Flows. This is easily a separate post or series of them but wanted to bring this to your attention in case you didn’t know.

Bulkified

Whenever a trigger runs, it’s running on up to 200 records at a time. This means the trigger code has to be bulkified too. If 10,000 records were inserted, updated, or deleted, a trigger would run 50 times. This means that if it’s fetching configuration data from a custom setting, custom metadata, or a custom object, you probably want to cache it and lazy load it in a static variable or collection.

One Trigger Per Object

One trigger per object should be used despite Salesforce allowing multiple triggers per object. This allows one to control the execution order of code. With multiple triggers, the order in which they’re executed is random and non-deterministic which means they may run in one order and then in a different order the next time.

It also helps keep all the code centralized so updating it is in one place which makes maintenance easier, especially when paired with a trigger framework.

Logicless

Triggers should be logicless aka they should delegate their code to another class such as a Trigger Handler or one or more Service classes. This helps keep the code cleaner and more organized which makes it easier to maintain, enhance, and test.

Use A Trigger Framework

Use a trigger framework to help manage the triggers throughout your code base. See my Simple Trigger Framework for one example and it also has links to others.

Considerations

Order of Operations

One has to consider the order of operations that happen when a record is inserted, updated or deleted. See orders of operations documentation for details. This usually comes into play when trying to optimize code or trying to determine when things are happening to records upon save across your code and declarative functionality.

Recursion

With more complex customization a trigger may be invoked multiple times in the same execution context. For example, a trigger updates some child records which fires the child trigger or process builder which then causes the parent records to be updated again which causes the original trigger to fire again. This can be troublesome when you’re hitting various limits such as the Too Many SOQL queries limit or Too Many DML Statement limit.

Some common solutions are:

  • Detect & Avoid – Have the trigger code keep track of which records were already handled and if the same ones are firing again, skip running the code again. This can be problematic though if the trigger should be fired again so this is a delicate balancing act.
  • Async – Perhaps the operation can be handled asynchronously instead if it doesn’t need to be real-time.
  • Scheduled Job – A scheduled job may be a good fit if the operation can be done later. The trigger could simply mark the records for later processing by updating a status field for example.

Many Triggers Per Object Despite You Not Doing It

You’re creating one trigger per object but your objects may still have multiple objects on them. This is usually because someone has installed one or more packages which have triggers on those objects

Triggers Don’t Always Fire

For example, child triggers don’t fire on delete when its parent in a master-detail is deleted. Child records are automatically deleted through a “cascade delete” when their parent records are deleted in a master-detail relationship.

Some Standard Objects Don’t Allow Triggers

Some Standard Objects, such as OpportunityContactRole, don’t allow triggers. More Detailed List.

1% Test Code Coverage

Every trigger must have at least 1% code coverage in order to be deployed to production or uploaded into a package. Everyone is pretty familiar with the 75% test code coverage limit but this one is mandatory too.

Other Resources

Happy Coding and let us know in the comments if you have any additional trigger information!

Leave a Reply

Your email address will not be published. Required fields are marked *