Salesforce, Python, SQL, & other ways to put your data where you need it

Need event music? 🎸

Live and recorded jazz, pop, and meditative music for your virtual conference / Zoom wedding / yoga class / private party with quality sound and a smooth technical experience

Document Salesforce TDTM to avoid dev-admin handoff issues

30 Sep 2021 🔖 salesforce
💬 EN

Table of Contents

Salesforce’s Nonprofit Success Pack (“NPSP”) and Education Data Architecture (“EDA”) installable packages both come with functionality called Table-Driven Trigger Management (“TDTM”).

The admin options are powerful enough to break dev code, so good practices around handoff are essential.

Trigger developers working in an org with one of these packages installed are encouraged to break their code up into “trigger handler” classes compliant with TDTM.

TDTM allows admins to change the way triggers fire with clicks, not code, including live in a production org (which wouldn’t be allowed for code deployment itself). That’s the point of it – to reintroduce a little flexibility in trigger management that isn’t baked into the Salesforce trigger-code platform.

However, it also means that admins have live-in-production control over how Apex classes fire in response to data being saved in the org, and that they might click buttons that developers of these Apex classes never intended to be clicked.

Whenever developers release a new suite of TDTM-ready trigger handlers into an org, they should include robust documentation and training for all current and future Salesforce administrators who will have to wade through the “Trigger Handlers” object in Salesforce and try to guess what its records are responsible for and whether they’re configured correctly.

Example problems

Asynchronous TDTM

Async TDTM kills context

When an admin or dev checks the Asynchonous After Events” box in NPSP or the Asynchronous” box in EDA on a record in the TDTM Trigger Handler object and saves it, the corresponding Apex code loses its ability to compare internal and Trigger.old Apex values. and Trigger.old are what Apex programmers use to determine whether specific field values actually changed during the data-editing event that fired off the trigger handler.

In my opinion, it’s often a good idea for Apex trigger handler developers to write code that inspects and Trigger.old.

A large amount of computationally expensive business logic (like updating related records) might only need to happen if a record-save involved a “relevant” field’s value changing.

In fact, it seems that NPSP and EDA developers themselves make heavy use of and Trigger.old, because the TDTM admin documents for both packages warn:

“Asynchronous After Events (7)—Allows this Apex class to run asynchronously. Requires that your Apex class supports asynchronous processing. NPSP classes are not designed to run asynchronously. Test thoroughly if you enable this option.”

“Asynchronous (10)—Allows this Apex class to run asynchronously. Requires that your Apex class supports asynchronous processing. EDA classes are not designed to run asynchronously. Test thoroughly if you enable this option.”

Workaround: sync TDTM with async code

Uh-oh! Asynchronicity is important, right?

At Dreamforce, people always seem to say that if you’re struggling to get records to save because of “CPU time” or other Salesforce governor limits, you need to have your developers make as many of your triggers “asynchronous” as possible, right?

The good news is, even if admins can’t save the day on limits issues by flipping the “Asynchronous” button in TDTM, that doesn’t mean that “asynchronous triggers” can’t save the day.

It just means that developers have to program asynchronicity into the Apex trigger handler itself’s codebase.

Developers can write trigger handler Apex code to:

  1. Do the comparisons that they need to do of and Trigger.old within an Apex “trigger handler” class summoned by TDTM synchronously.
  2. Make a list of all Salesforce object IDs that “are actually relevant & need to have work done on them.”
  3. Asynchronously invoke a different Apex class against only the pre-screened set of “IDs that need work” (rather than the larger set of “IDs that just went through record-saves” processed in step 1).
Developer note

Developers, see step 3? When you program that “different Apex class” to “do the work,” there are major dev-admin cooperation advantages to architecting it as a “service layer” à la Andy Fawcett.

“Service layer” code should accept a Set<Id> parameter indicating “IDs that need work” and then independently SOQL-query the database for any record details needed to do the work. Don’t let your service layer rely on cheats like inspecting for that data, even though, yes, you’re possibly “wasting” a SOQL query.

When you architect “service classes” like this, you can easily cooperate with admins to back-fill records after introducing a new trigger handler into an org, as if the trigger handler had been active all along.

All you have to do is manually pass some IDs into Anonymous Apex that invokes your “service class.”

(Whereas under normal execution, it’s a trigger handler that decides which IDs are worth passing to your “service class.”)

Admin caveat: TDTM async breaks the workaround

Note that when developers use the “workaround” pattern above to write Apex code for trigger handlers, the codebase will get confused if it’s run in asynchronous mode.

This is because the Apex that Salesforce wrote … the Apex that makes TDTM work … lies to the Apex code that your developers are writing.

When the Asynchronous box is checked, TDTM passes fake and Trigger.old values to your custom code. Custom trigger-handler code can choke on those values.

Often, you might see that trigger handlers you thought were tested & working suddenly break. Often with a System.NullPointerException: Attempt to de-reference a null object error that isn’t traceable to a specific line of code because it happens buried in a mysterious (System Code) context.


When a nonprofit decides to use TDTM to control their in-house trigger handlers, it’s important to:

  1. Develop a system for documenting what the TDTM record for a given trigger handler should vs. shouldn’t look like.
  2. Make sure all developers are trained by admins in what makes for a “thorough” piece of documentation and are trained never to approve a TDTM-oriented “trigger handler” being set to “Active” in production until they’ve written up thorough documentation.
  3. Make sure admins are trained to read it before editing the contents of a TDTM record.

The “vs. shouldn’t” bit is important.

Developers should think ahead about:

  • “If this trigger handler were deactivated, but some other trigger handler, process flow, or workflow weren’t, what might go ‘wrong’ according to business-logic expectations?”
  • “If this trigger handler were set to asynchronous / unset from asynchronous, what might go ‘wrong’ with its execution?”
  • etc.

Admins and developers together should participate in a manual process of “playing with all of the fields on a Trigger Handler record to see what happens” part of the quality assurance testing processes in various scratch orgs and sandboxes (and documenting the results in a place that people are trained to read the docs).

Do this at the same time as other “actual dummy-checking the trigger with live data edits” QA work happens in sandboxes and scratch orgs.

Below is a bonus tip for EDA developers about testing and documentation when you’re in the process of writing unit tests.

Whereas normally maybe your unit tests might mock up TDTM records like this, using the 4-parameter TdtmToken constructor:

List<hed.TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();
tokens.add(new hed.TDTM_Global_API.TdtmToken('The_Apex_Handler_Class', 'The_Custom_Object__c', 'AfterInsert;AfterUpdate', 2.00));

Try using the longest available TdtmToken constructor instead:

List<hed.TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();
tokens.add(new hed.TDTM_Global_API.TdtmToken('The_Apex_Handler_Class', 'The_Custom_Object__c', 'AfterInsert;AfterUpdate', 2.00, TRUE, FALSE, NULL, NULL, FALSE, NULL, NULL));

For example, the Active and Asynchronous checkboxes are represented by constructor parameters 5 & 6.

Once you have your full codebase written, unit tests fully written & passing, etc … play with flipping around the parameter values in hed.TDTM_Global_API.TdtmToken(...) and run your tests again.

Wade through the tests runs’ stack trace logs, line-by-line code coverage, and System.assert() results.

You should be able to use this information to help you document, in plain English, what an admin might expect to see happen if they were to change various values on an actual record on the Trigger Handler database object.

Once you’ve finished playing with TdtmToken(...) parameters and documenting the results, you can put your unit test TdtmToken(...) invocations back to “normal” so that your tests pass, announce your code is “ready for manual testing” as usual, and move forward in the QA process like you normally would.

--- ---