" MicromOne: Sending Emails from Templates in Dynamics 365: A Real-World Client-Side Pattern

Pagine

Sending Emails from Templates in Dynamics 365: A Real-World Client-Side Pattern

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

  1. Validate form state (no unsaved changes)

  2. Check mandatory business conditions

  3. Retrieve the Email Template by name

  4. Instantiate the template against the current record

  5. Load TO / CC addresses from configuration

  6. Resolve recipients to system users when possible

  7. Build Activity Parties (FROM / TO / CC)

  8. Create the Email activity

  9. 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.internalemailaddress exists → bind the user

  • Otherwise → 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
CC3
BCC4


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);

        }

    );

}