10.1: Add custom interactive validators when composing and replying to email #36
jstanden
started this conversation in
Guides and Tutorials
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
cerb10_1_interactive_validators.mov
Introduction
We've received several feature requests related to validating an outgoing email message with custom rules before allowing it to be sent.
For example, we may want to catch these mistakes:
A simple approach would be using automations to review the draft and aborting the 'Send' action if a mistake was found.
However, in many cases we just want these validations to be suggestions that may be bypassed. Otherwise it would be frustrating for a worker to constantly have to reword their replies to correct false errors.
For instance, I may mention the phrase "see attached" while describing this feature, without including an attachment, and a rigid rule would force me to either use another phrase, or to include an extraneous attachment, before continuing.
In 10.0 we introduced interactions, which are a perfect for this. We just needed a way to string a series of interactions together between clicking a button like 'Send' and performing that action; where workers could accept or ignore a rule, and any of the interactions could abort the action.
We call this feature "interactive validators".
Many interactive validators follow a confirmation pattern -- "Did you mean to do this? Yes/No".
Interactions in 10.0 only had buttons for "Continue" and "Reset". Displaying a Yes/No option required a sheet: element in a
form:continuation.This worked fine, but it had two drawbacks:
It usually required two clicks -- one to select an option, and another to continue.
Far worse, it required a hefty amount of complex Sheets KATA for every prompt.
Here's the KATA for the simple confirmation interaction above:
Sheets are very flexible -- they can do paging, filtering, display different kinds of columns (cards, icons, descriptions), use external data sets, do single/multiple selection, and so on. But they're overkill here when we just need a simple yes/no.
We improved the submit: element in 10.1 so you can have custom buttons at the bottom of the interaction, including multiple continue buttons with different values.
Now you can use this simpler KATA:
To get an interaction like:
This allows one-click interactions. The continue button you click determines the value of the element's placeholder (in this case
prompt_confirm).Our next issue was that some interactive validators might run and then determine that they weren't necessary, and we didn't want a blank interaction popup to briefly open and then disappear. The automation event can conditionally skip some interactions ahead of time, but others require logic or external data that can't be done in the event.
We had a kludgy
headless:option for interactions in toolbars, which was a hint to Cerb that the interaction was actually non-interactive and just returned output. For instance, on the 'Participants' widget on a ticket profile, selecting a participant in the sheet and clicking the 'Remove' interaction just needed to remove them and refresh the sheet. It didn't need any user input. But we needed to know that ahead of time.We ended up refactoring how interactions work so they run on the server first before the popup opens. If they exit with a continuation (like
await:form:), then the popup opens to show it. Otherwise, if they exit withreturn:output we can use it immediately without ever showing the popup. Afterwards we were able to completely remove theheadless:option and the need to know that ahead of time.With those improvements we were ready to modify the compose and reply workflows to allow multiple interactions on the 'Send' button.
There was already a server-side validation step before sending mail, which did things like checking if the
To:orSubject:was blank. That now also checks which interactive validators are enabled and it returns them. When the browser receives the list of validators, it opens them in sequence using a chain of Promises.Any of the validators can
return:reject:to break the chain and abort the send action.Let's look at how this works in practice.
Import the package
We've put together a demonstration package for 10.1 with three examples:
As an administrator, navigate to Setup >> Packages >> Import and paste the following package:
{ "package": { "name": "", "requires": { "cerb_version": "10.1.0" } }, "records": [ { "uid": "automation_validator_profanity", "_context": "automation", "name": "wgm.reply.validation.profanity", "description": "Warn a worker against rude replies", "extension_id": "cerb.trigger.mail.draft.validate", "script": "inputs:\n record/draft:\n record_type: draft\n default: {{caller_params.draft_id}}\n\nstart:\n outcome/notProfane:\n if@bool:\n {% set badwords %}\n bleep\n bleeped\n bleeper\n bleeping\n {% endset %}\n {{\n array_intersect(\n array_values(\n array_unique(\n inputs.draft.params.content|strip_lines('>')|tokenize\n )\n ),\n badwords|split_crlf\n ) is empty\n }}\n then:\n return:\n \n await/confirm:\n form:\n title: Before you send that...\n elements:\n say:\n content@text:\n # Be nice!\n Take a deep breath. Do you really want to send this?\n submit/prompt_next:\n buttons:\n continue/yes:\n label: Continue sending\n icon: send\n icon_at: start\n continue/no:\n label: I'll fix it\n style: secondary\n value: nope\n \n outcome/reject:\n if@bool: {{'nope' == prompt_next}}\n then:\n return:\n reject@bool: yes", "policy_kata": "" }, { "uid": "automation_validator_attached", "_context": "automation", "name": "wgm.reply.validation.attachments", "description": "Notify a worker when they refer to an attachment without including one", "extension_id": "cerb.trigger.mail.draft.validate", "script": "inputs:\n record/draft:\n record_type: draft\n default: {{caller_params.draft_id}}\n\nstart:\n outcome/ifAttached:\n if@bool:\n {% set text = inputs.draft.params.content|strip_lines('>')|lower %}\n {{not(\n (\n \"i've attached\" in text\n or \"see attached\" in text\n )\n and\n inputs.draft.params.file_ids is empty\n )}}\n then:\n return:\n \n await/confirm:\n form:\n title: Before you send that...\n elements:\n say:\n content@text:\n # Forget something?\n You mentioned an attachment. Did you forget to include it?\n submit/prompt_next:\n buttons:\n continue/yes:\n label: Continue sending\n icon: send\n icon_at: start\n continue/no:\n label: Whoops! I'll fix it\n style: secondary\n value: nope\n \n outcome/reject:\n if@bool: {{'nope' == prompt_next}}\n then:\n return:\n reject@bool: yes", "policy_kata": "" }, { "uid": "automation_validator_formatting", "_context": "automation", "name": "wgm.reply.validation.formatting", "description": "Warn when using formatting in plaintext", "extension_id": "cerb.trigger.mail.draft.validate", "script": "inputs:\n record/draft:\n record_type: draft\n default: {{caller_params.draft_id}}\n\nstart:\n outcome/ifFormatted:\n if@bool:\n {% set text = inputs.draft.params.content|strip_lines('>') %}\n {{not(\n (\n \"~~~\" in text\n or \"```\" in text\n or \"![\" in text\n or text matches '#\\\\[.*?\\\\]\\\\([^\\\\s]*\\\\)#'\n )\n and\n inputs.draft.params.format != 'parsedown'\n )}}\n then:\n return:\n\n await/confirm:\n form:\n title: Before you send that...\n elements:\n say:\n content@text:\n # Properly formatted?\n It looks like you're sending formatting in plaintext.\n submit/prompt_next:\n buttons:\n continue/yes:\n label: Continue sending\n icon: send\n icon_at: start\n continue/no:\n label: I'll fix it\n style: secondary\n value: nope\n \n outcome/reject:\n if@bool: {{'nope' == prompt_next}}\n then:\n return:\n reject@bool: yes", "policy_kata": "" } ], "events": [ { "event": "mail.draft.validate", "kata": "automation/profanityCheck:\n uri: cerb:automation:wgm.reply.validation.profanity\n inputs:\n draft@key: draft_id\n\nautomation/attachments:\n uri: cerb:automation:wgm.reply.validation.attachments\n inputs:\n draft@key: draft_id\n\nautomation/formatting:\n uri: cerb:automation:wgm.reply.validation.formatting\n inputs:\n draft@key: draft_id" } ] }Click the Import button.
This will create three automations and bind them on the
mail.draft.validateevent.Testing custom validators
Let's compose a message that breaks all three of these rules.
Navigate to Search >> Tickets >> (+).
Type a subject like
Testing interactive validatorsand paste the following message:Click the Send message button at the bottom of the compose popup.
The first interactive validator is actually one that we didn't import here -- it ships with Cerb 10.1+. It warns you when opening a ticket without recipients; which is permissible (phone support) but rare in most environments.
We meant to do it here, so click Send without recipients to continue.
Next, we're warned about our use of (tactful) profanity:
We should fix this, but let's Continue sending again anyway.
We're then warned that we mentioned an attachment without including one:
Again, let's Continue sending.
Finally, we're advised that we used Markdown syntax without formatting enabled:
We can click I'll fix it to address these issues.
Bleep off!toHave a nice day!Note that we're still not including a recipient because we don't want to send this test to anyone.
Click the Send message button at the bottom of the compose popup.
We're warned about the lack of recipients. This is intentional. Click Send without recipients.
The ticket is then created without any additional warnings:
How it works
As an administrator, you can review the
mail.draft.validateevent from Search >> Automation Events.This should have at least four bindings for interactions. The first one was included by the 10.1 update and is only enabled when composing without recipients. The next three were added by the package in this guide and are always enabled for both composing and replying.
Let's look at one of the interaction automations.
Hold
<Command>(macOS) or<Ctrl>(Windows) and click oncerb:automation:wgm.reply.validation.attachmentsinautomation/attachments:. Then click the Edit button at the top of the automation's card popup.Here's the automation KATA:
The automation has one input of
record/draftthat is passed in from the event. This loads the draft record for the compose or reply, and allows key expansion.The automation begins at
start:.We immediately do a negated check with
outcome/ifAttached:to see if our key phrases are mentioned in the normalized message body (inputs.draft.params.content|strip_lines('>')|lower-- excluding quoted lines and lowercased), and no attachments are included (inputs.draft.params.file_ids is empty).Because these conditions are negated by wrapping them in
not(), we only run the outcome'sthen:if what we're looking for does not match. If we didn't mention the phrases or we did include files, we exit early withreturn:.Otherwise, we enter an
await:form:continuation state to let the user know they mentioned an attachment without including one. This uses the newsubmit:buttons:functionality to show a confirmation.If the user clicks Whoops! I'll fix it then we
return:reject:fromoutcome/reject:which aborts sending. In any other case the interaction ends without objection and either sending continues or the next interactive validator runs.That's it! You can close the automation editor.
Conclusion
You've learned how to create interactive validators to catch mistakes, or enforce organization/team policies, when sending email.
There are many other places throughout Cerb where interactive validators would be useful (clicking 'Reply' on a message, or 'Save changes' on any record editor), and we'll be expanding their use going forward.
All reactions