I need some assistance, and greatly appreciate any help I can receive. I am trying to tag a lead with its most recent pre-mql campaign. The code I have does in fact work on insertion of a campaign member and on edit, but when deleting the campaign member the field still contains the original campaign and doesn't re-evaluate to pick up the next most recent campaign. Is there a way to get the system to do this within a trigger or would this involve doing something else?

Any guidance is greatly appreciated.

trigger UpdateRecentPreMQLCampaign on CampaignMember (After insert, After update) 
List<Id> cmIds = new List<Id>();
    List<Lead> leadsToUpdate = new List<Lead>();
    List<Id> cmLeadIds = new List<Id>();

    for(CampaignMember cm: Trigger.New)
    for(Lead l: [ SELECT Id, Recent_Pre_MQL_Campaign__c, (SELECT CampaignId FROM CampaignMembers WHERE Pre_MQL_Check__c = TRUE ORDER BY Pre_MQL_TimeStamp__c DESC LIMIT 1) FROM Lead WHERE Id IN (SELECT LeadId FROM CampaignMember WHERE Id IN :cmIds AND LeadId != NULL)])
        List<CampaignMember> cmListL = l.CampaignMembers;
        if(cmListL != null && cmListL.size() > 0) {
            l.Recent_Pre_MQL_Campaign__c = cmListL[0].CampaignId;

    update leadsToUpdate;

1 Answer 1

  • If you want a trigger to work on deletion of a record, the first thing is that you need to add either before delete or after delete to the list of events that your trigger fires for.

  • After that, you should realize that trigger.new is not available in delete triggers, so you'll have to add some logic to determine when to use trigger.new, and when to use trigger.old.

  • Instead of gathering CampaignMember ids and using a semi-join, you should probably just gather the LeadId from each CampaignMember and use that data directly.

Those changes should be sufficient to get your logic working on deletion, but I'd argue that you should go a bit further.

Best practice is to keep as much logic out of the trigger as possible (and only have one trigger per object). Trigger frameworks help with this, but you don't need to use a framework. At the very least, you would move your current trigger logic into an Apex class, and then call that class from your trigger.

// A basic, logic-less trigger
// The only thing it's concerned about is calling the appropriate class methods in a
//   specified order depending on which trigger event is happening
// A simple trigger framework would take this one step further, and have this code
//   also be in a separate Apex class (making it so that the trigger only calls into 
//   the framework)
trigger UpdateRecentPreMQLCampaign on CampaignMember (before insert, after insert, before update, after update, before delete, after delete, after undelete){
    // One of the other benefits of this approach is it takes on the responsibility for
    //   determining _which_ trigger context variable(s) to use
    switch on trigger.operationType{
        when TriggerOperation.AFTER_INSERT, TriggerOperation.AFTER_UPDATE {
        when TriggerOperation.AFTER_DELETE {

public class MyClass {
    public static myMethod(List<CampaignMember> givenCampaignMembers){
        // This can almost be a straight copy/paste of your existing trigger
        // You'd just need to replace instances of "Trigger.new" with "givenCampaignMembers"

One thing that people often overlook when handling rollups manually (and what you're doing is effectively a rollup summary) is that when you delete a child record, it's possible that was the last remaining child. Taking that into consideration usually means creating a map and setting a default value on the parent record before running the main query.

In practice, that would cause your logic to look something like this

Map<Id, Lead> leadsToUpdateMap = new Map<Id, Lead>();

// Gather lead ids and set the default value
for(CampaignMember member :givenCampaignMembers){
    // We only need an Id to be able to update an SObject
    // By setting this default value, any Leads that lose their last
    //   CampaignMember will be updated to have no recent campaign (as opposed
    //   to keeping the campaign of the final member)
    leadsToUpdateMap.put(member.LeadId, new Lead(
        Id = member.LeadId,
        Recent_Pre_MQL_Campaign__c = null

// This step may or may not be necessary, but I generally like to do it for
//   a little extra assurance

// Use the lead ids directly instead of bothering with a semi-join
for(Lead l: [SELECT Id, Recent_Pre_MQL_Campaign__c, (SELECT CampaignId FROM CampaignMembers WHERE Pre_MQL_Check__c = TRUE ORDER BY Pre_MQL_TimeStamp__c DESC LIMIT 1) FROM Lead WHERE Id IN :leadsToUpdateMap.keySet()]){
    // The safest way to access child records is to use a nested loop
    // I don't think that's necessary here because the maximum size of the
    //   embedded list of children here is 1
    // The embedded list should never be null, so no need for a null check (or even
    //   "?.", the safe navigation operator)

    // When there is a campaign to be assigned, this put() will override
    //   the previous (default) lead instance in the map
    // You could simply update the Lead instance referred to by the "l" loop variable
    //  here instead of creating a new Lead instance, but I like consistency
    leadsToUpdateMap.put(l.Id, new Lead(
        Id = l.Id,
        Recent_Pre_MQL_Campaign__c = l.CampaignMembers[0].CampaignId

update leadsToUpdateMap.values();
  • thank you so much, worked like a charm appreciate all the notes!
    – AS5900
    May 7, 2022 at 19:06

You must log in to answer this question.

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