Customize your AX Workflow Email Templates

12 12 2012

A number of our clients have made requests of us to provide more meaningful information available on the Email notifications that are sent to the workflow work-item assignees. The typical information that they would like to see is line item information for workflow relating to Purchase Requisitions etc.

The easiest way is to modify the various task and step instructions in your workflow configuration. You can select tags relating to the lines such as %Purchase Requisition.Purchase requisition lines.ItemId% etc. These instructions can be displayed in your workflow email notification by including the %message% tag in your email template.

Screen Shot 2012-12-12 at 10.11.38 AM

However using this approach is not very flexible or visually appealing as each tag is replaced by a comma separated list of the values from the various lines. Most of our clients have required a more tabular format for the lines. My approach to solving their issue is by using a code based solution that I will describe below.

I’d like to say at the outset that the disadvantages to this approach is that it doesn’t easily allow for multi-language, its fairly rigid, and it is a more hard-coded solution making it not very flexible.

Step 1. Create a method ‘wfDescription’.

Create a ‘wfDescription’ method on each Workflow Document table that you are using. This method should return an block of html (or plain text) with the content that you would like to display for the given document type. E.G. For a purchase requisition add the following method to the PurchReqTable table.

str wfDescription() 
 PurchReqLine line;
 str ret="";
 ret = ret + strfmt("<strong>Motivation: </strong>%1<br/>", this.businessJustification()); ret = ret + "<strong>Lines:</strong><br/>";
 while select line where line.PurchReqId==this.PurchReqId
   ret = ret + strfmt("%1. %2 <span style='color: #009966'> (%3 @ %4 %5) - %6</span><br/>",num2str(line.LineNum,0,0,1,3), line.itemName(), line.PurchQty, line.CurrencyCode, line.PurchPrice, line.LedgerAccount);
 return ret;

Step 2: Add a description tag to your email template

Open up your email template and place a %documentdescription% tag in the place where you would like the text/html block from Step 1 to appear in your emails.

Step 3: Enable the %documentdescription% tage

This is the key step in the whole process. To enable workflow to replace the new %documentdescription% tag created in Step2 with the contents from the method in Step 1. To do this we will be customizing the ‘EventNotificationWorkflow’ class:

  • Open the ‘EventNotificationWorkflow’ class.
  • Edit the sendMail method.
  • Add the line ‘this.addCustomMergeValues();’ after the line ‘this.addBaseMergeValues();’
  • Create a new method named ‘private void addCustomMergeValues()’ to the ‘EventNotificationWorkflow’ class.
    This method will determine whether the workflow document has the ‘wfDescription’ method and will replace the tag with what the method returns.
private void addCustomMergeValues2()
  SysDictTable dictTable; 
  str description;
  dictTable = new SysDictTable(tablenum(PurchReqTable));

  if (dictTable.isMethodActual('wfDescription'))
     description = dictTable.callObject('wfDescription',record);
    description = "";
  mergeValues.insert("documentdescription", description);

The final step is to prevent the html from your return method from being escaped. To do so:

  • Open up the SysEmailTable (Table) in the AOT
  • edit the HTMLEncodeParameters method
  • Add a conditional statement before the line ‘encodedMap.insert(mapEnum.currentKey(), SysEmailTable::htmlEncode(mapEnum.currentValue()));’ so that your new tag %documentdescription% is not html encoded.
if (mapEnum.currentKey() == 'documentdescription')
    encodedMap.insert(mapEnum.currentKey(), mapEnum.currentValue());
    encodedMap.insert(mapEnum.currentKey(), SysEmailTable::htmlEncode(mapEnum.currentValue()));

If all goes well you should now receive more detailed information in your email notification.
Let me know if you have any better ways of accomplishing this or comments on my solution.

Happy daxing.




11 responses

25 01 2013

Great blog. THX.
I just want mention that in addCustomMergeValues() method you shall use ExecutePermission perm;
perm = new ExecutePermission();


Otherwise emails can not be sent.

28 06 2013

how to set link in Email notification for EP Expense Reports in Dynamics Ax 2009.
if the user get email notification and clicks link, it should show particular expense in EP Portal .i have tried using the below option to get the same.

Go to %for%

but its redirecting to dynamics Ax form .need to know the syntax for redirecting the expense to Enterprise portal page instead of showing the Ax form

Kindly suggest the procedure to get the link with ep portal .

17 02 2015
Thinking Enterprise Solutions

[…] Customize your AX Workflow Email Templates […]

22 03 2015

Hi Jonathan
I followed you blog and it worked well for adding custom description to email template for sales order workflow except one problem i.e. when I am submitting Purchase order workflow I am getting following error:

Workflow Instance ID: WI-0011209 System.ServiceModel.Internals SysWorkflowQueue-resume SysWorkflow-save SysWorkflowWorkItem-createWorkItems SysWorkflowWorkItem-create X++ Exception: Error executing code: PurchTable table does not have method ‘workflowDescription’.
at System.Runtime.AsyncResult.End[TAsyncResult](IAsyncResult result)
at System.Runtime.DurableInstancing.InstancePersistenceContext.EndOuterExecute(IAsyncResult result)
at System.Activities.WorkflowApplication.UnloadOrPersistAsyncResult.OnPersisted(IAsyncResult result)
at System.Runtime.AsyncResult.SyncContinue(IAsyncResult result)
at System.Activities.WorkflowApplication.UnloadOrPersistAsyncResult.Persist()
at System.Activities.WorkflowApplication.UnloadOrPersistAsyncResult.CollectAndMap()
at System.Activities.WorkflowApplication.UnloadOrPersistAsyncResult.Track()
at System.Activities.WorkflowApplication.UnloadOrPersistAsyncResult.InitializeProvider()
at System.Activities.WorkflowApplication.UnloadOrPersistAsyncResult..ctor(WorkflowApplication instance, TimeSpan timeout, PersistenceOperation operation, Boolean isWorkflowThread, Boolean isInternalPersist, AsyncCallback callback, Object state)
at System.Activities.WorkflowApplication.IdleEventHandler.OnStage1Complete(IAsyncResult lastResult, WorkflowApplication application, Boolean isStillSync)
at System.Activities.WorkflowApplication.IdleEventHandler.Run(WorkflowApplication instance)
at System.Activities.WorkflowApplication.OnNotifyPaused()

Will be grateful if you could let me know how to resolve this issue. Thanks in advance.

25 03 2015

Hi Haroon.

Please ensure that you have a method in your PurchTable called workflowDescription and ensure that you declare it public. It also may be useful to perform a full or incremental CIL on the AOT.

On Sun, Mar 22, 2015 at 10:08 AM, Dynamics AX Workflow Wanderings wrote:


27 03 2015

Hi Jonathan, thanks for your reply. I do not understand why the system is checking ‘workflowDescription’ method on PurchTable whereas I am calling this methods on ‘SalesTable’ only.

27 03 2015

Hi. are you sure you have included the check for whether the method exists?

if (dictTable.isMethodActual(‘wfDescription’))

3 04 2015

Hi Jonathan

I got it worked out by checking tableId of record like this

if (dictTable.isMethodActual(‘wfDescription’))
description = dictTable.callObject(‘wfDescription’,record);

I am stuck in another problem where the wfDescription method is not returning correct result. I am trying to print following information in email template

Sales order : SO-00001
Open balance : 0.00
Current order : 0.00
New balance : 0.00
Credit limit : 0.00
Credit excess : 0.00

but instead of bringing values it is showing zeros against each. I have checked permission of user defined in Workflow execution account and System administration is assigned to this user but still no success.

Can you let me know what might be the issue. For your information, I am customizing email template on Microsoft Dynamics AX 2012 R2. Thanks.

9 04 2015

Hi. You shouldnt need both conditional statements, but no harm in having them both. Please check tha the record object is being properly initialised and that your code is WF description method is initialising all the additional fields correctly. You may need to initialise a copy of your SalesTable in your method e.g. SalesTable sales = SalesTable::find(this.RecId); then use this copy to populate your description.

25 07 2015
Sam Framk

Hi Jonathan.
This code seems to be for the 2009 version. Have you tried to do this in AX 2012?

7 08 2015
Maneendra Kumar

Hi Jonathan,

Does the above process is restricted to 2009. can’t follow the same in AX 2012 R1?
Do we have any alternate process to implement the same customization in AX 2012 R1.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: