In Microsoft Dynamics 365 for Finance and Supply Chain, the system sends out emails when certain transactional events occur. For example, when a sales order is placed, shipped, or read for pick up. Businesses will often have custom fields that require them to extend a D365 email template. In this article, learn how to extend a D365 email template to use custom data and placeholders in emails sent to customers.
Background
Before you extend a D365 email template, there are a couple of things you need to understand first.
First, set up an email provider to send emails.
Second, set up an email template.
After you set up the email template, you may discover that you want to show additional pieces of data in the email. The next step is to modify the code to relate the placeholder value in the email template to that data.
Sales Order Example
The easiest way to understand how to extend a D365 email template is with an example. There are different steps for extending an email based off an order, compared to other types of emails. We will start with a sales order email.
Setup Email Template
To start, create an email template that includes the new placeholders you want to replace with data from the order. To allow you to follow along, create and run this runnable class (job). This will create an email template named ‘SalesOrder‘.
class tutorial_CreateEmailTemplateSalesOrder
{
public static void main(Args _args)
{
SysEmailTable sysEmailTable;
SysEmailId emailID = "SalesOrder"; //Usually this would come from a parameter.
LanguageId languageID = "en-us"; //Usually this would come from a parameter.
sysEmailTable = SysEmailTable::find(emailID);
if (!sysEmailTable)
{
sysEmailTable.EmailId = emailID;
sysEmailTable.Description = "Sales order sample email template";
sysEmailTable.DefaultLanguage = "en-us";
sysEmailTable.Priority = eMailPriority::Normal;
sysEmailTable.SenderAddr = "april@contosoax7.onmicrosoft.com";
sysEmailTable.SenderName = "Dynamics 365 Musings Orders";
sysEmailTable.BatchGroupId = "";
sysEmailTable.insert();
Info(strfmt("Added sysEmailTable record with EmailID %1",emailID));
}
SysEmailMessageTable sysEmailMessageTable = SysEmailMessageTable::find(emailID, languageID);
if (!sysEmailMessageTable)
{
sysEmailMessageTable.EmailId = emailID;
sysEmailMessageTable.LanguageId = languageID;
sysEmailMessageTable.LayoutType = SysEmailLayoutType::StaticLayout;
sysEmailMessageTable.Subject = "Sales order %salesId%";
str mailBody = @'
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "https://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css" nonce="">
body,
td,
div,
p,
a,
input {
font-family: arial, sans-serif;
}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Your Fabrikam order details</title>
<style type="text/css" nonce="">
body,
td {
font-size: 10px;
font-family: "Segoe UI", "Segoe", "Tahoma", "Verdana", "Arial", "sans-serif";
margin: 0px;
border: 0px;
}
a:link,
a:active {
color: #1155cc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
cursor: pointer;
}
a:visited {
color: #6611cc;
}
img {
border: 0px;
}
.logo {
left: -7px;
position: relative;
}
.label {
font-weight: bolder;
}
.totalslabel {
text-align: right;
padding-right: 20px;
}
.headerprops {
margin-bottom: 4px;
}
.linevalue {
text-align: right;
}
</style>
</head>
<body style="background-color: #eeeeee; color: #1a1a1a">
<table
style="margin-top: 20px; padding: 0px 20px 20px 20px; width: 640px; background-color: #fff"
align="center"
>
<tbody>
<tr>
<td>
<table style="width: 100%; margin-bottom: 10px">
<tr>
<td style="text-align: right">Order: %salesid%</td>
</tr>
<tr>
<td style="text-align: center; padding-top: 10px">
<!-- Logo -->
<a href="https://sales1.commerce.dynamics.com/"
><img
width="180"
style="border-style: none; outline: none"
src="https://images-us-prod.cms.commerce.dynamics.com/cms/api/dbsbfgcfvq/imageFileData/MAaq0?pubver=1"
alt="Fabrikam Online logo"
/></a>
</td>
</tr>
<tr>
<td style="text-align: center">
<h1 style="font-family: "Segoe UI light"; margin-bottom: 10px">
<!-- Subject -->
Thank you for your order!
</h1>
</td>
</tr>
<tr>
<td style="text-align: center; padding: 0px 100px 10px 100px">
<!-- Instructions -->
Hello %customername%. We received your order and we are working on it now. We will
email you an update as soon as your order is processed.
</td>
</tr>
<tr>
<td style="text-align: center" align="center">
<table style="width: auto" align="center">
<tbody>
<tr>
<td
style="
padding: 0 25px;
vertical-align: middle;
background-color: #4c833a;
color: #fff;
height: 30px;
text-align: center;
"
>
<!-- CTA -->
<a
style="
color: #fff;
text-decoration: none;
background-color: #4c833a;
box-sizing: border-box;
display: inline-block;
font-size: 16px;
height: 30px;
line-height: 30px;
vertical-align: middle;
"
href="https://sales1.commerce.dynamics.com/orderdetails?confirmationId=%orderconfirmationid%&propertyName=email&propertyValue=%customeremailaddress%"
target="_blank"
>View my order status</a
>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table style="width: 100%; margin-bottom: 10px">
<tr>
<td
colspan="2"
style="
padding: 2px 5px 2px 5px;
margin-bottom: 10px;
background-color: lightblue;
font-size: larger;
"
>
Your order
</td>
</tr>
<!-- Order parameters -->
<tr>
<td width="50%" style="vertical-align: top; padding: 5px 0 0 5px">
<div class="headerprops">
<span class="label">Order number</span>:
<a
style="color: #1570a6; text-decoration: none"
href="https://sales1.commerce.dynamics.com/orderdetails?salesid=%salesID%"
target="_blank"
>%orderconfirmationid%</a
>
</div>
<div class="headerprops">
<span class="label">Order date</span>: %shipdate%<br />
</div>
<div class="headerprops">
<span class="label">Mode of delivery</span>: %modeofdelivery%<br />
</div>
<div class="headerprops">
<span class="label">Tutorial Custom Field</span>: %TutorialField%<br/>
</div>
</td>
<td width="50%" style="vertical-align: top; padding: 5px 0 0 5px">
<div class="headerprops">
<span class="label">Delivery address</span>:<br />
%customername%<br />
%deliveryaddress%
</div>
</td>
</tr>
</table>
<table style="width: 100%; margin-bottom: 10px">
<!-- Product list -->
<tr>
<td
colspan="5"
style="
padding: 2px 5px 2px 5px;
margin-bottom: 10px;
background-color: lightblue;
font-size: larger;
"
>
Items
</td>
</tr>
<!--%tablebegin.salesline% -->
<tr>
<td style="text-align: left; padding-left: 5px">
<a href="https://sales1.commerce.dynamics.com/%productid%.p"
><img
style="float: left"
src="https://cms-ppe-imageresizer-mr.trafficmanager.net/cms/api/gfhwnkhdlh/imageFileData/search?fileName=/Products/%lineitemid%_000_001.png&w=125&h=125&q=80&m=6&f=jpg&cropfocalregion=true"
alt="%lineproductname%"
/></a>
</td>
<td>
<div style="margin-bottom: 3px">
<a href="https://sales1.commerce.dynamics.com/%productid%.p"
><span style="font-size: larger; color: #1570a6">%lineproductname%</span></a
>
</div>
<div style="margin-bottom: 12px">
<span style="font-weight: bold">Item number:</span> %lineitemid%
</div>
<table style="padding: 0 15px 0 0; text-align: left">
<tr style="font-weight: bold">
<td>Item price:</td>
<td>Qty:</td>
<td>Discount:</td>
<td>Total:</td>
<td>Custom Field:</td>
</tr>
<tr>
<td>%lineprice%</td>
<td>%linequantity_withoutunit%</td>
<td>%linediscount%</td>
<td>%linenetamount%</td>
<td>%TutorialLineField%</td>
</tr>
</table>
</td>
</tr>
<!--%tableend.salesline%-->
</table>
<!-- Order summary -->
<table width="30%" align="right" style="margin-bottom: 30px">
<tbody>
<tr>
<td class="totalslabel">Subtotal</td>
<td style="text-align: right" width="60">%ordernetamount%</td>
</tr>
<tr>
<td class="totalslabel">Discount</td>
<td style="text-align: right">%discount%</td>
</tr>
<tr>
<td class="totalslabel">Sales Tax</td>
<td style="text-align: right">%tax%</td>
</tr>
<tr>
<td class="totalslabel"><b>Total</b></td>
<td style="text-align: right"><b>%total%</b></td>
</tr>
</tbody>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td>
<table style="background-color: #eee; padding: 0px 5px">
<tr>
<td>
This email was sent to %customeremailaddress%. Fabrikam respects your privacy. See
our online
<a style="color: #1570a6" href="https://aka.ms/commerceemailsetup" target="_blank"
>Privacy Statement</a
>.<br /><br />
Fabrikam Corporation, One Fabrikam Way, Redmond, WA, 98052, USA
</td>
</tr>
<tr>
<td style="font-size: smaller">
Notification type: <b>Order created</b><br />
Mode of delivery: <b>default</b><br />
Order confirmation ID: <b>%orderconfirmationid%</b>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
';
sysEmailMessageTable.Mail = mailBody;
sysEmailMessageTable.insert();
Info(strfmt("Added sysEmailMessageTable record with EmailID %1",emailID));
}
}
}
Note, this is essentially a copy of the ‘New_dflt‘ email template in the Microsoft demo data. However, I made two changes to it.
First, I added the placeholder ‘%TutorialField%‘ in the first column of the top table. See the bold text. Here, we intend to have the system replace the placeholder for data from the sales order header.

Second, I added a placeholder ‘%TutorialLineField%’ in the lines section of the email template. Here, we intend to have the system replace this placeholder with data from each sales line.

Configure Email Notification Profile
After creating an email template with the new placeholders, we need to tell the system to use the email template with an event.
First, go to Retail and Commerce>Headquarters setup>Parameters>Commerce parameters. Review and set if needed, the value in the ‘Email notification profile’ field.

Second, navigate to Retail and commerce>Headquarters setup>Commerce email notification profile. Next, select the profile from the previous step.
Second, select the profile that is specified in the Commerce Parameters form,
Third, locate the line in the grid with the ‘Email notification type‘ set to ‘Order created‘. Create it if it does not exist.
Finally, set the ‘Email ID’ value in this row to the new email template. For this example, set this to ‘SalesOrder‘.

To confirm the email profile was created correctly, click on the ‘SalesOrder‘ email ID value. This will drill into the ‘Organizational email templates‘ form.
Click on the ‘Email message’ button to view a preview of the order template. Confirm you see the new placeholders added.


Extend The Email Template Code
Finally, the next step to extend a D365 email template is to create a Chain of Command class that extends RetailOENInfo. This code will replace and populate the newly added placeholders.
First, in Visual Studio, create a new class named ‘tutorialRetailOENInfo_Class_Extension’.
Second, add the attribute ‘[ExtensionOf(classStr(RetailOENInfo))]’ above the class declaration.
Third, add the keyword word ‘final‘ in front of the word ‘class’. This is a requirement of Chain of Command.
Fourth, add Macros for each of the new custom placeholders you have in the email template. See this example, along with several other placeholders.
[ExtensionOf(classStr(RetailOENInfo))]
final class tutorialRetailOENInfo_Class_Extension
{
#define.TutorialField('TutorialField')
#define.TutorialLineField('TutorialLineField')
}
Fourth, add the method initParameterMap. Then, for each placeholder, insert a name/value into the map named ‘parameterMap‘. This is a map defined in the parent class. Set the name to the placeholder name defined in the macro. Then, specify the second parameter as the value you wish to dynamically use in place of that placeholder in the email template.
protected void initParameterMap()
{
next initParameterMap();
//Add your placeholder code for the sales order header here.
parameterMap.insert(#TutorialField, salesOrder.TutorialField);
}
Fifth, add the method createSalesLineMap. Similarly, for each placeholder that read from sales line data, insert name/value pairs into the salesLineMap
protected Map createSalesLineMap(SalesLine salesLine, RetailGiftCardTransactions retailGiftCardTransactions,MCRSalesLine mcrSalesLine)
{
Map saleslineMap = next createSalesLineMap(salesLine, retailGiftCardTransactions, mcrSalesLine);
//Add your placeholder code for the sales order lines here
saleslineMap.insert(#TutorialLineField, salesLine.TutorialLineField);
return saleslineMap;
}
As shown above, use this process to extend a d365 email template and display new custom fields.
Display Existing Fields And Methods
It may be obvious, but this same process can be used to add placeholders that will be replaced by data from any field or method related to the sales order.
In the example below, notice the multiple added placeholders. See how they are set using a mixture of existing fields and new methods to retrieve their values.
[ExtensionOf(classStr(RetailOENInfo))]
final class tutorialRetailOENInfo_Class_Extension
{
#define.CustomerPhone('CustomerPhone')
#define.TrackingNumber('trackingnumber')
#define.TrackingInfo('trackinginfo')
#define.TrackingURL('trackingurl')
#define.CreatedDate('createddate')
#define.TutorialField('TutorialField')
#define.DeliveryNameLine('deliverynameline')
#define.TutorialLineField('TutorialLineField')
protected void initParameterMap()
{
next initParameterMap();
CustTable custTable = CustTable::find(salesOrder.CustAccount);
parameterMap.insert(#CustomerPhone, custTable.phone());
parameterMap.insert(#TrackingNumber, this.getTrackingNumber(salesOrder));
parameterMap.insert(#TrackingInfo, this.getTrackingInfo(salesOrder));
parameterMap.insert(#TrackingURL, this.getTrackingUrl(salesOrder));
parameterMap.insert(#CreatedDate, RetailENInfo::formatDatetimeData(DateTimeUtil::date(salesOrder.CreatedDateTime), languageId));
parameterMap.insert(#TutorialField, salesOrder.TutorialField);
}
protected Map createSalesLineMap(SalesLine salesLine, RetailGiftCardTransactions retailGiftCardTransactions,MCRSalesLine mcrSalesLine)
{
Map saleslineMap = next createSalesLineMap(salesLine, retailGiftCardTransactions, mcrSalesLine);
saleslineMap.insert(#DeliveryNameLine, salesLine.DeliveryName);
saleslineMap.insert(#TutorialLineField, salesLine.TutorialLineField);
return saleslineMap;
}
public str getTrackingNumber(SalesTable _salesTable)
{
SalesPackingSlipTrackingInformation trackingInformation;
CustPackingSlipJour custPackingSlipJour;
str trackingNumber;
while select RecId, PackingSlipId, DeliveryDate
from custPackingSlipJour
Where custPackingSlipJour.SalesId == _salesTable.SalesId
join TrackingNumber
from trackingInformation
where trackingInformation.SalesId == _salesTable.SalesId
&& trackingInformation.PackingSlipId == custPackingSlipJour.PackingSlipId
&& trackingInformation.DeliveryDate == custPackingSlipJour.DeliveryDate
{
if (!trackingNumber)
{
trackingNumber = trackingInformation.TrackingNumber;
}
else
{
trackingNumber += ', ' + trackingInformation.TrackingNumber;
}
}
return trackingNumber;
}
public str getTrackingUrl(SalesTable _salesTable)
{
SalesPackingSlipTrackingInformation trackingInformation;
CustPackingSlipJour custPackingSlipJour;
str trackingUrl;
while select RecId, PackingSlipId, DeliveryDate
from custPackingSlipJour
Where custPackingSlipJour.SalesId == _salesTable.SalesId
join TrackingURL
from trackingInformation
where trackingInformation.SalesId == _salesTable.SalesId
&& trackingInformation.PackingSlipId == custPackingSlipJour.PackingSlipId
&& trackingInformation.DeliveryDate == custPackingSlipJour.DeliveryDate
{
if (!trackingUrl)
{
trackingUrl = trackingInformation.TrackingURL;
}
else
{
trackingUrl += ', ' + trackingInformation.TrackingURL;
}
}
return trackingUrl;
}
public str getTrackingInfo(SalesTable _salesTable)
{
SalesPackingSlipTrackingInformation trackingInformation;
CustPackingSlipJour custPackingSlipJour;
str trackingInfo = '<table style="margin: 0 20px 15px;">';
while select RecId, PackingSlipId, DeliveryDate
from custPackingSlipJour
Where custPackingSlipJour.SalesId == _salesTable.SalesId
join TrackingNumber, TrackingURL
from trackingInformation
where trackingInformation.SalesId == _salesTable.SalesId
&& trackingInformation.PackingSlipId == custPackingSlipJour.PackingSlipId
&& trackingInformation.DeliveryDate == custPackingSlipJour.DeliveryDate
{
trackingInfo = trackingInfo
+ '<tr> '
+ '<td style="padding-right: 50px;">' + trackingInformation.TrackingNumber + '</td> '
+ '<td>' + trackingInformation.TrackingURL + '</td> '
+ '</tr>';
}
trackingInfo = trackingInfo + '</table>';
return trackingInfo;
}
}
Testing
To test and see if your changes to extend a D365 email template worked, perform the event associated with your email template. In the example above, we associated the email template with creating a sales order.
Importantly, when creating the sales order, ensure the sales order is a call center sales order. See my article on how to Create A Call Center In D365.
Also, on the sales order, on the header tab, set the ‘Email‘ to the email address of an address you have access to.

After creating the sales order, run the batch job Retail and Commerce>Retail and Commerce IT>Email and notifications>Send email notification, or wait for it to run on recurrence.

Finally, check your email. Notice that the new placeholders are filled in.


Microsoft Documentation
While I did not find specific documentation on adding additional placeholders, this documentation includes some information about the existing placeholders.
Conclusion
While the most common fields already have placeholders for emails, businesses will likely want to add more. For example, tracking number information is not included in the base functionality. Therefore, it is very helpful to understand how to extend a D365 email template to include custom placeholders. These placeholders can be linked to database data. This allows for email messages that are accurate, timely, and informative.
Great post, very informative! I learned something new today. Looking forward to reading more from you. (ref:f7c76ea6dc0b)