Salesforce In-App Surveys by Shashank and Ritik

<This blog has been written by Shashank Lagishetty and Ritik Aggarwal>

Use the Salesforce Feedback Management (SFM) Response APIs to build in-app surveys. Users can respond to these surveys contextually instead of getting redirected to another screen or application. With these APIs, you can build a user-friendly survey experience that matches your company’s theme and branding.

Importance of Customer Satisfaction Surveys

The success of any company depends on their customers’ satisfaction. Companies gather continuous feedback from their customers through surveys, and then try to meet their expectations by addressing the feedback. If companies notice a positive change for their efforts to improve customer satisfaction, they start investing more to improve the customer experience further. If there’s no positive change for their effort, they make alternative plans. So, gathering customer feedback becomes an important part of any company’s growth lifecycle.

Low Response Rate for Surveys

Studies show that the general response rate for online surveys is in the range of 5% – 30%. Achieving a good survey response rate is important to avoid sampling bias. Also, companies may not be able to draw any meaningful conclusions if the survey response rate is low. Some of the reasons for users not completing surveys are:

  • Application switching: Users have to go to a different application to respond to the survey.
  • Bad survey experience: The survey either doesn’t match the company’s branding or the content isn’t displayed properly in certain devices and browsers.

To increase the response rates of surveys, especially when there’s no major incentive for users in completing them, the responding process must be as user-friendly as possible.

Increase Survey Response Rates with SFM APIs

Salesforce Feedback Management provides REST APIs that you can use to build a custom runtime experience with minimal effort. 

In this blog post, we describe how to use Lightning Web Components (LWCs), a Salesforce recommended framework, through which users can respond to any Feedback Management survey. We will also provide a sample implementation, which can be uploaded to a Salesforce org and used after adding the org’s details into them. After this setup is completed, you can embed LWCs in either the Salesforce Lightning pages or Experience Cloud sites, or both.

  • Salesforce Lightning Pages: If you want internal users to respond in the Salesforce UI itself, embed the components in the homepage or record pages etc.
  • Experience Cloud sites: If you want to focus on Experience cloud users and provide a flexible runtime experience, embed the components in Experience Cloud sites.

You can also build custom iOS or Android components for mobile applications using the Salesforce mobile SDK.

Feature Details

FeatureDetailsLearning Resources
SFM Response APIsSFM Response APIs are available only with the Feedback Management Starter or Growth licenses.Feedback Management LicensesSurvey Invitations Using Invitation Configurations (POST, PATCH)Surveys Invitations Using Invitation ID (POST, PATCH)
Custom Lightning Web ComponentsDevelop the required LWC for each type of question and deploy them in a Salesforce org.Lightning Web Components Basics
Apex classesCreate an Apex class and integrate with LWCs.Apex Basics for Admins Call Apex Methods
Named CredentialsDue to security constraints, sessions created by Lightning components or Apex classes aren’t enabled for API access. So, admins must create a named credential with a callout endpoint using their credentials and then use it in the Apex class REST calls to call the SFM APIs.Invoking Callouts Using Apex

Build a Sample In-App Survey

Let’s use an example to understand the process of building an in-app survey. 

A company, Dream House, wants to get employee feedback on their new hire onboarding process. The Salesforce admin of Dream House enables their employees to answer the feedback survey from Salesforce 

The Salesforce admin of Dream House must perform the following steps to build an in-app survey:

  1. Create a basic survey in the Survey Builder with the following questions:
    1. Rating question: How do you rate the onboarding process?
    2. Short text question: Do you suggest this process to your colleagues?
    3. Long text question: Describe your experience in a few words.
  2. Display survey runtime using custom or Salesforce provided sample LWCs on the Salesforce app homepage or anywhere else to display questions. These components are used to show the survey and get the survey responses from the users.
  3. In the initialization of these LWCs, the Apex class method must be invoked which creates a survey invitation using the API POST call.
  4. The Apex class parses the API response and returns details of the survey and survey questions to the component after the POST call is successful.
  5. Using these survey details, you can populate questions on the application UI as per the requirements. 
  6. With the content of the question populated, users can answer the survey questions on the UI and send their responses to the Apex class which in turn calls API PATCH call.
  7. The feedback is submitted after the final PATCH API response is successful.

Create a Survey Invitations for Users

Creating survey invitations is the first step in getting feedback from the user. 

  • The Apex class calls the API endpoint with the required payload to create the survey invitation. 
  • The POST call works as a describe API and returns details about the survey and its questions. 


Developer Guide: POST API

Endpoint: /services/data/v56.0/connect/surveys/<surveyId>/survey-response

Note: Retrieve surveyId from the Survey object for the corresponding survey needed to be shown.

Payload Structure


  “languageCode”:  “<If you have provided translations and want to take the survey in a specific language, then pass the locale code. Ex: “en”/”de”>”,


      “allowGuestUserResponse”: <true/false based on authentication required or not>,

      “allowParticipantsAccessTheirResponse”: <true/false based on if participants can see their responses>,

      “collectAnonymousResponse”: <true/false based on if the responses should be anonymous>,

      “invitationExpirationDate”:”<Date until when invitation can be valid. Ex: 2023-07-31T21:32:54>“,

      “communityId”:”<Id of community if allowGuestUserResponse is made true (Optional)>“,

      “invitationOwner”:”<Owner of the invitation (Optional)>





   “errors”:[], // Errors observed in the API call

    “flowInterviewState”:”<This will be a encoded string and needs to be passed to the PATCH call>“,

   “invitationId”:”<Unique id and needs to be passed to PATCH call>“,

   “status”:”<Success/Failed. Status of this API call>“,


      “label”:”<Label of survey>“, 

      “name”:”<Internal name of survey>“,


         “label”:”<Internal label of page>“, 

         “name”:”<Internal name of the page>“,

         “surveyQuestions”:[ // List of questions


               “description”: “<Description of the question>”,

               “isResponseRequired”: <true/false based on if response is required>,

               “label”:”<Content of the question>“,

               “name”:”<Internal name of the question, needs to be passed to PATCH call>“,

               “questionType”:”<Type of the question>“,

               “responseDataType”:”<Object type of response>“,

               “validationFormula”: <Formula if any custom validation is added>




      “versionLabel”:”<Survey name>“, 

      “versionNumber”: <Survey version>


   “warnings”:[] // Warnings observed in the API call


Populate Survey Questions on the Salesforce UI

Get the details of all the survey questions and show them on the Salesforce UI using the response from the POST call. You can have a submit button or any other trigger to call the Apex class again for submitting the responses. Ensure to pass the user provided answers to the Apex class so that they can be persisted.

We recommend you to have different LWCs for different question types to style question types according to the company’s standard branding and styling.

Persist Survey Responses

After the front end passes the user responses, the Apex class can call the PATCH endpoint to persist the survey responses. This API returns whether the call is successful and the Thank you message that’s shown to users at the end of the survey.


Developer Guide: PATCH API

Endpoint: /services/data/v56.0/connect/surveys/<surveyId>/invitation/<invitationId>/survey-response’

Note: invitationId is retrieved from the earlier POST API call.

Payload structure


 “invitationId”: “<Unique invitationId from post API response>“,

 “languageCode”: “<If you have provided translations and want to take the survey in a specific language, then pass the locale code. Ex: “en”/”de”>”,

 “surveyPageResponses” : { 

    “questionResponses” : [ // List of questions with their responses


                “name”: “<Internal name of the question from post API response>“,

                “questionType” : “<type of the question from post API response>“,

                “responseValue” : “<User given answer>




  “flowInterviewState” : “<flowState from post API response>





   “status”:”<Success/Failed. Status of the API call>“,





       “redirectUrl”: “<URL of the page to be redirected>”,

       “thankYouMessage”:”<Thank you message to be displayed>“,

       “urlButtons”:[ // List of URL text and URL for buttons to be displayed




Final Output

After all the LWCs are built and integrated, the survey is shown on the Salesforce UI and the Experience Cloud site.

Survey Builder

The survey in the Survey Builder

Salesforce UI

The survey runtime on the Salesforce UI

Experience Cloud Site

The survey runtime on the Experience Cloud site

Sample Implementation

A sample implementation of the LWCs and the Apex class can be found below. After the package is installed and configured in a Salesforce org, you can embed the SurveyContainer LWCs in the layouts to use the custom survey runtime.

Note: In the configured package, a valid SurveyId must be specified and a valid namedCredentials must be referenced in the Apex class.

Lightning Web ComponentDescription
SurveyContainerInteracts with the Apex class and displays the survey contents.
SurveyQuestionInvokes the question-specific component.
surveyBooleanQuestionShow the boolean type of questions
surveyShortTextQuestionShow the short text type of questions.
surveySingleSelectQuestionShow the single selection type of questions
surveyMultiSelectQuestion/surveyNpsQuestion/surveyRatingQuestion/surveyFreeTextQuestionShows other supported question types.

The SurveyApi Apex class holds the values for surveyId/namedCredentials and makes SFM API POST/PATCH calls. 

Benefits of In-App Surveys

Some of the benefits of in-app surveys are:

  1. Application switching isn’t required.
  2. Users are more likely to respond to surveys and with higher and faster response times.
  3. Users have more flexibility in designing the user experience of survey runtime. It can be made to sync with the application’s branding and theme.
  4. There’s no additional training required for the users to respond.
  5. Currently, the SFM runtime isn’t supported in LWR experience sites because it’s primarily an Aura-based application. With these APIs and sample implementation, you can enable support for Surveys in LWR experience sites.

Scope and Limitations

To be able to call the SFM APIs from Apex, named credentials must be created. It’s not feasible for each user to create a named credential before responding to the survey. So, the admin’s credentials are used to create the named credentials that’s used in the Apex class and all the responses are submitted using these credentials. 

Due to this limitation as of Winter ‘23, all the survey responses are submitted by the admin user because of the named credentials used. In the upcoming releases, we are planning to improve this experience. Stay tuned about the improvements we make by going through the release notes each release.


The frontend of a survey runtime experience is one of the essential aspects for any small business, a medium-scale organization, or an enterprise. If it isn’t seamless, there’s a high probability of users not attempting or completing the survey. With in-app surveys, the survey runtime experience can be more user-friendly. In-app surveys can bring a huge difference in usability and enhance overall customer engagement.

Word Cloud for Salesforce Feedback Management by Bharadwaj Kopparthi, Chocks Kandasamy, Navteshwara Kaur & Nimesh Gupta

From Cameron Farrier-Mcgoldrick: Suppress Survey Invitations based on Criteria


Salesforce Feedback Management provides ‘Survey Invitation Rules’ feature OOTB which lets admins define rules and conditions under which survey invitations can be sent.  

This is equivalent to configuring a survey trigger based on triggering conditions.  Here is an example where a survey invitation is sent out to capture additional information everytime a case is created or updated.  

Problem Statement

What if there are additional requirements to control the frequency of the survey invitation to be sent.  A frequent use case we keep hearing is “How can I suppress survey invitations if there was already an invitation sent in the last 2 days?”

How do we solve such use cases?

The limitations with survey invitations feature is we cannot add advanced filtering conditions. To overcome this the recommendation is NOT to use “Survey Invitation Rules”, rather use custom Flows.

Solution Approach

The approach we will take will include us creating a custom flow and an additional check to validate if there was an invitation that was sent in the last 2 days to the recipient related to this Survey, before sending out the invitation.

Creating the Survey Invitation Flow

Choosing Your Trigger

Just like with the Invitations Rules, we first have to specify what will trigger our custom ‘Send Survey’ Flow. In this example we will attempt to send a CSAT survey when a Case status is changed to closed.

For this we will use the Record Triggered Flow type, and set the triggering object to Case. We will also set the Flow to start only when Case is updated.

We also need to set the entry criteria, which ensures surveys are only sent to Cases that are equally closed. This is where we can add additional criteria, such as not sending surveys to critical Cases.

You should also select the following options:

  1. When to Run the Flow > Every time a record is updated and meets the condition requirements
  2. Optimize the Flow for > Actions and Related Record 

Suppressing a Survey

The entry criteria is our first initial method of suppressing survey invitations. But what if we want to take this to a more advanced level? To suppress surveys where the recipient has already received one in the last 2 days, we can check if any Survey Invitation records exist.

Creating Date Resource

The first step is to create a resource to store the date two days before today’s date. You can do this by clicking ‘New Resource’ on the left panel and entering the following information.

Creating ContactId Resource

We also need to do the same for storing the contact/survey participant ID. However this time make sure you select Variable as the resource type and Text as the data type (You could also do the same for a Lead or User).

Getting Previous Survey Invitations

The second step is to get all survey invitations that the current Contact has viewed in the last two days. If no records are returned then it means we can go ahead and send a new survey invitation.

Add a new ‘Get Records’ element onto the canvas and add the following information as shown below:

  1. Object: Survey Invitation
  2. Filter Conditions:
    1. Survey Invitation ContactId = ContactId (the resource we created previously for the Case Contact)
    2. LastViewedDate = TwoDaysAgo (the data resource we created previously)

Tip: Instead of filtering by ‘LastViewedDate’. You could check if they actually completed a survey by filtering using the ‘LastModifiedDate’ and ‘ResponseStatus’ (NOT Equal to Completed) fields.

You also need to add the following configurations to store a Null value when no records are returned. This will be used in the next step.

Checking if a Survey Was Viewed

Finally we actually need to check if any records were returned and what to do for each of these scenarios.

Add a new ‘Decision’ element to the canvas and create the following two ‘Outcomes’.

  1. “Send Survey”:
    1. Record Collection = the record collection you just created in the last step
    2. Operator = Equals
    3. Value = False
  2. “Suppress Survey”:
    1. Record Collection = the record collection you just created in the last step
    2. Operator = Equals
    3. Value = True

Sending a Survey

At this point you can decide how you would like your survey to be distributed if it’s allowed to be sent. A few options are:

  1. Creating an invitation record and sending via Marketing Cloud.
  2. Creating an invitation record and sending to digital channels using the Digital Engagement add-on for Service Cloud.
  3. Send using the Email Survey Flow Action.

For simplicity let’s use the third example. Add a new ‘Flow Action’ element to the screen and search for the survey you want to send (make sure you update the default email settings in the survey builder for the survey you want to send). Then follow the steps in this article using the contactId variable as the ‘Recipient Type’.

What Can I Do After Suppressing a Survey?

If a flow has been suppressed we have a few options:

  1. Do nothing and end the Flow
  2. Update a field on the Contact or Related Record (Case) for reporting purposes
  3. Create a Survey Invitation record to be scheduled at a later date
  4. Check if the last survey was complete and send a reminder if it wasn’t

Finished Flow

Useful links

From: Shantinath Patil: Review Your Resource

In Spring 22, the Salesforce scheduler has given a new feature: enabling rendering HTML tags in Service Resource cards. This new feature has opened up many possibilities to show additional information about the resource.

This help guide lets you add fields to the Service Resource cards:

Peer reviews and ratings are common decision making guides for customers today to help them pick among

Imagine you were to add the capabilities of a feedback management software (eg: Salesforce Feedback management) to Salesforce Scheduler, you could ask your customers to provide ratings for their appointments and then use those ratings to guide future customers.

With Feedback Management you can not only use survey invitation rules to distribute surveys when an appointment is closed but also use the data mapper feature to easily map the rating from the survey to any object in Salesforce, in this case to the Service Appointment object.

Refer to this link for more details on the datamapper and survey invitation rules

Once you have captured the rating data for your resources, there are several possibilities

  • Help the consumer decide between resources
  • Help Territory Mgr evaluate resources & branch
  • Give preferential treatment to premium customers by only showing them higher rated resources
  • Use Salesforce Einstein’s Machine Learning capabilities to predict who would be the best resource for this prospect based on past ratings giving by similar prospects eg: Someone from the South might prefer a resource from the south, or someone from an age group would prefer certain Resources.

For this blog, we’re going to focus on the Salesforce Scheduler changes you need to make to get the above capabilities going.

let’s take an example of adding a rating to the service resource. Please follow the below steps to show the shining rating stars on the resource card:

1. Fields on Service Resource

To show the rating, we need to create a formula field on Service Resource to render rating stars in cards. We will add this field to the compact layout to show up in the out of the box component. However, to populate value in this field, we need two more fields on Service Resource: to capture the average rating for each of the appointments he served and the number of appointments.

Field LabelField API NameData TypeComments
RatingRating__cNumber(1,0)Number field to store rating between 1-5
Review CountReview_Count__cNumber(18,0)Number field to store number of reviews this service resource have received
Star RatingStar_Rating__cFormula(Text)Formula field which will show ratings based on above 2 fields

The sample formula will turn out like this:

IF(TEXT( CEILING(Rating__c) ) = '0', "/img/samples/stars_000.gif",
IF(TEXT( CEILING(Rating__c) ) = '1', "/img/samples/stars_100.gif",
IF(TEXT( CEILING(Rating__c) ) = '2', "/img/samples/stars_200.gif",
IF(TEXT( CEILING(Rating__c) ) = '3', "/img/samples/stars_300.gif",
IF(TEXT( CEILING(Rating__c) ) = '4', "/img/samples/stars_400.gif",
IF(TEXT( CEILING(Rating__c) ) = '5', "/img/samples/stars_500.gif",
'Rating Level'
) + ' ('+ TEXT( Review_Count__c ) + ' Reviews)'

2. Field on Service Appointment

We will have to create a field on Service Appointment to capture the sentiment of a customer based on how the appointment went. We can populate this value from a Survey, which will be sent out once an appointment is complete.

Field LabelField API NameData TypeComments
RatingRating__cNumber(1,0)Number field to store rating between 1-5

3. Propagate value from Service Appointment to Service Resource

Now that we have fields on Service Resource and Service Appointment, we will have to process the aggregate reviews. As shown in the below data model diagram, we can traverse through Assigned Resource and populate this value to Service Resource.

The review aggregator formula that we can use is:

 ((Current Average Rating * Total Reviews) + New Rating) / (Total Reviews+ 1)

Using this formula, we can write a trigger on Service Appointment. As soon as we get a rating from Service Appointment, we will aggregate the review and update its corresponding Service Resource. A sample trigger code is given below:

trigger UpdateSurveyRating on ServiceAppointment (after insert, after update) {

Set<Id> vSetServiceAppointment = new Set<Id>();

for(ServiceAppointment sapp :{
if(Trigger.isInsert && sapp.Rating__c != null){
}else if(Trigger.isUpdate && Trigger.oldMap.get(sapp.Id).Rating__c != sapp.Rating__c){


Map<Id, List<ServiceResource>> vMapServiceAppToResources = new Map<Id, List<ServiceResource>>();
List<AssignedResource> vListAssResource = [SELECT Id, ServiceResourceId, ServiceAppointmentId FROM AssignedResource WHERE ServiceAppointmentId IN: vSetServiceAppointment];

if(vListAssResource != NULL && !vListAssResource.isEmpty()){

Set<Id> vSetServiceResource = new Set<Id>();

for(AssignedResource vAssRes : vListAssResource){

Map<Id, ServiceResource> vMapServiceResource = new Map<Id, ServiceResource>([SELECT Id, Rating__c, Review_Count__c FROM ServiceResource WHERE Id IN: vSetServiceResource]);
for(AssignedResource vAssRes : vListAssResource){
if(!vMapServiceAppToResources.keySet().isEmpty() && vMapServiceAppToResources.containsKey(vAssRes.ServiceAppointmentId)){
List<ServiceResource> vTempResources = vMapServiceAppToResources.get(vAssRes.ServiceAppointmentId);
vMapServiceAppToResources.put(vAssRes.ServiceAppointmentId, vTempResources);
List<ServiceResource> vTempResources = new List<ServiceResource>();
vMapServiceAppToResources.put(vAssRes.ServiceAppointmentId, vTempResources);

//((Current Average Rating * Total Reviews) + New Rating) / (Total Reviews+ 1)
Map<Id, ServiceResource> vMapServiceResourceToUpdate = new Map<Id, ServiceResource>();
for(ServiceAppointment sapp :{
for(ServiceResource vResource : vMapServiceAppToResources.get(sapp.Id)){
if(vMapServiceResourceToUpdate != NULL && vMapServiceResourceToUpdate.containsKey(vResource.Id)){
ServiceResource vTempResource = vMapServiceResourceToUpdate.get(vResource.Id);
vTempResource.Rating__c = (((vTempResource.Rating__c * vTempResource.Review_Count__c) + sapp.Rating__c) / vTempResource.Review_Count__c+1);
vMapServiceResourceToUpdate.put(vTempResource.Id, vTempResource);
Decimal rating = vResource.Rating__c != NULL ? vResource.Rating__c : 0;
Decimal reviews = vResource.Review_Count__c != NULL ? vResource.Review_Count__c : 0;
vResource.Rating__c = (((rating * reviews) + sapp.Rating__c) / (reviews+1));
vResource.Review_Count__c = ++reviews;
vMapServiceResourceToUpdate.put(vResource.Id, vResource);

UPDATE vMapServiceResourceToUpdate.values();

Note: This is just a concept illustrator built for a simple create Guest appointment use case using the out of the box flow template.


How I Built This: Drive Your Survey Automations with Flow – Part 2

The Use Case

The customer wanted to update several fields on the related Case that tracked whether the person responded to the latest survey, and then update the Case status appropriately based on a specific question’s response. 

They also wanted to send an in-app alert to a Queue if the person responded extremely negatively to any of the numerical questions being asked in the Survey. So if they responded with a 0, 1, or 2 to the CSAT question, they wanted to know. 

Using Custom Metadata / Custom Settings to Automate Specific Questions.

In the previous article we talked about the Survey data model and a sample scenario with one question and 3 choices. We’ll use that in this new flow. Remember how I said it’s important to understand the Survey data model? That was no joke – you will likely be lost if you don’t take time to understand it. 

Just a caveat – this solution assumes you do not have Feedback Management, which has the super helpful Data Mapper solution which lets you do some of what Flow does, but without the Custom Metadata complexities introduced here. Feedback Management and Flow can live in tandem, however. If you need basic automations done in your system, Feedback Management is a great way to do that… However, if you need to do things like sending an in-app alert to a queue, or re-use automations across Surveys you’ll need some help from Flow.

I will say generically that if you have the same automation that needs to run for every survey in your system or is shared across many surveys, I recommend a Flow-first approach. This allows you to make changes to your automation in one place vs. doing it on a per-survey basis. Again, though it may be worth sacrificing 

Flow Entry / Early Conditions

I won’t go element by element unless demand arises, but I do want to call out a few checks you can make.

In the rare scenario where a Survey isn’t complete, we likely want to check that the survey is actually completed:

Next up we check that the Question Responses’s survey name matches what we’re expecting. That can be done by traversing upward: $Record.SurveyVersion.Name (or $Record.SurveyVersion.Survey.Name). We can’t do that in our entry criteria (yet), so we have to do it here in a decision node.

In my example below I actually have a custom lookup on Survey Invitation called ‘Related Case’, and I plan to update some fields on the Case, so I ensure there is actually a value there. That can be done by traversing upward as well: $Record.SurveyInvitation.Related_Case__c

Set Case Fields Based on the Survey Response

Next up is where I actually set some values on the related case to track the survey completion at the Case leve (not the Invitation/Response level). These fields on Case track that an evaluation was completed and when it was completed.

Notice I don’t do an Update here – I use an assignment to save on DML as I will likely be making more changes to the case downstream in the Flow. Efficiency!

{!recordCaseToUpdate.Id} Equals {!$Record.Invitation.Related_Case__c}

{!recordCaseToUpdate.Date_Evaluation_Complete__c}  Equals {!$Flow.CurrentDate}

{!recordCaseToUpdate.Evaluation_Status__c} Equals Complete

Set Case Fields Based on a Specific Response

As Anakin Skywalker once said, “This is where the fun begins”… and hopefully not “I’ve got a bad feeling about this”. 

The first thing we want to do is grab all of the Custom Metadata Mappings related to this survey.

Step 1

Do a null check on the results just to make sure you’ve got some records, then things get a bit more interesting and purely based on your use case and what you’re trying to do. 

In my Flow I looked for any questions that were specifically Choice questions (so no CSAT/numerical questions). This was done by checking that the QuestionChoiceId field (the lookup to the actual Choice in the question) is not null:

Step 2

My survey only had 1 possible question with a choice, so you may want to consider adding more criteria to future proof things.

Step 3 & 4

Next up is where I wish we had more collection processing power without assistance of invocables, but alas, this was my solution. I looped over the CMT mapping records I grabbed in Step 1, then I did another loop (GASP!) on the collection of responses above. I felt comfortable doing this because I was only ever going to have 1 response record x 3 mapping records, so there was no risk of hitting the dreaded 2,000 iteration limit. 

Note: This is needed because we can’t reference the DeveloperName of the responses’s related question record on the left side of a Get records element yet. Another added complexity is that we cannot directly reference CMT records like you can in Process Builder, so we have to retrieve them with Get elements.

Step 5

In the first decision we match up a Custom Metadata record that stores the Question Choice DeveloperName with the response’s Question Choice DeveloperName. Once we get that match, we move forward.

{!Loop_Over_CMT_Records.Question_Choice_DeveloperName__c} Equals {!Loop_Over_Found_Choice_Responses.QuestionChoice.DeveloperName}

Step 6

In the loop I check to see that the Response record’s related Choice Matches a particular outcome stored in the Custom Metadata field Question Choice Text. This is where we route the flow based on the automation we want.

{!Loop_Over_CMT_Records.Question_Choice_Text__c} Equals Request_Met

Step 7

From there, the world’s your oyster.  In my flow, I had 3 outcomes that did different things to the case. Remember how we used an assignment element for the Case earlier instead of an Update? That’s because we’re doing another update to it here:

Summary (In Words)

  1. Check that the Response’s Survey ($Record.SurveyVersion.DeveloperName) is the one you want to automate.
  2. Get all of the Survey Automation Mapping custom metadata records for the particular survey.
  3. Get all of the SurveyQuestionResponse records that have a related Choice record.
  4. Loop over the CMT Mapping records
  5. Loop over the found SurveyQuestionResponse that are choices (loop within a loop only if the expected responses with choices is tiny)
  6. In the loop, do a decision to match up the looped Custom Metadata record that stores the Question Choice DeveloperName with the 2nd loop’s Question Choice DeveloperName
  7. In the next decision, create your decision paths for each of the results you want to handle using the Question_Choice_Text__c field on the custom metadata record
  8. Do your thang!

Send an Alert to a Queue on a Poor Responses


Okay this one’s a bit easier. A lot easier, actually.

One nifty thing I’ve done is setting up an in-app alert to go to a specific queue when somebody responds poorly (1,2, or 3 out of 10) to a CSAT question. 

Step 1 

Grab any question responses that have a specific numerical value tied to it. Note that I use the Id of the record that triggered the Flow (the Survey Response’s record id)

Step 2

Do your null/empty check on this step ^

Step 3  (Optional)

Here we do some stuff to the case if needed.

I actually set the case status to ‘Problem’ if we found any matching results.

Step 4 (Optional)

Loop over the questions to store the question text

We Loop over the question responses to create a text variable of all of the question names for the alert we’ll make. This will go into the alert body.

The left side is a Text Collection variable of your choosing, the right side is: {!Loop_Over_Question_Names.Question.Name}

If you’re a big USF fan like me, you’ll have the handy ExtractStringsFromCollection action to do this for you, but if you don’t want to use that, you can go this route. 

Step 5 – Send the Alert

Next up is standard fare for using the Alert standard action in Flow. I made a post about it a year and a half ago on LinkedIn before I joined the UnofficialSF world: 

Your Notification Body can be a text template that references your text collection variable of the questions that were responded to poorly.

Step 6 – Do your Last Updates

We finally update the Case with all the various updates made in various assignment elements throughout the Flow:

And you’re done! I’d love to hear everyone’s thoughts on this – I know it’s a bit complex but once you get it going it can really scale quite well and you won’t need to make edits to your Flows if you make text changes to your Surveys.

How I Built This: Drive Your Survey Automations with Flow – Part 1

I’ve seen quite a number of people asking how to drive automations using Salesforce Surveys and Flow and thought I would share a solution I’ve used in a recent project that utilizes Custom Metadata records to intelligently drive automation based on specific responses.

First, I will say that for basic automations the Surveys add-on called Feedback Management will handle most scenarios for you – simply create a data map for each survey and perform a series of automations in your org. For more advanced automation, or if you can’t purchase Feedback Management, you may want more control and flexibility with the help of Flow.

Secondly, I will also say this is not the most basic solution. This takes some time to digest and if you want to sacrifice scalability for speed, you don’t need Custom Metadata records… however you will need to be careful of any changes made to the survey and the logic used in your flow that may look at the question text. Understanding this model will serve you well in the long run – both in knowing how to use Custom Metadata and in understanding the Survey data model.

The Survey Data Model

It’s critical to understand the Survey Data model in order to understand the Flow automation I built to handle survey responses. When a survey taker responds, a Survey Response record is made, with a series of Survey Question Responses for each responded question.

Please excuse this extremely un-architechty diagram.

Survey: This is the actual survey you create and has multiple versions. Everything eventually rolls up to this.

Survey Version: This is where we track the various versions of the related survey. Think of this as a similar version of ContentDocument / ContentVersion.

Survey Question: The questions that you create on your survey each have their own Question record that all relate to the Survey and Survey Version.

Survey Question Choice: If your question has multiple options (picklist, for example), you will have multiple Survey Question Choice records that all relate to a single Survey Question.

Invitations & Responses

Now we get into when a survey taker responds to the survey.

Survey Invitation: This is where the unique URL is made for the survey. This is also where you specify other details like if the user needs to be logged in to respond, and whether  You can add custom fields to this.

Survey Subject: A junction object that connects the survey response to another object. You can add custom fields here. This typically grants visibility to the survey.

Survey Response: Represents information about a participant’s response to a survey, such as the status of the response, the participant’s location, and when the survey was completed.

Survey Question Response: This is the actual response to a question. You can choose to drive your automations on this object, or Survey Response.

Take this SOQL query for example that looks at the out of the box Customer Satisfaction Survey choices. SurveyQuestionChoice looks up to SurveyVersion and SurveyQuestion. We use the relationship fields just to demonstrate how they all relate.

SELECT Id,Name,SurveyVersion.Name,Question.Name 

FROM SurveyQuestionChoice 

Where SurveyVersion.Name = ‘Customer Satisfaction’

Which object do you select as your starting point?

The first thing you need to consider is whether you want your automation to run when a Survey Response is created vs. when a Question Response (a response to an individual question) is created. Both have valid use cases.

Trigger on Survey Response

Trigger your flow here when you only want your Flow to fire once. 

  1. If you have a ton of questions with potentially complex logic and lots of looping, I would suggest starting here. 
  2. If you really only care that somebody has submitted a survey and you want to track its status elsewhere (like on a Case), go this route. Otherwise, you will needlessly fire off a Flow for every single question.

Trigger on Survey Question Response

Trigger your flow when you want your Flow to fire for every question. 

This works well when you don’t have a ton of questions and you don’t necessarily care about doing something with the status of the overall survey.

If you don’t necessarily care what happens on submission and you’re okay with multiple instances of your automation firing, choose this object as your starting point. 

Be wary of going this route if you have something like 50+ questions though, you could run into limit issues if your flow is complex and has lots of loops / processing. 

Custom Metadata: Survey Automation Mappings

The main thing that drives this Flow is the Survey Automation Mapping custom metadata object. In it, we store the developernames of specific Choice and Survey records. This allows us to avoid hard-coding responses in decision nodes in our Flow to drive automation on specific responses to questions. This means that we can change the answers to questions in surveys without worrying about also updating the Flow. 

Here are the fields:

Active: Keeps the mapping activate. Generally always a good idea to have an Active flag in any CMT record.

Department: Not needed for generic use, but could provide for department-specific automations for the same survey responses.

Mapping Help Text: Provides the admin the ability to provide context / help for the mapping record like where and why it’s used

Question Choice DeveloperName: Stores the developer name of the Question Choice – we do this so we dont have to reference static text in the automation.

Question Choice Text: Stores the answer response text – used in the decision node in the Survey Response Flow

Question Label: Optional. Used more as a handy reference when viewing mappings to know which question is being referenced in this CMT record.

Question Query Text: Used by Flow to retrieve the right CMT record and get the developer name of the question being queried. For example ‘Was_Request_Met’ AND Survey DeveloperName = ‘Surveyname’

Survey Developer Name: Stores the developer name of the survey containing the mapping

Each response to a ‘Choice’ type question has a related Choice record for each option for that question that has a flexible label, what the customer sees, but a static DeveloperName. We use that DeveloperName in the custom metadata record. See below for an example of how the data model looks with Questions, Choices, and Responses. 

Let’s take a scenario where there is a question on a survey that asks ‘Was your request met with us’ with 3 possible choices (see below). 

Getting the DeveloperName of Questions & Surveys

You will need to use SOQL (Dev Console, Workbench) or Salesforce Inspector’s (a Chrome Extension) ‘View Data’ feature to see a survey/question/choice record’s DeveloperName. VS Code also has some SOQL functionality.

Here is how you’d query for everything you need to create your Metadata Mappings:

SELECT id,Name, DeveloperName,Question.Name, Question.DeveloperName, SurveyVersion.Survey.DeveloperName

FROM SurveyQuestionChoice 

WHERE SurveyVersion.VersionNumber = (use the version # you want here)
AND SurveyVersion.Name = ‘Survey Name Here’


Using this, we can create some mapping records:

ActiveLabelSurvey Automation Mapping NameMapping Help TextQuestion DeveloperNameQuestion LabelQuestion Query TextQuestion Choice TextSurvey Developer NameQuestion Choice DeveloperName
TRUEMember Feedback – Request Not MetMember_Feedback_Request_Not_MetUsed to automate Case field ‘Request Status’ to drive if the case has to be re-opened or can be closedq_963113e4_d736_4a0d_b992_7f42bdb540e1Was your request met with us?Was_Request_MetNot_Met_Close_Casecustomer_satisfaction_surveyc_315e2827_6ffc_49d4_93b9_54b55cd356
TRUEMember Feedback – Request Not MetMember_Feedback_Request_MetUsed to automate Case field ‘Request Status’ to drive if the case has to be re-opened or can be closedq_963113e4_d736_4a0d_b992_7f42bdb540e1Was your request met with us?Was_Request_MetRequest_Metcustomer_satisfaction_surveyc_69a6ba6f_0aec_4f1d_8283_feff74543339

Next up – we’ll look at using these custom metadata records to drive specific responses in our Flow

Survey Invitations Demystified: Part 1 Builder

You can generate several types of invitations for your survey depending on the business use-cases of your organization. Some of the use cases would require the survey api’s while for certain others you can use the default invitations provided in the builder. In a series of posts, we are going to be discussing some of the options to generate survey invitations. This post talks about survey invitations you can generate from the survey builder. Read on to find out more

A few housekeeping stuff before we get into the nitty-gritty of invitations

Communities: For all surveys that involve external participants such as a contact or a lead there needs to be an active community setup. Communities come free with most editions of Salesforce and it is fairly easy to set one up.

Authenticated Surveys: Surveys rely on salesforce SSO for authenticating users and employees. For authenticating contacts and leads, you will need to rely on the authentication capabilities given by communities and this may involve the purchase of additional licenses. Authenticated survey responses are tied back to the participant. All authenticated survey invitations expire after a single take!

AnonymousSurveys: Survey invitations can be anonymous, which means that no information about the respondent is captured, the responses hence cannot be tied back to a participant and a participant can take this survey as many times without the link expiring.

Identified Surveys (aka Identified links): There are tons of use-cases where a survey would need to be tied back to the respondent but at the same time you don’t want to use authentication (SSO or want the customer to log in with a user name and password) before submitting the survey. This is where identified surveys come in and the idea here is that the survey invitation is generated with the information about the participant and these will expire after a single take.

Invitations generated by the Survey Builder: Get Survey Link

Types of invitations supported: Authenticated Surveys, Anonymous Surveys

As soon as you activate a survey, the survey builder generates 2 sets of invitations

  1. A link for particpants in your company like users and employees
  2. A link for particpants outside your company like contacts and leads

                                    Survey Builder -> Get Invitation

You will notice that there is a check box to anonymize the invitations in this modal. These links can be sent out via various email clients or even via the marketing cloud. There is an option to download the QR code equivalent of these links as well

Invitations generated by the Survey Builder: Send Email

Types of invitations supported: Authenticated Surveys, Anonymous Surveys, Identified Surveys

You have the option to send a survey via email directly to a single recipient or to a list in salesforce from the survey builder. The invitation link settings control the type of invitation

Here is what each of the options mean:

a) Unique link is the same as an identified survey.

b) Anonymize responses will obfuscate the survey responses

c) Don’t require authentication ensures that users are not asked to log in before taking the survey

d) Autoexpires gives you the flexibility of expiring the survey in a future date

In the next post we will take a look at some of the other ways to distribute surveys via process builder and flows