Populate Campaign Members from Opportunities based on Opportunity Stage

Populate Campaign Members from Opportunities based on Opportunity Stage

Last Updated on April 15, 2022 by Rakesh Gupta

Guest Author: - Stephen Stanley

By default, when you create a new campaign, campaign member status values are limited to

  • Sent
  • Received

This limits how specific you can be when sending mass emails to a campaign. Unless you add new Campaign Member Status values. Since Spring’20 you have at least been able to clone a campaign (Clone with Related) with related and bring the Member Status values over from prior campaigns

If you want to email contacts associated with opportunities in bulk, then the easiest way to do this is:

  • Associate Opportunities with a Campaign
  • Send a list email from the campaign to the Opportunity Contacts you are interested in.

Unfortunately using Standard Salesforce, the only way you can segment the campaign members is based on the Campaign Member Status – which typically has been “Sent” or “Responded” and this only has an indirect relationship to the Opportunity Stage.  (It seems that when an opportunity is first associated with a campaign, the “Is Default” members status is assigned to the member and if the Opportunity is Won or Lost, the member status is set to the “Responded” Status – but I’ve not tested this sufficiently to be certain)

So, what if you want to send a different message to campaign members based on the stage in which the associated opportunity is in?  You can’t send a list email to the Opportunities on the Opportunities related list on the Campaign, and while you can choose campaign contacts one by one and send them templated emails to (or send emails en masse), you can’t see the stage of the opportunity when you are looking at the contact (i.e. Campaign Member).  You also can’t reliably add the Opportunity Stage to the campaign members related list with a formula field because there can be many opportunities related to a single contact.

So, if you want to send different templated emails to all opportunity contacts associated with a campaign, and select the contacts based on the stage that the opportunity is in, how do you go about it?

Salesforce Flow of course!

My particular use case is for tracking Training Course inquiries, bookings, sending pre-course reminders, and then after the event has run, sending completion certificates, exam results, and/or post attendance satisfaction surveys. Inquiries and Bookings are created as Opportunities and as they progress through the reservation and attendance lifecycle, the opportunity stage is updated to reflect the status.  Each Training Course that is delivered is a campaign (each separate delivery of the same course is a separate campaign) and all course deliveries of the same course type have the same parent campaign

I want to be able to select opportunities at one or more Opportunity stages and send a mass email to the contacts associated with those opportunities. To do this I have built a flow which:

  1. Is invoked from a campaign
  2. Looks up all the related opportunities and retrieves each different opportunity Stage name that is used by the related opportunities
  3. Presents the list of these values in the form of a series of checkboxes and also allows a “select all” option
  4. For the selected values, adds the stage names as Campaign Member Status values
  5. If any campaign members exist, delete them from the campaign
  6. Finally, add all the matching opportunity contacts to the campaign as campaign members, using the Opportunity Stage as the Campaign Member Status

The relevant parts of the data model are:

Walking through the key parts of the data model:


Opportunity Contact Role (not shown) is a Many-to-Many relationship, but the primary opportunity contact is a lookup from Opportunity to Contact and the ID of the contact is stored in the Opportunity. Contact field. This is the relationship we use to identify which contact to add as a campaign member.

From the field definition, the ContactID field on the Opportunity record is a bit unwieldy to work with and since I’m using the Non-Profit Success Pack, it is simpler to use the NPSP field “Primary Contact” but you could use the ContactID field instead.  See the field definition from the Salesforce documentation

Opportunity.ContactID - ID of the contact associated with this opportunity, set as the primary contact. Read-only field that is derived from the opportunity contact role, which is created at the same time the opportunity is created. This field can only be populated when it’s created, and can’t be updated. To update the value in this field, change the IsPrimary flag on the OpportunityContactRole associated with this opportunity.

The relationship between Campaign and Contact is via the CampaignMember junction object.  Each Campaign Member must have a member status and the status value must already exist before you can use it when adding a campaign member record.

The campaign has a lookup relationship to itself allowing you to manage campaign hierarchies

This Screen Flow is launched from a Lightning Action added to the Campaign Layout

Flow overview



Starting off the flow

This part of the flow handles passing the campaign record to the flow, retrieving any related opportunities, and handling the case where there are no related opportunities which have contacts.

Step 1

Create an Input variable named recordId. Be certain to copy this capitalization shown. This automatically receives the record ID of the record being viewed when you launch the flow


Step 2

Get Records.  Retrieve (any) all opportunities associated with this campaign Because we want to use this to create Campaign Members and send them emails, if there is no contact associated with the opportunity, exclude the opportunity.

This example uses the Non-Profit Starter Pack so it checks excludes opportunities where npsp__Primary_Contact__c is null.  If you are not using NPSP, but are using Campaign Influence with your opportunities, then use the ContactID field on opportunity instead in this filter condition, excluding Opportunity records where ContactID is null.

If you want to exclude opportunities where the primary contact’s email address is not known, then add a formula field to the opportunity to return the email of the opportunity contact and add a filter condition on the get records to exclude anywhere this formula field is blank.


When you retrieve the records, sort them by stage name as this makes later looping more efficient.


When storing retrieved records, you have to use the advanced option as you will be checking to see if the campaign has any associated opportunities and if there aren’t any, presenting an error screen. In order to handle the case where there are no returned records, remember to check the checkbox at the bottom of the screen.


Step 3

Having remembered to set the checkbox in step 2, you can now act on it.  Create a decision element


Building the collection of distinct Opportunity Stage Names


Step 1

Create a Loop Element which iterates over the collection of Opportunities you previously retrieved into {!RelatedOpportunities}.


Step 2

Because you can’t loop through a single field in a record collection, we need to build a collection of all the stage names which are in our collection of Opportunities.



Using an Assignment Element, just add the stage name from the opportunity record that is being presented in the allOpportunities loop Variable into a text collection.  When creating the text collection called {!allStageNames} remember to tell the flow that this is a collection of text variables by checking the box

Step 3


Feed your newly constructed collection of stage names into another loop in order to weed out any duplicates

Step 4

Loop through the collection of stage names concatenating each new one to a “control string” which we use to detect if we have seen this value before.  If we have seen if before, grab the next value from the collection. If we haven’t, then we concatenate the value to our control string and set the label values up.  On the way through, we check that we haven’t run out of label variables.  Hopefully, 10 distinct stage names will be adequate, but if 10 is not sufficient, then see the instructions at the end of this document to see what to do if you have more than 10

Add a Decision Element which checks if this value is new. If the Control String contains the value, we have seen it before, so grab the next item from the collection of stage names. If it is new, then we go the label switching decision element.


Step 5

This is where you use lots of copy/paste.  For each of your label variables, you need a decision outcome, plus one for where you have run out of label variables (in my case when you have more than 10 stage names)

Create outcomes with labels from 0-9 and have the outcome criteria be as shown. Outcome 0 matches to a {!labelCounter} value of 0, outcome 1 to a value of 1, etc, all the way to 9 (or your maximum number of stage names)

Have a final outcome “Too Many Stages” which means that you have more stages than your flow has been built to handle


Step 6

Create a Screen element to provide feedback to the user if you run out of stage variables.  I plan to update the text to link to this document so when the unlucky system admin is called up by the user, they can see what needs to be done to add more stage name variables.


Step 7


It’s at this stage that you are grateful to the Salesforce development team who thought of adding a copy button to the toolbox. It’s about to get a workout

You need 10 (or however many unique stage names you are anticipating) copies of this assignment element. In each copy, adjust the name of the variable to reflect the number of the counter that passes the flow to this assignment element.  In this case the labelCounter = 0.  When the labelCounter = 1 the variable you want to assign the loop variable to is {!chkLabel1} and so on up to the number of stage names you want to be able to handle.


Step 8

After each time you assign a new stage name to a label variable, you need to add the stage name to the Control String, so add a new assignment Element and increment your label counter as follows:


Presenting the list of possible stage name values to the user


This part of the flow presents a screen with a series of checkboxes, one per stage, and allows the user to select one or more of them.

There’s also a “Select All” checkbox, which if selected, makes all the individual checkboxes disappear.

This takes advantage of the fact that screen elements are self-referencing and their behaviors can be influenced by options a user selects when interacting with the Screen

Step 1


This screen is made up if several differently configured components:

Map the component to the number in the section below the screen to see how each of them is configured

  1. Select All. This is a simple checkbox
    1. Label: Select All
    2. API Name: chkAllSelected
  2. A text box – displayed when the user checks the “Select All” checkbox
    1. API Name: dlgAllSelected
    2. Component Visibility: All conditions are met: {!chkAllSelected} equals {!GlobalConstant.True}
  3. Another text box – only displayed when the “Select All” checkbox has not been selected
    1. API Name: dlgSelectStageNames
    2. Component Visibility: All conditions are met: {!chkAllSelected} equals {!GlobalConstant.False}
  4. A series of checkboxes with dynamic labels and visibility.  Set each of them up to reflect the label number as follows, substituting the “X” for the label number from 0-9. Include the braces and exclamation mark or it won’t be a dynamic label. These checkboxes disappear if “Select All” is checked by the user
    1. Label: {!chkLabelX}
    2. API Name: chkLabelXchk
    3. Component Visibility: All conditions are met: {!chkAllSelected} equals {!GlobalConstant.False}
  5. A warning dialog box which is only displayed after the first time through this loop – and then only if the user has clicked [Next] without selecting any checkboxes
    1. API Name: dlgRetry
    2. Component Visibility: All conditions are met: {!stagesSelected} equals {!GlobalConstant.False}

Step 2

An assignment element which checks to see if any checkbox has been selected. Looking at this now, this step might not have been necessary. In step 3 below you could probably replace {!StagesSelected} with {!StagesHaveBeenSelected} and remove this assignment step! I originally had this in place when I was treating select all and non-select all options differently


Step 3

A decision element that sends you back to the selection screen if none of the checkboxes have been selected or into a loop element if at least one checkbox has been selected. If you dispose of the previous step, then the Resource to be checked in the first comparison condition should be changed to {!StagesHaveBeenSelected}.


Step 4

Create A Loop Element


We now run through the opportunities we retrieved right at the start of the flow and if the stage name matches one of the selected values add the contact details related to the opportunity into a collection of Contacts. If Select All has been checked, then all opportunity contacts are added to the collection.

Step 5

Create a Decision Element


This decision checks to see if the stage name of this opportunity is one of the ones that have been selected.

The clever bit of this is the two formulae “allSelectedStageNames” and “stageIsSelected“.

  1. allSelectedStageNamesPicture27
  2. stageIsSelectedPicture28

Step 6

A formula and two assignment elements

  1. The newline FormulaPicture29
  2. Build the output collection of Campaign Members and text variables used later to confirm that the flow will do what the user is hoping it will. Note the use of the Formula {!newline} which adds a carriage return between items we are displaying on the screen.Picture30
  3. Add the Campaign Member you just created to a Campaign Member record collection variable and nullify the holding record variablePicture31

Step 7

The confirmation screen is displayed before any changes are made.  This is the only screen element that exposes the [Previous] navigation element. If the user selected [Previous] then they go back to the selection screen which allows them to change the selected stages. The top section of the screen displays the string you built in the previous step which lists out all the opportunities which will be copied to Campaign Members


Create The New Member Status Values for the Campaign


Step 1

Retrieve the existing member status values into a collection. You can use the default retrieval process here (no need for advanced) as Campaigns must always have at least one Member Status value, so you don’t need to worry about checking for no records returned.


Step 2

Pass the returned records into a loop

Use an assignment Element to concatenate the Member Status name onto the end of the Text String.


Step 3

Loop through the Opportunity Stage Names and build a collection of Campaign Member Status records where they don’t already exist.



If your string that was concatenated in step 2 contains the current stage name, go to the next record, otherwise, create a Campaign Member Status holding record and add it to a collection.

The holding record:


Adding the holding record to the Campaign Member Status record collection.

Step 4

When you exit the loop,  save the new Campaign Member Status values to the database. If you want to be absolutely sure that you won’t encounter unexpected errors, you may want to check to see if there are any records in the collNewMemberStatusValues to save before executing the Create Records Element. You may want to skip this element if there are no records in the collection


Save your new Campaign Members to your Campaign


Step 1

Retrieve any existing Campaign Members for this Campaign

Use the Advanced mode so you can detect if no records are returned.

As you will only be using this collection to delete records, you only need the Campaign Member ID to be retrieved


Step 2

A Decision Element which directs the flow to a deleted element or bypasses it if there are no existing Campaign Members.


Step 3

If there are existing Campaign Members, delete them


Step 4

Finally, create the new Campaign Member records from your collection


Variable Definitions

Type Name Type Other
Collection Variable allStageNames Text Allow multiple values = TRUE
Collection Variable uniqueStagenames Text Allow multiple values = TRUE
Formula allSelectedStageNames Text “” & IF({!chkLabel0chk},”,”&{!chkLabel0},””)& IF({!chkLabel1chk},”,”&{!chklabel1},””)& IF({!chkLabel2chk},”,”&{!chkLabel2},””)& IF({!chkLabel3chk},”,”&{!chkLabel3},””)& IF({!chkLabel4chk},”,”&{!chkLabel4},””)& IF({!chkLabel5chk},”,”&{!chklabel5},””)& IF({!chkLabel6chk},”,”&{!chkLabel6},””)& IF({!chkLabel7chk},”,”&{!chkLabel7},””)& IF({!chkLabel8chk},”,”&{!chkLabel8},””)& IF({!chkLabel9chk},”,”&{!chklabel9},””)
Formula newLine Text BR()
Formula stageIsSelected Boolean Contains({!allSelectedStageNames},TEXT({!forOppsToCopy.StageName}))
Formula StagesHaveBeenSelected Boolean {!chkLabel0chk} || {!chkLabel1chk} || {!chkLabel2chk} || {!chkLabel3chk} || {!chkLabel4chk} || {!chkLabel5chk} || {!chkLabel6chk} || {!chkLabel7chk} || {!chkLabel8chk} || {!chkLabel9chk}
Record Variable holdingCampaignMember Campaign Member Allow multiple values = FALSE
Record Variable holdingMemberStatus Campaign Member Status Allow multiple values = FALSE
Record Collection Variable collCampaignMember Campaign Member Allow multiple values = TRUE
Record Collection Variable collNewMemberStatusValues Campaign Member Status Allow multiple values = TRUE
Record Collection Variable existingCampaignMembers Campaign Member Allow multiple values = TRUE
Record Collection Variable relatedOpportunities Opportunity Allow multiple values = TRUE
(10) Variables chkLabel0..chkLabel9 Text Create 10 text variables, chkLabel0 through to chkLabel9
Variable concatMemberStatuses Text Allow multiple values = FALSE
Variable labelCounter Number 0 decimal places, Default value = 0. Allow multiple values = FALSE
Variable recordId Text Available for Input, Allow Multiple Values = FALSE. NB Exact capitalisation is required as per the variable name
Variable StagesSelected Boolean Allow multiple values = FALSE
Variable strSelectedRecordsToCopy Text Allow multiple values = FALSE


Variables that need to be added/adjusted and changes that need to be made if you have more than 10 opportunity stage names on opportunities associated with a campaign that you feed to the flow.  

If you try to run the flow with related opportunities that have more than 10 stages, you will be presented with a screen telling you so and the flow will halt.

Item Item Type Purpose Modifications needed if adding an Xth stage name
allSelectedStageNames Text Formula A formula that is made up of all selected stages concatenated together. Used to check to see if a stage name is in those which have been selected Add this text to the end of the formula
& IF({!chkLabelXchk},”,”&{!chklabelX},””)
StagesHaveBeenSelected Boolean Formula Returns TRUE if one or more stages have been selected on the stage name selection screen.  If it is FALSE, you can’t proceed past the stage selection screen Add this text to the end of the formula
|| {!chkLabelXchk}
chkLabelXchk Checkbox Screen component Dynamically displayed and labeled checkbox on the stage name selection screen (scrSelectStages). Label names are set based on opportunity stage names retrieved and displayed only if there are enough different values to mean that they need to be displayed Add a checkbox screen component to the scrSelectStages screen element with the following characteristics:
Label: {!chklabelX}
API Name: chkLabelXchk
Component Visibility: all conditions are met: {!chkAllSelected} Equals {!$GlobalConstant.False}
chkLabelX Text Variable Contains the Xth opportunity Stage name Create a new Text Variable named chkLabelX
labelCounterSwitcher Decision Element Based on how many stage names have been retrieved so far, switches the flow to the correct assignment element to build the next stage name variable Add another outcome.  i.e. for an Xth stage name, add:
Label: X
When all conditions are met
{!labelCounter} = X
Assignment X Assignment Element Assigns the stage name from the opportunities loop variable to the label to allow it to be selected and queried Create the following assignment:
Variable: {!chklabelX}
Operator: Equals
Value: {!forAllStageNames}

Great! You are done! Feel free to modify it based on your business requirement. You can install the package by using this URL

Formative Assessment:

I want to hear from you!  

What is one thing you learned from this post? How do you envision applying this new knowledge in the real world? 

Let me know by Tweeting me at @automationchamp, or find me on LinkedIn.

Have feedback, suggestions for posts, or need more information about Salesforce online training offered by me? Say hello, and leave a message!

Preferred Timing(required)

2 thoughts on “Populate Campaign Members from Opportunities based on Opportunity Stage

  1. Nice flow, Its hard to grasp for me, being a learner. looks too complicated, I will come back again to implement when I know better. Thanks for sharing.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.