Customer Satisfaction Surveys
Introduction
This workflow adds three custom records for tracking customer satisfaction metrics, a set of automations, a workspace with reporting widgets, and a community portal for gathering survey responses.
The following metrics are tracked:
- Net Promoter Score (NPS) - per contact, how likely your clients are to recommend your product/service to their friends and colleagues.
- Customer Satisfaction (CSAT) - at each response, how satisfied a client was with the interaction.
- Customer Effort Score (CES) - at resolution, how satisfied a client was with the entire process of resolving their issue.
Installation
This workflow is built into Cerb 11.0+. It will automatically update.
You can enable it from Search » Workflows » (+) » Customer Satisfaction Surveys.
Usage
Customizing the workflow
Edit the cerb.satisfaction.surveys workflow from Search » Workflows.
Click the Update Template at the top of the popup.
Click the Continue button at the bottom.
You can enable surveys from here, and customize the survey questions.
If you’ve deployed the portal to a custom domain name, update the portalBaseUrl. By default, the workflow will use your Cerb URL which you may not want to expose to your clients.
If you're a Cerb Cloud subscriber, we include high-availability community portal hosting. This is already handled for you.
Once you’ve made changes, click the Continue button twice.
NPS
Testing an NPS survey
Navigate to Search » Workspace Pages and click on Satisfaction.
On the NPS tab, click the Create NPS survey link button.
Select an email address and click the Continue button.
Copy or click on the survey link to test the survey interaction.
Select a rating, optionally add a comment, and then click the blue continue button.
Using the satisfaction dashboard
You should now have your first NPS rating.
Navigate back to Search » Workspace Pages » Satisfaction.
You should see your first NPS rating:
Broadcasting NPS survey links
Navigate to an email based worklist like Search » Contacts. Filter the list as needed.
Click on the bulk update button below the worklist.
Check the Send Broadcast section.
In To: check Contact email address.
Enter a Subject: like:
How likely are you to recommend Cerb to friends and colleagues?
In Compose: enter a message with placeholders like:
{% set inputs = { email: broadcast_email_address } -%}
Hi {{broadcast_email_address}},
We'd really appreciate your feedback on this two question survey about your experience with Cerb:
{{cerb_automation('cerb.satisfaction.surveys.nps.scriptingGetLink',inputs).return.survey_link}}
#signature
You can see the queued email messages from Setup » Mail » Outgoing » Queue. Each recipient receives a personalized survey link.
CSAT
Enabling CSAT surveys
CSAT survey links on outgoing messages are enabled per group.
Navigate to Search » Groups and edit a group.
If the Customer Satisfaction fieldset isn’t added yet, click the Add Fieldset button and select it.
Check the box to the right of Enable CSAT surveys: and click the Save Changes button.
Testing CSAT surveys
When you reply to a message from a group with CSAT surveys enabled, you’ll see a #survey-csat
tag below your #signature
.
When your message is sent, this tag will be converted to a survey link in plaintext and HTML formats. The link will only be appended to the delivered message and not the copy saved in Cerb.
An administrator can view the outgoing message with the survey link from Setup » Mail » Outgoing » Log.
You can also generate a CSAT survey link from the Search » Workspace Pages » Satisfaction page in the CSAT tab.
The link opens a survey interaction.
Responses are displayed on the CSAT tab of the Satisfaction workspace page.
CES
Enabling CES surveys
CES survey links for closed tickets are enabled per group.
Navigate to Search » Groups and edit a group.
If the Customer Satisfaction fieldset isn’t added yet, click the Add Fieldset button and select it.
Select a ticket-based snippet in CES Email Template:.
We’ve included a default snippet of Satisfaction CES Survey Email. You can optionally create your own and use the {{survey_link}}
placeholder.
Click the Save Changes button.
Testing CES surveys
When you closed a ticket in a group with CES surveys enabled, a survey link will be emailed to the initial sender.
An administrator can view the outgoing message with the survey link from Setup » Mail » Outgoing » Log.
You can also generate a CES survey link from the Search » Workspace Pages » Satisfaction page in the CES tab.
The link opens a survey interaction.
Responses are displayed on the CES tab of the Satisfaction workspace page.
Reference
You can build your own customer satisfaction workflow using this template as a reference.
Change occurrences of cerb.satisfaction.surveys to your own workflow identifier. Use a prefix based on a domain you own (e.g. com.example.workflow
).
workflow:
name: cerb.satisfaction.surveys
version: 2024-10-22T00:00:00Z
description: Gather and monitor customer satisfaction metrics like NPS, CSAT, and CES.
website: https://cerb.ai/workflows/cerb.satisfaction.surveys/
requirements:
cerb_version: >=11.0 <11.1
cerb_plugins: cerberusweb.core, cerb.website.interactions
config:
text/portalTitle:
default: Cerb - Customer Satisfaction
text/portalBaseUrl:
default: {{cerb_url('c=portal&p=csat-survey')}}
text/npsQuestion:
default: How likely are you to recommend Cerb to your friends and colleagues?
text/npsCommentLabel:
default: Why did you choose this rating? (optional)
text/csatQuestion:
default: How satisfied are you with the service you received?
text/cesQuestion:
default: The Cerb team made it easy to resolve my issue.
text/cesEmailSubject:
default: How did we do?
text/hashSecret:
default: {{random_string(40)}}
records:
custom_fieldset/fieldset_group_satisfaction:
fields:
name: Customer Satisfaction
context: group
owner__context: app
owner_id@int: 0
custom_field/field_group_satisfaction_csat:
fields:
name: Enable CSAT surveys
context: group
uri: satisfaction_csat_enabled
custom_fieldset_id: {{records.fieldset_group_satisfaction.id}}
type: C
pos@int: 1
custom_field/field_group_satisfaction_ces_snippet:
fields:
name: CES Email Template
context: group
uri: satisfaction_ces_snippet
custom_fieldset_id: {{records.fieldset_group_satisfaction.id}}
type: L
pos@int: 2
params:
context: snippet
custom_record/recordNps:
fields:
name: NPS Survey
name_plural: NPS Surveys
uri: nps_survey
params:
owners:
contexts@csv: cerberusweb.contexts.app
custom_field/recordNpsEmail:
fields:
name: Email
uri: email
context: nps_survey
pos: 0
type: L
params:
context: cerberusweb.contexts.address
custom_field/recordNpsCohort:
fields:
name: Cohort
uri: cohort
context: nps_survey
pos: 1
type: D
params:
options@csv: Promoter, Passive, Detractor
custom_field/recordNpsRating:
fields:
name: Rating
uri: rating
context: nps_survey
pos: 2
type: N
custom_field/recordNpsComment:
fields:
name: Comment
uri: comment
context: nps_survey
pos: 3
type: T
custom_record/recordCes:
fields:
name: CES Survey
name_plural: CES Surveys
uri: ces_survey
params:
owners:
contexts@csv: cerberusweb.contexts.app
custom_field/recordCesTicket:
fields:
name: Ticket
context: ces_survey
uri: ticket
type: L
params:
context: cerberusweb.contexts.ticket
custom_field/recordCesRating:
fields:
name: Rating
context: ces_survey
uri: rating
type: N
custom_field/recordCesComment:
fields:
name: Comment
context: ces_survey
uri: comment
type: S
custom_field/recordCesIp:
fields:
name: IP
context: ces_survey
uri: ip
type: S
custom_record/recordCsat:
fields:
name: CSAT Survey
name_plural: CSAT Surveys
uri: csat_survey
params:
owners:
contexts@csv: cerberusweb.contexts.app
custom_field/recordCsatMessage:
fields:
name: Message
context: csat_survey
uri: message
type: L
params:
context: cerberusweb.contexts.message
custom_field/recordCsatWorker:
fields:
name: Worker
context: csat_survey
uri: worker
type: L
params:
context: cerberusweb.contexts.worker
custom_field/recordCsatRating:
fields:
name: Rating
context: csat_survey
uri: rating
type: N
custom_field/recordCsatComment:
fields:
name: Comment
context: csat_survey
uri: comment
type: S
custom_field/recordCsatIp:
fields:
name: IP
context: csat_survey
uri: ip
type: S
snippet/snippet_ces_email:
updatePolicy@csv:
fields:
title: Satisfaction CES Survey Email
context: cerberusweb.contexts.ticket
owner__context: app
owner_id: 0
content@raw:
Hello! You recently contacted us for support.
Reference: #{{mask}}
Subject: {{subject}}
[How satisfied are you with the service you received?]({{survey_link}})
workspace_page/satisfactionPage:
fields:
name: Satisfaction
extension_id: core.workspace.page.workspace
owner__context: workflow
owner_id: {{workflow_id}}
workspace_tab/satisfactionTabNps:
fields:
name: NPS
page_id: {{records.satisfactionPage.id}}
extension_id: core.workspace.tab.dashboard
pos: 1
params:
layout: sidebar_left
options_kata@text:
workspace_widget/npsWidgetScore:
fields:
label: NPS Score (90d)
extension_id: core.workspace.widget.sheet
tab_id: {{records.satisfactionTabNps.id}}
pos: 1
width_units: 4
zone: sidebar
params:
data_query@raw:
type:automation.invoke
name:cerb.satisfaction.surveys.nps.dataQuery.score
inputs:(
date_range:"-90 days"
)
format:dictionaries
cache_secs@text:
placeholder_simulator_kata@text:
sheet_kata@raw:
layout:
style: fieldsets
headings@bool: no
paging@bool: no
columns:
text/score:
params:
text_align: center
text_size@raw: 500%
value_template@raw: {{score}}
bold@bool: yes
slider/nps:
params:
show_labels@bool: yes
text_align: center
text_size@raw: 200%
min: -100
max: 100
value_template@raw: {{score}}
threshold_colors:
-100: #FF0000
-75: #FF9900
-25: #CCCCCC
25: #00AA00
75: #00FF00
markdown/summary:
params:
value_template@raw:
|
|-:|-:|:-
| **Promoters:** | {{num_promoters}} / {{num_responses}} | {% if num_responses %}({{"%0.1f"|format(num_promoters/num_responses*100)}}%){% endif %}
| **Passives:** | {{num_passives}} / {{num_responses}} | {% if num_responses %}({{"%0.1f"|format(num_passives/num_responses*100)}}%){% endif %}
| **Detractors:** | {{num_detractors}} / {{num_responses}} | {% if num_responses %}({{"%0.1f"|format(num_detractors/num_responses*100)}}%){% endif %}
workspace_widget/npsWidgetActions:
fields:
label: Actions
tab_id: {{records.satisfactionTabNps.id}}
extension_id: core.workspace.widget.form_interaction
pos: 1
width_units: 4
zone: content
params:
interactions_kata@raw:
interaction/createLink:
label: Create NPS survey link
uri: cerb:automation:cerb.satisfaction.surveys.nps.getSignedLink.interaction
icon: check
hidden@bool: {{not worker_is_superuser}}
is_popup: 1
options_kata@raw:
hidden@bool: {{not current_worker_is_superuser}}
workspace_widget/npsWidgetResponses:
fields:
label: Recent Survey Responses
extension_id: core.workspace.widget.sheet
tab_id: {{records.satisfactionTabNps.id}}
pos: 2
width_units: 4
zone: content
params:
data_query@raw:
type:worklist.records
of:nps_survey
expand: [customfields,]
query:(
limit:25
sort:[-created]
)
format:dictionaries
cache_secs:
placeholder_simulator_kata:
sheet_kata@raw:
layout:
style: table
headings@bool: no
paging@bool: yes
title_column: email__context
columns:
card/email__context:
label: Responder
params:
context_key: _context
id_key: id
underline@bool: no
text_size@raw: 150%
icon:
svg:
data_template@raw:
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="50" fill="{% if rating > 8 %}#65952E{% elseif rating > 5 %}#888888{% else %}#BE3B2A{% endif %}" />
<text x="50%" y="55%" font-size="80" font-weight="bold" fill="white" text-anchor="middle" alignment-baseline="middle">{{rating}}</text>
</svg>
text/comment:
date/created_at:
label: When
toolbar_kata:
workspace_tab/satisfactionTabCsat:
fields:
name: CSAT
page_id: {{records.satisfactionPage.id}}
extension_id: core.workspace.tab.dashboard
pos: 2
params:
layout: sidebar_left
options_kata@text:
workspace_widget/widgetCsatScore:
fields:
label: CSAT Avg Score (90d)
extension_id: core.workspace.widget.sheet
tab_id: {{records.satisfactionTabCsat.id}}
pos: 1
width_units: 4
zone: sidebar
params:
data_query@raw:
type:worklist.subtotals
of:csat_survey
by.avg:rating
query:(
created:"-90 days to now"
)
format:dictionaries
cache_secs@text:
placeholder_simulator_kata@text:
sheet_kata@raw:
layout:
style: fieldsets
headings@bool: no
paging@bool: no
columns:
text/rating:
params:
text_align: center
text_size@raw: 500%
value_template@raw: {{rating|number_format(1)}}
bold@bool: yes
slider/csat:
params:
show_labels@bool: yes
text_align: center
text_size@raw: 200%
min: 0
max: 10
value_template@raw: {{rating}}
threshold_colors:
1: #FF0000
3: #FF9900
5: #CCCCCC
7: #00AA00
9: #00FF00
workspace_widget/widgetCsatActions:
fields:
label: Actions
extension_id: core.workspace.widget.form_interaction
tab_id: {{records.satisfactionTabCsat.id}}
pos@int: 1
width_units@int: 4
zone: content
params:
interactions_kata@raw:
interaction/createLink:
label: Create CSAT survey link
uri: cerb:automation:cerb.satisfaction.surveys.csat.getSignedLink.interaction
icon: check
hidden@bool: {{not worker_is_superuser}}
is_popup: 1
options_kata@raw:
hidden@bool: {{not current_worker_is_superuser}}
workspace_widget/widgetCsatResponses:
fields:
label: Recent Survey Responses
extension_id: core.workspace.widget.sheet
tab_id: {{records.satisfactionTabCsat.id}}
pos@int: 2
width_units@int: 4
zone: content
params:
data_query@text:
type:worklist.records
of:csat_survey
expand: [customfields,]
query:(
limit:25
sort:[-created]
)
format:dictionaries
cache_secs@text:
placeholder_simulator_kata@text:
sheet_kata@raw:
layout:
style: table
headings@bool: no
paging@bool: yes
title_column: comment
columns:
card/comment:
label: Sent Message
params:
context_key: _context
label_key: comment
id_key: id
underline@bool: no
icon:
svg:
data_template@raw:
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="50" fill="{% if rating > 7 %}#65952E{% elseif rating > 5 %}#65952E{% elseif rating > 3 %}#888888{% elseif rating > 1 %}#BE3B2A{% else %}#BE3B2A{% endif %}" />
<text x="50%" y="55%" font-size="80" font-weight="bold" fill="white" text-anchor="middle" alignment-baseline="middle">{{rating}}</text>
</svg>
card/message_worker__context:
label: Worker
params:
underline@bool: no
card/message__context:
label: Message
params:
underline@bool: no
label_key: message_ticket__label
date/created_at:
label: When
toolbar_kata@text:
workspace_tab/satisfactionTabCes:
fields:
name: CES
page_id: {{records.satisfactionPage.id}}
extension_id: core.workspace.tab.dashboard
pos: 3
params:
layout: sidebar_left
options_kata@text:
workspace_widget/widgetCesScore:
fields:
label: CES Avg Score (90d)
extension_id: core.workspace.widget.sheet
tab_id: {{records.satisfactionTabCes.id}}
pos: 1
width_units: 4
zone: sidebar
params:
data_query@raw:
type:worklist.subtotals
of:ces_survey
by.avg:rating
query:(
created:"-90 days to now"
)
format:dictionaries
cache_secs@text:
placeholder_simulator_kata@text:
sheet_kata@raw:
layout:
style: fieldsets
headings@bool: no
paging@bool: no
columns:
text/rating:
params:
text_align: center
text_size@raw: 500%
value_template@raw: {{rating|number_format(1)}}
bold@bool: yes
slider/ces:
params:
show_labels@bool: yes
text_align: center
text_size@raw: 200%
min: 0
max: 7
value_template@raw: {{rating}}
threshold_colors:
1: #FF0000
3: #FF9900
4: #CCCCCC
5: #00AA00
6: #00FF00
workspace_widget/widgetCesActions:
fields:
label: Actions
extension_id: core.workspace.widget.form_interaction
tab_id: {{records.satisfactionTabCes.id}}
pos@int: 1
width_units@int: 4
zone: content
params:
interactions_kata@raw:
interaction/createLink:
label: Create CES survey link
uri: cerb:automation:cerb.satisfaction.surveys.ces.getSignedLink.interaction
icon: check
hidden@bool: {{not worker_is_superuser}}
is_popup: 1
options_kata@raw:
hidden@bool: {{not current_worker_is_superuser}}
workspace_widget/widgetCesResponses:
fields:
label: Recent Survey Responses
extension_id: core.workspace.widget.sheet
tab_id: {{records.satisfactionTabCes.id}}
pos: 2
width_units: 4
zone: content
params:
data_query@text:
type:worklist.records
of:ces_survey
expand: [customfields,]
query:(
limit:25
sort:[-created]
)
format:dictionaries
cache_secs@text:
placeholder_simulator_kata@text:
sheet_kata@raw:
layout:
style: table
headings@bool: no
paging@bool: yes
title_column: comment
columns:
card/comment:
label: Survey
params:
context_key: _context
label_key: comment
id_key: id
underline@bool: no
icon:
svg:
data_template@raw:
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="50" fill="{% if rating > 7 %}#65952E{% elseif rating > 5 %}#65952E{% elseif rating > 3 %}#888888{% elseif rating > 1 %}#BE3B2A{% else %}#BE3B2A{% endif %}" />
<text x="50%" y="55%" font-size="80" font-weight="bold" fill="white" text-anchor="middle" alignment-baseline="middle">{{rating}}</text>
</svg>
card/ticket__context:
label: Ticket
params:
underline@bool: no
label_key: ticket__label
date/created_at:
label: When
toolbar_kata@text:
community_portal/portalSatisfactionSurveys:
fields:
extension_id: cerb.website.interactions
name: Customer Satisfaction Surveys
uri: csat-survey
params:
automations_kata@raw:
automation/nps:
uri: cerb:automation:cerb.satisfaction.surveys.nps.interaction
disabled@bool: {{interaction != 'nps'}}
automation/csat:
uri: cerb:automation:cerb.satisfaction.surveys.csat.interaction
disabled@bool: {{interaction != 'csat'}}
automation/ces:
uri: cerb:automation:cerb.satisfaction.surveys.ces.interaction
disabled@bool: {{interaction != 'ces'}}
cors_origins_allowed@text:
portal_kata@raw:
layout:
meta:
title: {{cerb_workflow_config('cerb.satisfaction.surveys','portalTitle')}}
header:
logo:
text: {{cerb_workflow_config('cerb.satisfaction.surveys','portalTitle')}}
#navbar:
# link/website:
# label: Back to website
# href: https://cerb.ai/
# class: cerb-link-button
automation/automationNpsLink:
fields:
name: cerb.satisfaction.surveys.nps.getSignedLink
description: Generate a signed link for an NPS survey
extension_id: cerb.trigger.automation.function
script@raw:
inputs:
text/email:
type: email
required@bool: yes
start:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
survey_base_url: {{config.portalBaseUrl|trim('/', 'right')}}
params:
email: {{inputs.email}}
expires@date: +1 week
return:
survey_link: {{survey_base_url}}/nps?{{params|url_encode}}&s={{params|values|join|hash_hmac(cerb_workflow_config('cerb.satisfaction.surveys','hashSecret'),"sha256")[8:24]}}
automation/automationNpsScriptingGetLink:
fields:
name: cerb.satisfaction.surveys.nps.scriptingGetLink
extension_id: cerb.trigger.scripting.function
description: Create NPS survey links from snippets and scripting
script@raw:
inputs:
text/email:
type: email
required@bool: yes
start:
function/getSignedLink:
output: results
uri: cerb:automation:cerb.satisfaction.surveys.nps.getSignedLink
inputs:
email: {{inputs.email}}
on_success:
return:
survey_link: {{results.survey_link}}
policy_kata@raw:
commands:
function:
deny/uri@bool: {{uri != 'cerb:automation:cerb.satisfaction.surveys.nps.getSignedLink'}}
allow@bool: yes
automation/automationNpsLinkInteraction:
fields:
name: cerb.satisfaction.surveys.nps.getSignedLink.interaction
extension_id: cerb.trigger.interaction.worker
description@text:
script@raw:
start:
set:
prompt_email__context: address
await/prompt:
form:
title: Generate NPS Survey Link
elements:
sheet/prompt_email_id:
label: Email address:
required@bool: yes
data:
automation:
uri: cerb:automation:cerb.data.records
inputs:
record_type: address
query_required: isBanned:n isDefunct:n
schema:
layout:
style: table
filtering@bool: yes
headings@bool: no
paging@bool: yes
columns:
selection/_sel:
params:
mode: single
value_key: id
card/id:
params:
bold@bool: yes
underline@bool: no
card/org_id:
params:
underline@bool: no
function:
output: results
uri: cerb:automation:cerb.satisfaction.surveys.nps.getSignedLink
inputs:
email: {{prompt_email_address}}
await/results:
form:
elements:
say:
content: {{results.survey_link}}
policy_kata@raw:
commands:
function:
deny/uri@bool: {{uri != 'cerb:automation:cerb.satisfaction.surveys.nps.getSignedLink'}}
allow@bool: yes
automation/interactionNpsSurvey:
fields:
name: cerb.satisfaction.surveys.nps.interaction
extension_id: cerb.trigger.interaction.website
policy_kata@raw:
commands:
record.create:
deny/type@bool: {{inputs.record_type is not record type ('nps_survey')}}
allow@bool: yes
record.search:
deny/type@bool: {{inputs.record_type is not record type ('address', 'nps_survey')}}
allow@bool: yes
script@raw:
start:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
decision/validation:
outcome/missingLink:
if@bool: {{interaction_params.email is empty or interaction_params.expires is empty or interaction_params.s is empty}}
then@ref: invalidLink
outcome/badSignature:
if@bool: {{[interaction_params.email,interaction_params.expires]|join|hash_hmac(config.hashSecret,"sha256")[8:24] != interaction_params.s}}
then@ref: invalidLink
outcome/expiredSig:
if@bool: {{interaction_params.expires < 'now'|date('U')}}
then@ref: invalidLink
record.search/email:
output: email
inputs:
record_type: address
record_query: email:${lookup_email} limit:1
record_query_params:
lookup_email: {{interaction_params.email}}
record_expand: _label
validation@raw:
{{email.id is empty ? 'Record not found.'}}
on_error@ref: invalidLink
record.search/survey:
output: nps_survey
inputs:
record_type: nps_survey
record_query: email.id:${lookup_email_id} created:"today -30 days to now" limit:1
record_query_params:
lookup_email_id@int: {{email.id}}
on_success:
outcome/dupe:
if@bool: {{nps_survey.id}}
then@ref: dupeSurvey
await/survey:
form:
title: Survey
elements:
sheet/prompt_rating:
label: {{config.npsQuestion}}
required@bool: yes
data@json: {{array_fill_keys(range(0,10),[])|json_encode}}
validation@raw:
{{(prompt_rating is not numeric or prompt_rating < 0 or prompt_rating > 10) ? 'Rating must be from 0 to 10'}}
limit: 11
schema:
layout:
filtering@bool: no
headings@bool: no
paging@bool: no
style: scale
params:
min_label: Very unlikely
max_label: Very likely
columns:
selection/prompt_rating:
params:
label_key: __index
value_key: __index
mode: single
textarea/prompt_comment:
label: {{config.npsCommentLabel}}
record.create/nps:
output: nps_survey
inputs:
record_type: nps_survey
fields:
name: {{email._label}} rated {{prompt_rating}} / 10{% if prompt_comment %}: {{prompt_comment|truncate(128)}}{% endif %}
email@int: {{email.id}}
rating: {{prompt_rating|round}}
cohort: {{prompt_rating > 8 ? 'Promoter' : (prompt_rating < 7 ? 'Detractor' : 'Passive')}}
comment@key,optional: prompt_comment
owner__context: app
owner_id@int: 0
await/confirm:
form:
elements:
say:
message: Thanks for your feedback!
submit:
continue@bool: no
reset@bool: no
&invalidLink:
await:
form:
elements:
say:
message@text:
Sorry! This is an invalid or expired link.
submit:
continue@bool: no
reset@bool: no
return:
&dupeSurvey:
await:
form:
elements:
say:
message@text:
Thanks! We have already recorded your response to this survey link.
submit:
continue@bool: no
reset@bool: no
return:
automation/functionCsatSurveyLink:
fields:
name: cerb.satisfaction.surveys.csat.getSurveyLink
extension_id: cerb.trigger.automation.function
description@text:
script@raw:
inputs:
record/message:
record_type: message
required@bool: yes
start:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
survey_base_url: {{config.portalBaseUrl|trim('/', 'right')}}
expires_in@date: 7 days
draft_token: {{inputs.message.token}}
hash: {{[draft_token,expires_in]|join|hash_hmac(config.hashSecret)[26:18]}}
return:
survey_link@text:
{{survey_base_url}}/csat?m={{draft_token}}&s={{hash}}&expires={{expires_in}}
policy_kata@raw:
commands:
automation/automationCsatLinkInteraction:
fields:
name: cerb.satisfaction.surveys.csat.getSignedLink.interaction
extension_id: cerb.trigger.interaction.worker
description@text:
script@raw:
start:
await/prompt:
form:
title: Generate CSAT Survey Link
elements:
chooser/prompt_message_id:
label: Choose an outgoing message:
record_type: message
query@text: isOutgoing:y created:"-7 days"
multiple@bool: no
required@bool: yes
function:
output: results
uri: cerb:automation:cerb.satisfaction.surveys.csat.getSurveyLink
inputs:
message: {{prompt_message_id}}
await/results:
form:
elements:
say:
content: {{results.survey_link}}
policy_kata@raw:
commands:
function:
deny/uri@bool: {{uri != 'cerb:automation:cerb.satisfaction.surveys.csat.getSurveyLink'}}
allow@bool: yes
automation/interactionCsatSurvey:
fields:
name: cerb.satisfaction.surveys.csat.interaction
extension_id: cerb.trigger.interaction.website
description@text:
script@raw:
start:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
valid_s: {{[interaction_params.m,interaction_params.expires]|join|hash_hmac(config.hashSecret)[26:18]}}
decision/validation:
outcome/missingLink:
if@bool: {{interaction_params.m is empty or interaction_params.expires is empty or interaction_params.s is empty}}
then@ref: invalidLink
outcome/badSignature:
if@bool: {{valid_s != interaction_params.s}}
then@ref: invalidLink
outcome/expiredSig:
if@bool: {{interaction_params.expires < 'now'|date('U')}}
then@ref: invalidLink
# Find the message by token
record.search/message:
output: message
inputs:
record_type: message
record_query: token:${lookup_token} limit:1
record_query_params:
lookup_token: {{interaction_params.m}}
record_expand: _label, worker__label
validation@raw:
{{message.id is empty ? 'Record not found.'}}
on_error@ref: invalidLink
# Does this survey already exist?
record.search/survey:
output: lookup_survey
inputs:
record_type: csat_survey
record_query: message:(id:${message_id})
record_query_params:
message_id@int: {{message.id}}
on_success:
outcome/dupe:
if@bool: {{lookup_survey|length}}
then@ref: alreadyExists
# Prompt for the survey response
await/survey:
form:
title: Survey
elements:
sheet/prompt_rating:
label: {{config.csatQuestion}}
required@bool: yes
data:
10:
label: very satisfied
rating: 10
7:
label: satisfied
rating: 7
5:
label: neutral
rating: 5
3:
label: dissatisfied
rating: 3
1:
label: very dissatisfied
rating: 1
validation@raw:
{{(prompt_rating is not numeric or prompt_rating < 0 or prompt_rating > 10) ? 'Rating must be from 0 to 10'}}
limit: 5
schema:
layout:
filtering@bool: no
headings@bool: no
paging@bool: no
style: scale
columns:
selection/rating:
params:
mode: single
# [TODO] SVG icon
text/label:
textarea/prompt_comment:
label: {{config.npsCommentLabel}}
record.create/csat:
output: csat_survey
inputs:
record_type: csat_survey
fields:
name: Rated {{message.worker__label}} as {{prompt_rating}} / 10{% if prompt_comment %}: {{prompt_comment|truncate(128)}}{% endif %}
message@int: {{message.id}}
worker@int: {{message.worker_id}}
rating: {{prompt_rating|round}}
comment@key,optional: prompt_comment
ip: {{client_ip}}
owner__context: app
owner_id@int: 0
await/confirm:
form:
elements:
say:
message: Thanks for your feedback!
submit:
continue@bool: no
reset@bool: no
&invalidLink:
await:
form:
elements:
say:
message@text:
Sorry! This is an invalid or expired link.
submit:
continue@bool: no
reset@bool: no
return:
&alreadyExists:
await:
form:
elements:
say:
message@text:
We already have a response for this survey link.
submit:
continue@bool: no
reset@bool: no
return:
policy_kata@raw:
commands:
record.create:
deny/type@bool: {{inputs.record_type is not record type ('csat_survey')}}
allow@bool: yes
record.search:
deny/type@bool: {{inputs.record_type is not record type ('csat_survey','message')}}
allow@bool: yes
automation/functionCesSurveyLink:
fields:
name: cerb.satisfaction.surveys.ces.getSurveyLink
extension_id: cerb.trigger.automation.function
description@text:
script@raw:
inputs:
record/ticket:
record_type: ticket
required@bool: yes
start:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
survey_base_url: {{config.portalBaseUrl|trim('/', 'right')}}
expires_in@date: 7 days
ticket_mask: {{inputs.ticket.mask}}
hash: {{[ticket_mask,expires_in]|join|hash_hmac(config.hashSecret)[26:18]}}
return:
survey_link@text:
{{survey_base_url}}/ces?m={{ticket_mask}}&s={{hash}}&expires={{expires_in}}
policy_kata@raw:
commands:
automation/automationCesLinkInteraction:
fields:
name: cerb.satisfaction.surveys.ces.getSignedLink.interaction
extension_id: cerb.trigger.interaction.worker
description@text:
script@raw:
start:
await/prompt:
form:
title: Generate CES Survey Link
elements:
chooser/prompt_ticket_id:
label: Ticket:
record_type: ticket
query@text: status:c created:"-7 days"
multiple@bool: no
required@bool: yes
function:
output: results
uri: cerb:automation:cerb.satisfaction.surveys.ces.getSurveyLink
inputs:
ticket: {{prompt_ticket_id}}
await/results:
form:
elements:
say:
content: {{results.survey_link}}
policy_kata@raw:
commands:
function:
deny/uri@bool: {{uri != 'cerb:automation:cerb.satisfaction.surveys.ces.getSurveyLink'}}
allow@bool: yes
automation/interactionCesSurvey:
fields:
name: cerb.satisfaction.surveys.ces.interaction
extension_id: cerb.trigger.interaction.website
description@text:
script@raw:
start:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
valid_s: {{[interaction_params.m,interaction_params.expires]|join|hash_hmac(config.hashSecret)[26:18]}}
decision/validation:
outcome/missingLink:
if@bool: {{interaction_params.m is empty or interaction_params.expires is empty or interaction_params.s is empty}}
then@ref: invalidLink
outcome/badSignature:
if@bool: {{valid_s != interaction_params.s}}
then@ref: invalidLink
outcome/expiredSig:
if@bool: {{interaction_params.expires < 'now'|date('U')}}
then@ref: invalidLink
# Find the ticket by token
record.search/ticket:
output: ticket
inputs:
record_type: ticket
record_query: mask:${lookup_mask} limit:1
record_query_params:
lookup_mask: {{interaction_params.m}}
record_expand: _label
validation@raw:
{{ticket.id is empty ? 'Record not found.'}}
on_error@ref: invalidLink
# Does this survey already exist?
record.search/survey:
output: lookup_survey
inputs:
record_type: ces_survey
record_query: ticket.id:${ticket_id} limit:1
record_query_params:
ticket_id@int: {{ticket.id}}
validation@raw:
{{lookup_survey.id is not empty ? 'Survey response already exists.'}}
on_error@ref: alreadyExists
# Prompt for the survey response
await/survey:
form:
title: Survey
elements:
sheet/prompt_rating:
label: {{config.cesQuestion}}
required@bool: yes
data:
7:
label: strongly agree
rating: 7
5:
label: agree
rating: 5
4:
label: neutral
rating: 4
3:
label: disagree
rating: 3
1:
label: strongly disagree
rating: 1
validation@raw:
{{(prompt_rating is not numeric or prompt_rating < 1 or prompt_rating > 7) ? 'Rating must be from 1 to 7'}}
limit: 5
schema:
layout:
filtering@bool: no
headings@bool: no
paging@bool: no
style: scale
columns:
selection/rating:
params:
mode: single
# [TODO] SVG icon
text/label:
textarea/prompt_comment:
label: {{config.npsCommentLabel}}
record.create/ces:
output: ces_survey
inputs:
record_type: ces_survey
fields:
name: Rated [#{{ticket.mask}}] as {{prompt_rating}} / 7{% if prompt_comment %}: {{prompt_comment|truncate(128)}}{% endif %}
ticket@int: {{ticket.id}}
rating: {{prompt_rating|round}}
comment@key,optional: prompt_comment
ip: {{client_ip}}
owner__context: app
owner_id@int: 0
await/confirm:
form:
elements:
say:
message: Thanks for your feedback!
submit:
continue@bool: no
reset@bool: no
&invalidLink:
await:
form:
elements:
say:
message@text:
Sorry! This is an invalid or expired link.
submit:
continue@bool: no
reset@bool: no
return:
&alreadyExists:
await:
form:
elements:
say:
message@text:
We already have a response for this survey link.
submit:
continue@bool: no
reset@bool: no
return:
policy_kata@raw:
commands:
record.create:
deny/type@bool: {{inputs.record_type is not record type ('ces_survey')}}
allow@bool: yes
record.search:
deny/type@bool: {{inputs.record_type is not record type ('ces_survey','ticket')}}
allow@bool: yes
automation/dataQueryNpsScore:
fields:
name: cerb.satisfaction.surveys.nps.dataQuery.score
extension_id: cerb.trigger.data.query
description@text:
script@raw:
inputs:
text/date_range:
required@bool: yes
start:
data.query/nps_surveys:
inputs:
query@text:
type:worklist.subtotals
of:nps_survey
by.count:[cohort]
query:(created:${date_range})
format:dictionaries
query_params:
date_range@key: inputs:date_range
output: results
set:
num_responses@int: {{array_sum(results.data|column('count'))}}
num_promoters@int: {{array_sum(results.data|filter((v) => v.cohort=='Promoter')|column('count'))}}
num_passives@int: {{array_sum(results.data|filter((v) => v.cohort=='Passive')|column('count'))}}
num_detractors@int: {{array_sum(results.data|filter((v) => v.cohort=='Detractor')|column('count'))}}
score@int: {{100*((num_promoters/num_responses) - (num_detractors/num_responses))}}
return:
data:
nps:
num_responses@key: num_responses
num_promoters@key: num_promoters
num_passives@key: num_passives
num_detractors@key: num_detractors
score@key: score
policy_kata@raw:
commands:
data.query:
deny/type@bool: {{query.type != 'worklist.subtotals'}}
allow@bool: yes
automation/csatReplyIncludeCsatLink:
fields:
name: cerb.satisfaction.surveys.csat.replyAppendCsatLink
extension_id: cerb.trigger.mail.draft
description: Include the #survey-csat link on outgoing messages
script@raw:
start:
outcome/notNewReply:
if@bool:
{{
is_resumed
or draft_type != 'ticket.reply'
or '#survey-csat' in draft_params.content
}}
then:
return:
return:
draft:
params:
content: {{draft_params.content|replace({"#signature\n":"#signature\n#survey-csat\n"})}}
policy_kata@raw:
commands:
automation/csatGenerateSentCsatLink:
fields:
name: cerb.satisfaction.surveys.csat.generateSentCsatLink
extension_id: cerb.trigger.mail.send
description: Replace the #survey-csat token with a link on the sent message
script@raw:
start:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
survey_base_url: {{config.portalBaseUrl|trim('/', 'right')}}
expires_in@date: 7 days
survey_link@text:
{% set hash = [draft_token,expires_in]|join|hash_hmac(config.hashSecret)[26:18] %}
{{survey_base_url}}/csat?m={{draft_token}}&s={{hash}}&expires={{expires_in}}
return:
content:
# Remove from the saved copy
replace/saved:
on:
html@bool: yes
text@bool: yes
sent@bool: no
saved@bool: yes
text: #survey-csat
with@text:
# Replace on the sent HTML copy
replace/sentHtml:
on:
html@bool: yes
sent@bool: yes
text@bool: no
saved@bool: no
text: #survey-csat
with@text:
~~~~~~~~~~~~~~~~~~~~~~~~~
[How did I do?]({{survey_link}})
# Replace on the sent plaintext copy
replace/sentText:
on:
text@bool: yes
sent@bool: yes
html@bool: no
saved@bool: no
text: #survey-csat
with@text:
~~~~~~~~~~~~~~~~~~~~~~~~~
How did I do? {{survey_link}}
policy_kata@raw:
commands:
automation/cesSendClosedTicketSurvey:
fields:
name: cerb.satisfaction.surveys.ces.sendClosedTicketSurvey
extension_id: cerb.trigger.record.changed
description: Send a CES survey on closed tickets
script@raw:
start:
outcome/notClosed:
if@bool:
{{
is_new
or record__type is not record type ('ticket')
or not record_group_satisfaction_ces_snippet_id
or record_status == was_record_status
or record_status != 'closed'
or 0 == record_num_messages_out
or was_record_elapsed_resolution_first
}}
then:
return:
set:
config@json: {{cerb_workflow_config('cerb.satisfaction.surveys')|json_encode}}
survey_base_url: {{config.portalBaseUrl|trim('/', 'right')}}
expires_in@date: 7 days
hash: {{[record_mask,expires_in]|join|hash_hmac(config.hashSecret)[26:18]}}
survey_link@trim:
{{survey_base_url}}/ces?m={{record_mask}}&expires={{expires_in}}&s={{hash}}
kata.parse:
output: survey_email
inputs:
kata:
subject: {{config.cesEmailSubject}}
body: {{record_group_satisfaction_ces_snippet_content}}
dict@json:
{{cerb_placeholders_list('record_', '')|merge({'survey_link':survey_link})|json_encode}}
record.create:
output: new_draft
inputs:
record_type: draft
fields:
type: mail.transactional
is_queued: 1
queue_delivery_date@date: now
name: CES survey for {{record_initial_message_sender_address}} on {{record__label}}
params:
to: {{record_initial_message_sender_address}}
subject: {{survey_email.subject}}
headers:
Auto-Submitted: yes
X-Auto-Response-Suppress: All
format: parsedown
content: {{survey_email.body}}
policy_kata@raw:
commands:
record.create:
deny/type@bool: {{inputs.record_type is not record type ('draft')}}
allow@bool: yes
automation_event_listener/listener_ces_record_changes:
fields:
name: CES Surveys
event_name: record.changed
priority@int: 50
is_disabled: 0
event_kata@raw:
automation/cesClosedSurvey:
uri: cerb:automation:cerb.satisfaction.surveys.ces.sendClosedTicketSurvey
disabled@bool:
{{
is_new
or record__type is not record type ('ticket')
or not record_group_satisfaction_ces_snippet_id
or record_status == was_record_status
or record_status != 'closed'
or 0 == record_num_messages_out
or was_record_elapsed_resolution_first
}}
automation_event_listener/listener_csat_mail_draft:
fields:
name: CSAT Surveys
event_name: mail.draft
priority@int: 50
is_disabled: 0
event_kata@raw:
automation/csatLink:
uri: cerb:automation:cerb.satisfaction.surveys.csat.replyAppendCsatLink
disabled@bool:
{{
is_resumed
or draft_type != 'ticket.reply'
or not draft_ticket_group_satisfaction_csat_enabled
or not record_group_satisfaction_ces_snippet_id
or '#survey-csat' in draft_params.content
}}
automation_event_listener/listener_csat_mail_send:
fields:
name: CSAT Surveys
event_name: mail.send
priority@int: 50
is_disabled: 0
event_kata@raw:
automation/csatLink:
uri: cerb:automation:cerb.satisfaction.surveys.csat.generateSentCsatLink
disabled@bool:
{{
draft_type != 'ticket.reply'
or draft_worker_id is empty
or not draft_ticket_group_satisfaction_csat_enabled
or '#survey-csat' not in draft_params.content
}}
metric/metric_nps:
fields:
name: cerb.satisfaction.surveys.nps.score
type: gauge
description: Net Promoter Score (NPS) over time
metric/metric_csat_avg:
fields:
name: cerb.satisfaction.surveys.csat.score
type: gauge
description: Avg. Customer Satisfaction (CSAT) over time by worker
metric/metric_ces_avg:
fields:
name: cerb.satisfaction.surveys.ces.score
type: gauge
description: Avg. Customer Effort Score (CES) over time by worker
automation_timer/timer_metrics:
fields:
name: Sample satisfaction metrics
is_recurring: 1
recurring_patterns@raw:
# Every hour
0 * * * *