EDA and NPSP come with Table-Driven Trigger Management (TDTM), a trigger framework that uses records in a custom object to track which "trigger handler" Apex classes are active, rather than, say, using records in Custom Metadata. (I think it just precedes custom metadata or something.)

I've been enjoying CumulusCI as a way of managing my attempt to make clean, modular GitHub repositories for "clumps" of business logic, but one thing that's annoying is having to put steps into the repo's README.md file instructing admins that if they'd like to actually see how well the triggers in my codebase work, they have to actually bother to add some trigger handler records to that data table.

Is there a best-practice way, using CCI, to ensure that any time I spin up a new scratch org from the repo using cci flow run dev_org --org ScratchOrgAliasName or any time I install the repo into a persistent sandbox using a command like cci task run dx_push --org PersistentSandboxAliasName, data records get pushed into the "Trigger Handlers" table if ones like them don't already exist?

I see https://cumulusci.readthedocs.io/en/latest/data.html but it seems really involved (postgreSQL databases and such), so I'd like to make sure it's actually what I need for a task this small before diving in too deep on so complex an approach.

Update: I realized maybe EDA itself was using the advice at https://cumulusci.readthedocs.io/en/latest/data.html and did some copypasta from it and another open-source community commons package, and realized that the whole "SQL" thing isn't so bad -- you don't, like, actually have to set up and run a database or anything.

So far, I've been able to get cci task run load_dataset -o mapping datasets/mapping.yml -o sql_path datasets/data.sql --org ScratchOrgAliasName to work just by having a .yml and a .sql file in my repo:


Upsert hed__Trigger_Handler__c:
    sf_object: hed__Trigger_Handler__c
    table: hed__Trigger_Handler__c
        Name: Name
        hed__Active__c: hed__Active__c
        hed__Asynchronous__c: hed__Asynchronous__c
        hed__Class__c: hed__Class__c
        hed__Filter_Field__c: hed__Filter_Field__c
        hed__Filter_Value__c: hed__Filter_Value__c
        hed__Load_Order__c: hed__Load_Order__c
        hed__Object__c: hed__Object__c
        hed__Owned_by_Namespace__c: hed__Owned_by_Namespace__c
        hed__Trigger_Action__c: hed__Trigger_Action__c
        hed__User_Managed__c: hed__User_Managed__c
        hed__Usernames_to_Exclude__c: hed__Usernames_to_Exclude__c



CREATE TABLE "hed__Trigger_Handler__c" (
    "Name" VARCHAR(255),
    "hed__Active__c" VARCHAR(255),
    "hed__Asynchronous__c" VARCHAR(255),
    "hed__Class__c" VARCHAR(255),
    "hed__Filter_Field__c" VARCHAR(255),
    "hed__Filter_Value__c" VARCHAR(255),
    "hed__Load_Order__c" VARCHAR(255),
    "hed__Object__c" VARCHAR(255),
    "hed__Owned_by_Namespace__c" VARCHAR(255),
    "hed__Trigger_Action__c" VARCHAR(255),
    "hed__User_Managed__c" VARCHAR(255),
    "hed__Usernames_to_Exclude__c" VARCHAR(255),
    PRIMARY KEY (Name)



The only catch is if I run it twice, it inserts another record, so still some fiddling to do, I guess.

1 Answer 1


There are at least four ways to do this.

Dataset Tooling

You can persist (as you discuss in your question) your TDTM trigger handler records as part of a CumulusCI dataset, optionally alongside other data that you wish to load as part of an automation flow.

This strategy requires a minimal setup (as you observe, you don't need to run a database; CumulusCI handles all of the load and extract for you). However, it is not, and is not intended to be, idempotent. If you run a dataset load multiple times, you get multiple records.

Insert Record

The insert_record task allows you to insert exactly one record of any sObject, e.g.,

task: insert_record
    sobject: hed__Trigger_Handler__c
        hed__Active__c: True
        # ... and so on ...

This task is also not idempotent.

Anonymous Apex

You can always just run an Anonymous Apex script. This is currently the only way to achieve idempotency on data record creation.

task: execute_anon
    apex: |
        // Your Apex here.
        // Query for your TDTM Handler record, and create if nonexistent.

Python Script

Finally, you can write a Python task class that does more or less the same thing as the proposed Anonymous Apex: query for a TDTM Handler class and create it if not present. If you're interested, this could be nicely generalizable - it could take flexible options for the handler content - and would be a great contribution to CumulusCI itself, if you're so interested.

If that's something you're interested in pursuing, please feel free to discuss further on our Trailblazer Community group or by opening a feature request issue in GitHub; we'd be happy to offer guidance and feedback.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .