Overview
The Andi Skills Platform Email Power provides an array of features to generate emails that can be fully branded and customized through custom handlebar templates, embedded images, and attachments. It also provides a simpler, professional default out of the box experience with a default template.
In this Article
Templating
The Handlebars Templating Language allows authors to create custom reusable HTML that gets transformed along with data into the final content of the email. Templating provides the flexibility to create custom branded messaging beyond what can be done with default template.
Creating Email templates is like PrintPowers templates, but the author must keep in mind that email is presented by an email client and PrintPowers output is presented in a web browser so different constraints for each need to be kept in mind.
It is highly recommended that unless you are experienced creating emails with HTML that you do some research on the subject. There exist many great resources both paid and free that can guide you through the process. The reason being is not all aspects of HTML design will work with email and much of that is determined by that targeted email client and needs to be taken into consideration.
You might also consider 3rd party email authoring tools that usually come with an export to HTML option to get started quickly. From that base then its just a matter off adding Handlebar Template pieces where dynamic content is needed.
API Reference
The Email Power API is made up of a single function and two model types. The function and model types are imported from `@andi/powers`.
import { DataPowers, EmailBody, EmailOptions, EmailPowers } from "@andi/powers";
The Email Power is executed using the `send` function that takes in two parameters.
Email.Powers.send(content: EmailBody, options: EmailOptions): Promise
Email Body
- `htmlBody: string` : HTML text or Handlebars template. If template is provided, then it will be evaluated with `htmlData` to produce HTML content.
- `htmlData?: any` : (Optional) HTML data for Handlebars template.
- `template?: "default" | "none"` : (Optional) Default HTML Email template will be used if no value is provided, and the body section will be replaced with `htmlBody` content. If you do not wish to use wrapper template then use `none` value for `template`.
Email Options
- subject: string : Email subject.
- to: string: Recipient email addresses. Max 50.
- attachments?: Attachment[] : (Optional) List of attachments. Total size of attachments should not exceed 8 MB
- cc?: string[ ]: (Optional) CC recipient email addresses. Max 50.
- bcc?: string[ ]: (Optional) BCC recipient email addresses. Max 50
- replyTo?: string` : (Optional) Reply To override email address. If none is provided, replies will go to an unmonitored inbox.
- trackLinks?: boolean : (Optional) Activate link tracking for links in the Email body. Defaults to `false`.
- trackOpens?: boolean : (Optional) Activate open tracking for this email. Defaults to `false`.
Email Attachment
- name: string: Name of the attachment, should be in a filename format with extension indicating content type.
- Forbidden File Types: vbs, exe, bin, bat, chm, com, cpl, crt, hlp, hta, inf, ins, isp, jse, lnk, mdb, pcd, pif, reg, scr, sct, shs, vbe, vba, wsf, wsh, wsl, msc, msi, msp, mst.
- content: Buffer | string: Attachment content that can be either buffer or string.
- contentId?: string: (Optional) The content ID of the email attachment. This allows images to be embedded within the HTML document.
Email Limitations
- Email size cannot be larger than 8 Megabytes.
- Email will not be sent if that feature is turned off.
- Email will not be sent if the domain of the email address is included in the domain whitelist when specified.
- To total number of email recipient addresses cannot be greater than 50.
Template Case Example
How do we build a Monday morning email report?
Scenario
Management wants a weekly email that shows the list of Opportunities saved within the last week with details like - Name, URL and Total Loan Amount to be sent to a handful number of users.
Solution
As a skill writer we can leverage "Andi Skill Builder" to build a skill to accomplish the above scenario. To get all the Opportunities saved in the past week we will have to leverage Andi events since there is no specific API with the "PrecisionLender" application to get that information. In the skill we will capture the important information on each 'opportunitySavedClick' event using 'DataPowers'. Then will use the Andi cron event 'WeeklySundayMidnightEvent' as a trigger to get all the data captured during the data capture phase of the skill and package it and send it using the new EmailPowers.
The first step will be to define the 'shouldIRun' portion of the skill code with a concentration solely on 'opportunitySavedClick' event which is simple with some help from Andi powers as demonstrated below.
ShouldIRun.ts - Phase 1
import { ISkillContext, ShouldIRunResponseType } from "andiskills";
export async function shouldIRun(skillContext: ISkillContext): Promise {
// we want to listen on OpportunitySavedClickEvent to collect data
if (skillContext.powers.precisionLender.opportunity.isOpportunitySavedClickEvent())
return skillContext.powers.andi.shouldIRun.shouldIRunTrue();
return skillContext.powers.andi.shouldIRun.shouldIRunFalse();
}
After defining the initial 'shouldIRun' then it necessary to define a type that will be used in unison with 'DataPowers' to store the relevant data for the email phase of the skill. This will be done in the `run` portion of the skill.
Run.ts - OpportunityInfo type definition
import { ISkillActivity, ISkillContext } from "andiskills";
type OpportunityInfo = {
name: string;
url: string;
totalLoanAmount: number;
updateTimestamp: number;
}
With the preliminary pieces in place let's look at where data can be extracted from the event. The first place to start is opening the developer console with an Opportunity open in the PrecisionLender application and navigate to the `Network` tab within the console then click the Save button.
As demonstrated above find the event 'opportunitySavedClick' in the 'events' payload. From this event we find the needed properties.
Some of the other required data is located under 'financialStatement' property.
Once we know the path to the data on the event, we can update the 'run' code to include a mapper function that will take the data from the event and create an object with the type definition 'OpportunityInfo' we previously defined.
Run.ts - With Updated Event Mapper
import { DataPowers, EmailBody, EmailOptions, EmailPowers } from "@andi/powers"import { ISkillActivity, ISkillContext } from "andiskills";
type OpportunityInfo = {
name: string;
url: string;
totalLoanAmount: number;
updateTimestamp: number;
}
// method to extract relevant information from the
export function getOpportunityInfoFromEvent(skillContext: ISkillContext): OpportunityInfo {
const applicationEvent = skillContext.powers.andi.event.getApplicationEvent();
const applicationEventData = applicationEvent.applicationEventData;
const opportunityData = applicationEventData.opportunityData;
const opportunityInfo: OpportunityInfo = {
name: opportunityData?.name,
url: applicationEvent.eventMetaData.currentUrl,
totalLoanAmount: opportunityData?.financialStatement?.loanAmount,
updateTimestamp: Date.now()
};
return opportunityInfo;
};
The final part of this phase of the skill is create the 'run' function which will be responsible for capturing the data from the event with the mapper function and store with 'DataPowers' to be consumed latter during the email generation phase of the skill.
Run.ts - Run Function Capturing Event Data
import { DataPowers } from "@andi/powers";
import { ISkillActivity, ISkillContext } from "andiskills";
type OpportunityInfo = {
name: string;
url: string;
totalLoanAmount: number;
updateTimestamp: number;
}
// method to extract relevant information from the
export function getOpportunityInfoFromEvent(skillContext: ISkillContext): OpportunityInfo {
const applicationEvent = skillContext.powers.andi.event.getApplicationEvent();
const applicationEventData = applicationEvent.applicationEventData;
const opportunityData = applicationEventData.opportunityData;
const opportunityInfo: OpportunityInfo = {
name: opportunityData?.name,
url: applicationEvent.eventMetaData.currentUrl,
totalLoanAmount: opportunityData?.financialStatement?.loanAmount,
updateTimestamp: Date.now()
};
return opportunityInfo;
}
async function storeOpportunityInfo(dataKey: string, opportunityInfo: OpportunityInfo): Promise {
// use DataPowers to load existing data, if it's
// undefined then initialize it to an empty array
let opportunityInfoList: OpportunityInfo[] = (await DataPowers.get<OpportunityInfo[]>(dataKey)) ?? [];
opportunityInfoList.push(opportunityInfo);
// filter out older duplicate opportunities
const latestOpportunities = opportunityInfoList
.reduce((results, currentOpportunity) => {
// see if the current Opportunity exists
const found = results.find((item, index) => {
if (item.url === currentOpportunity.url &&
item.updateTimestamp < currentOpportunity.updateTimestamp) {
// replace older opportunity with latest
results[index] = currentOpportunity;
return currentOpportunity;
}
});
// if no match found then add to results
if (!found) results.push(currentOpportunity);
return results;
}, [] as OpportunityInfo[]);
// remove any older unwanted data so that our array
// is limited in size for performance
const filterDate = new Date();
filterDate.setMonth(filterDate.getMonth() - 1);
const finalListOfOpportunities = latestOpportunities
.filter((opp) => opp.updateTimestamp > filterDate.getTime());
// update this array in data store
await DataPowers.set(dataKey, finalListOfOpportunities);
return opportunityInfoList.length;
}
export async function run(skillContext: ISkillContext) : Promise{
// name for storing data, we will use this key
// name for retrieving data while building report later
const dataKey = `Email-Report-OppSaved-Weekly`;
const precisionLenderPowers = skillContext.powers.precisionLender;
const opportunityPowers = precisionLenderPowers.opportunity;
const andiPowers = skillContext.powers.andi;
const chatPowers = andiPowers.chat;
// logic to accumulate data for report on
// `OpportunitySavedClickEvent` unique key
const opportunityInfo = getOpportunityInfoFromEvent(skillContext);
const numberOfOpportunities = await storeOpportunityInfo(dataKey, opportunityInfo);
return chatPowers.sendSimpleHeader(`opportunityInfoList length: ${numberOfOpportunities}`);
}
At this point the skill will store all the data on each 'opportunitySavedClick' event and now in the next phase the skill will be enhance further to send out the email on the cron event 'WeeklySundayMidnightEvent'. The first step of this phase it to update the 'shouldIRun' to trigger a run for that event.
ShouldIRun.ts - with new cron event logic
import { ISkillContext, ShouldIRunResponseType } from "andiskills";
export async function shouldIRun(skillContext: ISkillContext):
Promise {
// we want to listen on OpportunitySavedClickEvent to collect data
if (skillContext.powers.precisionLender.opportunity.isOpportunitySavedClickEvent())
return skillContext.powers.andi.shouldIRun.shouldIRunTrue();
// for generating email report, we will listen on
// `WeeklySundayMidnightEvent` cron event
if (skillContext.powers.andi.event.checkWeeklySundayMidnightEvent())
return skillContext.powers.andi.shouldIRun.shouldIRunTrue();
return skillContext.powers.andi.shouldIRun.shouldIRunFalse();
}
Finally the 'Run.ts' gets updated to pull the data from storage on the cron event and send the email.
Run.ts - handling cron event and send email
Available in Contexts
With the 'run' and 'shouldIRun' completed there exists one last detail that needs to be configured in the Andi Skills Builder to ensure that not only the 'opportunitySavedClick' event gets trigger, but the 'WeeklySundayMidnightEvent' does as well. Since the 'WeeklySundayMidnightEvent' event lives outside of the `PrecisionLender-Opportunity` context in the 'andiEvent-andiCronJob' this will need to be manually configured.
NOTE: In most circumstances this is predetermined by the skill template.
The example above ensure that both contexts are listed in order:
- PrecisionLender-Opportunity
- andiEvent-andiCronJob
Resulting Email
Once the skill is deployed to the appropriate audience and several Opportunities are saved, an email will be sent to recipients on Monday morning.
Potential Enhancements
This case example can be further enhanced to make it more customized by include using custom templates or standard email bodies be pulled from storage with the new data powers. That option allows for design to be done with external tools and then upload those designs to the organization storage where it can be pull in by the skill.
Configuration could also be added to make it possible to specify email addresses or perhaps certain details that were not currently capture like opportunity region. That could be combined with audiences so that only certain regions would go to certain addressees.
Finally, EmailPowers does support sending attachments which can be used to include static content through data powers or embedded content directly from the skill code.
Attachments Example
Attachments can also be used to include embedded images in the body of the email using `field`. Below is an example of the an HTML using `books.png`.