In Microsoft Dynamics 365 / Dataverse, email templates are often
static and difficult to adapt to different business scenarios.
A common requirement is to dynamically inject data such as:
Recipient names
Related record information
Conditional fields (shown only when data exists)
This article presents a PreCreate Email plugin that replaces
placeholders inside the email body before the Email record is created,
using clean and reusable logic.
Plugin Overview
Execution details
Entity: email
Message: Create
Stage: Pre-Operation (20)
Purpose: Replace placeholders in email.description
Supported placeholders
PlaceholderSource
##ToRecipient##Names of recipients (To field)
##account.custom_taxcode##Account custom field
##opportunity.custom_recordurl##Opportunity record URL
Complete Plugin Code (Generic & Safe for Sharing)
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Text.RegularExpressions;
namespace Plugins.Email
{
public class PreCreateEmailReplacePlaceholders : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var serviceUser = factory.CreateOrganizationService(context.InitiatingUserId);
var serviceAdmin = factory.CreateOrganizationService(null);
if (context.Stage != 20 ||
context.MessageName != "Create" ||
context.PrimaryEntityName != "email")
{
throw new InvalidPluginExecutionException("Plugin not registered correctly.");
}
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity target)
{
ExecuteLogic(serviceAdmin, serviceUser, target, tracing);
}
}
private void ExecuteLogic(
IOrganizationService serviceAdmin,
IOrganizationService serviceUser,
Entity target,
ITracingService tracing)
{
tracing?.Trace("START PreCreateEmailReplacePlaceholders");
try
{
if (!target.Contains("description"))
return;
var description = target.GetAttributeValue<string>("description");
#region TO RECIPIENTS
if (target.Contains("to"))
{
var parties = target.GetAttributeValue<EntityCollection>("to")?.Entities;
var recipientNames = string.Empty;
foreach (var party in parties)
{
if (!party.Contains("partyid")) continue;
var reference = party.GetAttributeValue<EntityReference>("partyid");
if (reference == null) continue;
string primaryName;
switch (reference.LogicalName)
{
case "account": primaryName = "name"; break;
case "contact": primaryName = "fullname"; break;
case "systemuser": primaryName = "fullname"; break;
case "queue": primaryName = "name"; break;
default: continue;
}
var entity = serviceAdmin.Retrieve(
reference.LogicalName,
reference.Id,
new ColumnSet(primaryName));
var name = entity.GetAttributeValue<string>(primaryName);
recipientNames = string.IsNullOrEmpty(recipientNames)
? name
: $"{recipientNames}, {name}";
}
description = description.Replace("##ToRecipient##", recipientNames);
}
#endregion
#region REGARDING RECORD
if (target.Contains("regardingobjectid"))
{
var regarding = target.GetAttributeValue<EntityReference>("regardingobjectid");
if (regarding != null)
{
switch (regarding.LogicalName)
{
case "opportunity":
var opportunity = serviceAdmin.Retrieve(
"opportunity",
regarding.Id,
new ColumnSet("custom_recordurl"));
description = description.Replace(
"##opportunity.custom_recordurl##",
opportunity.GetAttributeValue<string>("custom_recordurl"));
break;
case "account":
var account = serviceAdmin.Retrieve(
"account",
regarding.Id,
new ColumnSet("custom_taxcode"));
var pattern = @"<li\s*>\s*Tax Code:\s*##account\.custom_taxcode##\s*</li>";
var taxCode = account.GetAttributeValue<string>("custom_taxcode");
var replacement = !string.IsNullOrWhiteSpace(taxCode)
? $"<li>Tax Code: {taxCode}</li>"
: string.Empty;
description = Regex.Replace(
description,
pattern,
replacement,
RegexOptions.IgnoreCase | RegexOptions.Singleline);
break;
}
}
}
#endregion
target["description"] = description;
}
catch (Exception ex)
{
tracing?.Trace(ex.ToString());
throw new InvalidPluginExecutionException(ex.Message, ex);
}
finally
{
tracing?.Trace("END PreCreateEmailReplacePlaceholders");
}
}
}
}
Example Email Template (HTML)
<p>Dear <strong>##ToRecipient##</strong>,</p>
<p>
A new request has been created in the system.
</p>
<ul>
<li>Tax Code: ##account.custom_taxcode##</li>
</ul>
<p>
You can open the related opportunity here:
</p>
<p>
<a href="##opportunity.custom_recordurl##">Open Opportunity</a>
</p>
<p>
Kind regards,<br/>
CRM Team
</p>
How It Works
1. Recipient Resolution
Reads all records in the To field (activityparty)
Supports users, contacts, accounts, and queues
Joins names into a single string
Replaces ##ToRecipient##
2. Conditional HTML Replacement
For Account-related emails:
If the tax code exists → <li> is rendered
If empty → <li> is completely removed via Regex
This keeps the email HTML clean and professional.
3. Why Pre-Operation?
Executing in PreCreate ensures:
Email body is already finalized when saved
No async jobs
No client-side JavaScript
Works with templates, workflows, Power Automate