Sending emails from Microsoft Dynamics 365 / Dataverse using Email Templates is a very common requirement.
However, real-world scenarios are rarely simple:
Multiple TO and CC recipients
Mix of internal users and external email addresses
Recipients driven by configuration, not hardcoded values
The need to review the email before sending
This article presents a production-proven, client-side pattern to create an Email activity from a template, keeping the implementation practical, explicit, and readable.
The code shown below is based on real production logic. Names, entities, and addresses have been anonymized, but the structure and flow are intentionally unchanged.
Why Client-Side?
This approach is ideal when:
The action is triggered by a command bar button or form event
The user must see and optionally edit the email before sending
Email recipients depend on current form data
For fully automated emails, a server-side solution (plugin or Power Automate) may be more appropriate.
High-Level Flow
Validate form state (no unsaved changes)
Check mandatory business conditions
Retrieve the Email Template by name
Instantiate the template against the current record
Load TO / CC addresses from configuration
Resolve recipients to system users when possible
Build Activity Parties (FROM / TO / CC)
Create the Email activity
Open the Email form for review
Recipient Resolution Strategy
One of the most important parts of the pattern is how recipients are handled.
For each configured email address:
If a matching
systemuser.internalemailaddressexists → bind the userOtherwise → use
addressused
This guarantees that:
Internal users appear correctly in Dynamics
External recipients are still supported
The same logic works across environments
The pattern also supports:
Multiple TO recipients
Multiple CC recipients
Automatic inclusion of the current user
Optional inclusion of an administrator or supervisor
Activity Party Masks Recap
| Sender (FROM) | 1 |
| Recipient (TO) | 2 |
| CC | 3 |
| BCC | 4 |
Correct usage of these values is essential when creating Email activities programmatically.
Configuration-Driven Design
Instead of hardcoding email addresses, this pattern reads them from a configuration table.
Benefits:
No deployments required for recipient changes
Easy maintenance by administrators
Safer promotion across DEV / TEST / PROD
Each configuration record simply contains a semicolon-separated list of email addresses.
Error Handling and User Experience
The implementation intentionally:
Stops execution early when prerequisites are not met
Provides clear feedback to the user
Avoids partially created Email records
This makes the behavior predictable and user-friendly.
This pattern is not about writing the shortest possible JavaScript.
It is about writing explicit, maintainable, and production-ready code that:
Mirrors real business requirements
Is easy to debug
Can be reused across multiple entities and scenarios
If you work frequently with Dynamics 365 Email Templates, this approach provides a solid and battle-tested foundation.
GenericDocumentCheck: async function (formContext) {
// =====================
// 1. Guard clauses
// =====================
if (formContext.data.entity.getIsDirty()) {
Xrm.Navigation.openAlertDialog({ text: "Please save the record before proceeding." });
return;
}
formContext.ui.clearFormNotification("MissingMandatoryField");
var mandatoryValue = formContext.getAttribute("custom_mandatoryfield")?.getValue();
if ((!mandatoryValue || mandatoryValue === "") && formContext.ui.getFormType() !== 1) {
formContext.ui.setFormNotification(
"Mandatory field is missing.",
"ERROR",
"MissingMandatoryField"
);
return;
}
// =====================
// 2. Template retrieval
// =====================
var recordId = formContext.data.entity.getId().replace(/[{}]/g, "");
var emailTemplateName = "Generic Request Template";
var templateFetch = "?fetchXml=" + encodeURIComponent(`
<fetch top="1">
<entity name="template">
<attribute name="templateid" />
<filter>
<condition attribute="title" operator="eq" value="${emailTemplateName}" />
</filter>
</entity>
</fetch>
`);
var templateResult = await Xrm.WebApi.retrieveMultipleRecords("template", templateFetch);
if (!templateResult.entities.length) {
throw new Error("Email template not found: " + emailTemplateName);
}
var templateId = templateResult.entities[0].templateid;
// =====================
// 3. Instantiate template
// =====================
var instantiateRequest = {
TemplateId: { guid: templateId },
ObjectType: "account",
ObjectId: { guid: recordId },
getMetadata: function () {
return {
boundParameter: null,
parameterTypes: {
TemplateId: { typeName: "Edm.Guid", structuralProperty: 1 },
ObjectType: { typeName: "Edm.String", structuralProperty: 1 },
ObjectId: { typeName: "Edm.Guid", structuralProperty: 1 }
},
operationType: 0,
operationName: "InstantiateTemplate"
};
}
};
var response = await Xrm.WebApi.execute(instantiateRequest);
if (!response.ok) {
throw new Error("Error during InstantiateTemplate execution.");
}
var responseBody = await response.json();
var emailSubject = responseBody.value[0].subject;
var emailDescription = responseBody.value[0].description;
// =====================
// 4. Read email addresses from configuration
// =====================
var getAddressesByConfigName = async (name) => {
var fetchXml = `<fetch>
<entity name="email_configuration">
<attribute name="value" />
<filter>
<condition attribute="name" operator="eq" value="${name}" />
</filter>
</entity>
</fetch>`;
var result = await Xrm.WebApi.retrieveMultipleRecords(
"email_configuration",
"?fetchXml=" + encodeURIComponent(fetchXml)
);
if (result?.entities?.length > 0) {
return result.entities[0].value.split(';');
} else {
throw new Error("Email configuration not found: " + name);
}
};
var toAddresses = await getAddressesByConfigName("EMAIL_TO");
var ccAddresses = await getAddressesByConfigName("EMAIL_CC");
// =====================
// 5. Build activity parties
// =====================
var activityParties = [];
// ---------- FROM (Sender) ----------
var fetchFromUser = "?fetchXml=" + encodeURIComponent(`
<fetch top="1">
<entity name="systemuser">
<attribute name="systemuserid" />
<filter>
<condition attribute="internalemailaddress" operator="eq"
value="noreply@company.com" />
</filter>
</entity>
</fetch>
`);
var fromResult = await Xrm.WebApi.retrieveMultipleRecords("systemuser", fetchFromUser);
var senderUserId = fromResult.entities.length
? fromResult.entities[0].systemuserid
: null;
activityParties.push({
participationtypemask: 1,
"partyid_systemuser@odata.bind": `/systemusers(${senderUserId})`
});
// ---------- TO (Recipients) ----------
for (var i = 0; i < toAddresses.length; i++) {
var address = toAddresses[i];
var fetchToUser = "?fetchXml=" + encodeURIComponent(`
<fetch top="1">
<entity name="systemuser">
<attribute name="systemuserid" />
<filter>
<condition attribute="internalemailaddress" operator="eq"
value="${address}" />
</filter>
</entity>
</fetch>
`);
var toResult = await Xrm.WebApi.retrieveMultipleRecords("systemuser", fetchToUser);
var partyTo = { participationtypemask: 2 };
if (toResult.entities.length) {
partyTo["partyid_systemuser@odata.bind"] =
`/systemusers(${toResult.entities[0].systemuserid})`;
} else {
partyTo["addressused"] = address;
}
activityParties.push(partyTo);
}
// ---------- CC (Recipients) ----------
for (var j = 0; j < ccAddresses.length; j++) {
var address = ccAddresses[j];
var fetchCcUser = "?fetchXml=" + encodeURIComponent(`
<fetch top="1">
<entity name="systemuser">
<attribute name="systemuserid" />
<filter>
<condition attribute="internalemailaddress" operator="eq"
value="${address}" />
</filter>
</entity>
</fetch>
`);
var ccResult = await Xrm.WebApi.retrieveMultipleRecords("systemuser", fetchCcUser);
var partyCc = { participationtypemask: 3 };
if (ccResult.entities.length) {
partyCc["partyid_systemuser@odata.bind"] =
`/systemusers(${ccResult.entities[0].systemuserid})`;
} else {
partyCc["addressused"] = address;
}
activityParties.push(partyCc);
}
// ---------- Current user in CC ----------
var globalContext = Xrm.Utility.getGlobalContext();
var currentUserId = globalContext.userSettings.userId.replace(/[{}]/g, "");
activityParties.push({
participationtypemask: 3,
"partyid_systemuser@odata.bind": `/systemusers(${currentUserId})`
});
// ---------- Optional admin user ----------
var adminUserId = CustomUtility.getAdminUserId?.();
if (adminUserId) {
activityParties.push({
participationtypemask: 3,
"partyid_systemuser@odata.bind": `/systemusers(${adminUserId})`
});
}
// =====================
// 6. Create email
// =====================
var email = {
subject: emailSubject,
description: emailDescription,
"regardingobjectid_account@odata.bind": `/accounts(${recordId})`,
email_activity_parties: activityParties
};
Xrm.WebApi.createRecord("email", email).then(
function success(result) {
Xrm.Navigation.openForm({
entityName: "email",
entityId: result.id
});
},
function error(e) {
console.error(e.message);
}
);
}