Tag Archives: workflow

Tutorial: Deleting a workflow instance in Microsoft Dynamics AX 2012

Skill level: Advanced

Hi everyone,

Today’s post demonstrates how to delete a workflow instance in Microsoft Dynamics AX 2012. I don’t mean “workflow definition”. I mean the actual instance of a workflow which is attached to a source object like a purchase requisition or sales order. Functional experts will claim that it can’t be done. Well, that may be true with out-of-the-box Dynamics AX 2012, but as developers of the AOT we know that of course it can be done, and safely, too.

Microsoft has done a wonderful job developing the workflow capabilities of Dynamics AX 2012, but once we dig into it, we notice several limitations. One of these, being allowed just a single available “default” workflow definition to instantiate, is addressed by my post here.

Another limitation is that we may only initiate one instance of a workflow on a source object. For example, we can never have two workflows initiated on the same source object.

A third limitation (and the subject of today’s post) is that once we commit a workflow instance to a source object, we are stuck with it. We cannot “undo” the instantiation of a workflow, so our choice is to delete the source object (purchase requisition, sales order, etc.) and start over, or cancel the workflow. But even if we cancel the workflow, the relationship between the source object and the workflow remains and we still cannot instantiate another workflow on it.

What if we wanted to delete a workflow instance but not delete the source object it was attached to? In other words, to “reset” the source object as though the workflow initiation never occurred? It sounds useful to me. Would we need to do it frequently? Probably not. But wouldn’t it be nice if we could? Today’s post demonstrates how to delete a workflow instance from an object, with support for the deletion of nested subworkflows. (See my post on how to configure subworkflows.)

Disclaimer

These techniques are not for beginners. Only you know your skill level, so use your best judgment. Since we’re going to be deleting data you’ll want to be working in your development environment or a backup until you’re comfortable. That said, I’ve tested this solution over a period of weeks and thoroughly understand what is happening. Though this is an experimental concept, I believe it is safe.

Workflow in the AOT

So let’s look at workflow from the AOT side of things. There are roughly forty (40) data tables involved in workflow, and believe me the relationships between these tables are complicated. One afternoon I decided to see exactly how complicated they are, so I mapped the relationships by hand. At first I used PowerPoint as my mapping tool but quickly realized that it wasn’t up to the task. Surprisingly, neither was Microsoft Visio. In the end, I had to use AutoCAD to painstakingly draw the relationships between all of the tables which define the workflow engine in Microsoft Dynamics AX 2012.

The results are not pretty.

The table relations for workflow in Microsoft Dynamics AX 2012

The table relations for workflow in Microsoft Dynamics AX 2012

Fortunately, I’ve done the heavy lifting for you. I’ve investigated each and every one of these tables and determined the Who, What, Why and When, and now I know:

> Which tables are related to the workflow definition
> Which tables are related to the workflow instance
> Which tables are related to both

Investigating these relationships took a lot of time and effort, but eventually I arrived at a solid understanding of how they work. Step after step, I analysed what is happening to the data in these tables throughout the workflow process. Curiously, the single most important piece of knowledge is not WHERE the data exists, it is WHEN the data exists. This is due to the ever-changing nature of workflows, with their many “states” and statuses.

My goal for this project was to learn how to delete a workflow instance without introducing new table relationships or modifying the structure of these tables in any way. I wanted to write my X++ code so that it could be applied to any workflow regardless of its type. I wanted to rely entirely on the existing tables in their out-of-the-box state, including taking advantage of existing delete actions. In the end, I came very close to reaching that goal. Sure, I could delete the workflow instance, but I learned about one final required step.

What did I learn? After we delete a workflow instance, we must reset our source object back to its initial status so that a new workflow instance may be attached in the future. Now, this is not particularly difficult. The information is easy to find – it is located in the workflow’s primary data table in a method called canSubmitToWorkflow. But, each source object contains different conditions for workflow eligibility, so we’ll need to add special coding to our class to account for each of these conditions. I will demonstrate this idea later.

So let’s begin. I will provide the code each step of the way, but will also provide an XPO containing the class and the action menu, which is available here.

Step 1: Mapping the Data

We already know that the most critical object relationships in Microsoft Dynamics AX 2012 are table relations. Nothing happens without valid relationships. The AOT developers with the greatest skills are those with a thorough understanding of how table relations work. Microsoft Dynamics AX 2012 is a vast landscape, and table relationships form a very important map for us to navigate.

All of this is true for workflow as well. While the complete table relationship diagram for workflow is truly mind-boggling in its complexity, there IS a way through. As I noted above, I’ve cleared away a great deal of debris and have identified the name and purpose of each of the tables related to workflow. Fortunately, when we’re dealing with workflow instances, there are relatively few tables to manage. For this project, the tables we’ll work with are:

> SysWorkflowInstanceTable
> SysWorkflowTable
> WorkflowQueueDocumentCommonFlds
> WorkflowTrackingArgumentTable
> WorkflowTrackingCommentTable
> WorkflowTrackingStatusTable
> WorkflowTrackingTable
> WorkflowTrackingWorkItem
> WorkflowWorkItemTable

An illustrated view of the table relations for this group of tables is shown below. It is comparitively straight-forward and no more complicated than any other collection of tables in the AOT.

Step 2: Creating our Class

So let’s start coding. In the AOT, create a new class. I’ve named my class zWorkflowInstanceDeletion but you can name yours whatever you want. Create a new method called deleteWorkflowInstance, and add the code below. I highly recommend that you read the comments because I’ve described in detail what the code is doing.

void deleteWorkflowInstance(RefRecId _objectRecId, TableID _tableID)
{

 // definition of incoming variables:

 //  _objectRecId = the RecID of the object in the main definition table.
 //                 for example, the CustTable, InventTable, etc.

 // _tableName    = the name of the main definition table, so that we
 //                 can get its tableID number.

 // these two variables are used to identify the workflow instance
 // we’re trying to delete – the main record of which is stored in the
 // WorkflowTrackingStatusTable table.

 WorkflowTrackingStatusTable     workflowTrackingStatusTable, workflowTrackingStatusTable_forDelete;
 WorkflowTrackingTable           workflowTrackingTable;
 WorkflowTrackingWorkItem        workflowTrackingWorkItem;
 WorkflowWorkItemTable           workflowWorkItemTable_forDelete;

 WorkflowCorrelationId           workflowCorrelationId;
 SysWorkflowTable                sysWorkflowTable_forDelete;

 // the first thing we’ll do is to get the workflow CorrelationID, which is the
 // workflow instance’s unique identifier. we’ll use it later to delete our
 // records out of the SysWorkflowTable table.

 select workflowTrackingStatusTable where workflowTrackingStatusTable.ContextRecId == _objectRecId
   && workflowTrackingStatusTable.ContextTableId == _tableID;

 if (workflowTrackingStatusTable)
 {
   // we’ve found the unique workflow instance for our "attached" object

   workflowCorrelationId = workflowTrackingStatusTable.CorrelationId;

   // workflow tables have many, many relationships, but based on the
   // data analysis I’ve done, we need only be concerned about a few.
   // fortunately there are already cascade delete actions on most (but not all)
   // of the table relationships we care about.

   // the first thing we have to do is delete the data in the WorkflowWorkItemTable
   // (because there is no delete action on it). to do that, we have to navigate
   // through a few tables first, tracing the known relationships, so that we can
   // properly identify the data we want deleted.

   // we use a WHILE statement here because there are multiple records for our
   // workflow instance in both of the joined tables…

   while
    select workflowTrackingTable where
      workflowTrackingTable.WorkflowTrackingStatusTable == workflowTrackingStatusTable.RecId
    join workflowTrackingWorkItem where
      workflowTrackingWorkItem.WorkflowTrackingTable == workflowTrackingTable.RecId
   {

     // we want to delete our data from the WorkflowWorkItemTable table
     // but, one of the things I’ve found is that COMPLETED workflow
     // instances do not have data in this table.

     // this means that even though there are known data relationships
     // between the tables, we can’t join them all in one statement.

     // the call to delete from the WorkflowWorkItemTable
     // has to come in a second, stand-alone statement which uses the
     // data from the first statement as variables.

     try
     {
      ttsBegin;

      while select forUpdate workflowWorkItemTable_forDelete where
       workflowWorkItemTable_forDelete.RecId == workflowTrackingWorkItem.WorkflowWorkItemTable
       && workflowWorkItemTable_forDelete.Id == workflowTrackingWorkItem.WorkItemId
      {
       // now we can delete all records from the WorkflowWorkItemTable
       // where we have a relationship to the WorkflowTrackingWorkItem
       // table. Note that all related records in the WorkflowQueueDocumentCommonFields
       // table will also be deleted because of the cascade delete action.

        workflowWorkItemTable_forDelete.delete();
      }

      ttsCommit;
     }
     catch(exception::Error)
     {
      info("Error deleting the workflow instance of item "
        + guid2str(workflowCorrelationId) + " from the WorkflowWorkItemTable table.");
     }

   }

   // ————————————————————————————————-

   // we’ve deleted the data out of tables workflowWorkItemTable and WorkflowQueueDocumentCommonFields.

   // now it’s just a simple matter of deleting the data out of the WorkflowTrackingStatusTable table.
   // when that happens, all related data will be deleted from the following tables by delete actions:

   // + WorkflowTrackingStatusTable                                 (one record per workflow instance)
   //
   //  + WorkflowTrackingTable                                    (many records per workflow instance)
   //
   //    + WorkflowTrackingWorkItem                               (many records per workflow instance)
   //
   //      + WorkflowTrackingCommentTable                         (many records per workflow instance)
   //
   //      + WorkflowTrackingArgumentTable                        (many records per workflow instance)

   // ————————————————————————————————-

   // our delete statement will delete the records from the WorkflowTrackingStatusTable using the
   // main variables we defined at the beginning of this code:

   //   _objectRecId
   //   tableID

   try
   {
    ttsBegin;

    while select forUpdate workflowTrackingStatusTable_forDelete where
     workflowTrackingStatusTable_forDelete.ContextRecId == _objectRecId
     && workflowTrackingStatusTable_forDelete.ContextTableId == _tableID
    {
     workflowTrackingStatusTable_forDelete.delete();
    }

    ttsCommit;
   }
   catch(exception::Error)
   {
    info("Error deleting the workflow instance " + guid2str(workflowCorrelationId)
     + " from the workflowTrackingStatusTable table.");
   }

   // ————————————————————————————————-

   try
   {
    ttsBegin;

    while select forUpdate sysWorkflowTable_forDelete where
     sysWorkflowTable_forDelete.WorkflowCorrelationId == workflowCorrelationId
    {

     // we’ve deleted the data from the WorkflowTrackingStatusTable.
     // the last thing to do is to delete any related data from the
     // SysWorkflowTable table. When we do that, any related data
     // will be deleted from the SysWorkflowInstanceTable table.

     sysWorkflowTable_forDelete.delete();
    }

    ttsCommit;
    info("Successfully deleted the workflow instance from the selected object.");

   }
   catch(exception::Error)
   {
    info("Error deleting the workflow instance " + guid2str(workflowCorrelationId)
     + " from the sysWorkflowTable table.");
   }

  }

 else
 {
  throw error (‘Cannot locate a workflow instance for this object.’);
 }

}

Now add a another method and name it doWorkflowDeletion. The code for this method is shown below.

void doWorkflowDeletion(RefRecId _objectRecId, TableID _tableID, Common _common)
{

 // this is the main "engine" which starts the workflow deletion
 // process. Its arguments are supplied by the selected record
 // on a list page or details page.

 WorkflowTrackingStatusTable     workflowTrackingStatusTable;
 zWorkflowInstanceDeletion       workflowInstanceDeletion = new zWorkflowInstanceDeletion();

 select workflowTrackingStatusTable
  where workflowTrackingStatusTable.ContextRecId   == _objectRecId
     && workflowTrackingStatusTable.ContextTableId == _tableID;

 if (workflowTrackingStatusTable)
 {
  if (Box::yesNo("Are you sure you want to…\r\r   1. Delete the primary workflow instance\r   2. Delete any subworkflows managed by the primary workflow\r\r…from this object?\r\rThis operation cannot be undone! Contact a system administrator if you aren’t certain of how to proceed.", DialogButton::No, ‘Delete workflow instance?’) == DialogButton::Yes)
  {

   // first, delete all of the workflow’s subworkflows recursively
   workflowInstanceDeletion.deleteSubworkflowInstances(workflowTrackingStatusTable.CorrelationId);

   // next, delete the object’s attached workflow
   workflowInstanceDeletion.deleteWorkflowInstance(_objectRecId, _tableID);

   // lastly, update the object’s workflow status so a new workflow can be attached if desired
   workflowInstanceDeletion.resetWorkflowStatusAfterDeletion(_common);

  }
 }
 else
 {
  throw error (‘Cannot locate a workflow instance for this object.’);
 }
}

Add a third method and name it deleteSubworkflowInstances. This method recursively deletes any subworkflow or nested subworkflows associated to the primary workflow which we’re deleting. If you don’t want subworkflows to be deleted then you won’t need to call this.

void deleteSubworkflowInstances(WorkflowCorrelationId _workflowCorrelationId)
{

 // recursive function for deleting all subworkflows under a given ParentCorrelationID

 WorkflowTrackingStatusTable     workflowTrackingStatusTable;
 WorkflowCorrelationId           tmpWorkflowCorrelationId;

 while
  select workflowTrackingStatusTable
   where workflowTrackingStatusTable.ParentCorrelationId == _workflowCorrelationId
    && workflowTrackingStatusTable.ParentCorrelationId != str2guid(‘{00000000-0000-0000-0000-000000000000}’)
 {

  // store the value in a variable so we can use it after the workflow is deleted
  tmpWorkflowCorrelationId = workflowTrackingStatusTable.CorrelationId;

  // physically delete the subworkflow
  this.deleteWorkflowInstance(workflowTrackingStatusTable.ContextRecId, workflowTrackingStatusTable.ContextTableId);

  // call this function recursively to root out any children
  this.deleteSubworkflowInstances(tmpWorkflowCorrelationId);

 }
}

Fourth, add our Main method, with the code as shown below.

public static void main(Args _args)
{

  // the purpose of this class is to delete a workflow association
  // from an object. In addition, it also deletes (recursively)
  // any subworklows (including nested subworkflows) that may be
  // associated with the object’s primary workflow.

  // the deletion is, of course, permanent. ;)

  zWorkflowInstanceDeletion   workflowInstanceDeletion = new zWorkflowInstanceDeletion();

  // ——————————————————————————————————- //

    // delete the workflow and any associated subworkflows
    workflowInstanceDeletion.doWorkflowDeletion(_args.record().RecId, _args.record().TableId, _args.record());

  // ——————————————————————————————————- //

}

Lastly, we need to add a method for resetting the selected source object back to a status which allows workflow to be initialized on it again. As I mentioned above, each source object (like a purchase requisition or a sales order) has its own conditions for determining whether workflow is allowed to be initialized. This means that YOU MUST ADD CODE to support all workflow types you wish to delete.

void resetWorkflowStatusAfterDeletion(Common _common)
{

  // now, after we’ve deleted a workflow, we will be unable to attach
  // a new workflow if the table’s canSubmitToWorkflow method calls for
  // specific conditions for attaching workflows. This is a very common
  // condition, so we must set the object’s workflow status back to
  // an allowable state for workflow attachment.

  // the downside is that each workflow "concept" in AX has its own
  // conditions, so we’ll need to look at each case we support, and
  // reset the workflow status.

  // this is where you would list your primary tables, like "PurchReqTable"

  //PurchReqTable        purchReqTable, purchReqTable_forUpdate;

  switch (_common.TableId)
  {

   // after we delete the workflow instance we have to set the object’s
   // status back to the criteria which is defined in the canSubmitToWorkflow
   // method. otherwise we will not be able to submit a new workflow in
   // the future.

   //case tablenum(purchReqTable):
   // purchReqTable= _common;

    //try
    // {
     //ttsBegin;

     // note that this is a SAMPLE! You must account for all tables you’re working with

     //while select forUpdate purchReqTable_forUpdatewhere purchReqTable_forUpdate.PurchReqId == purchReqTable.PurchReqId
     // {
     // purchReqTable_forUpdate.RequisitionStatus = PurchReqRequisitionStatus::Draft;
     // purchReqTable_forUpdate.update();
    // }
    //ttsCommit;

    //Box::warning("It is highly recommended that you refresh the form to view the most current records.", "Refresh Form");

    //}
    //catch(exception::Error)
    //{
    // info("There was an error updating the project status after deleting the workflow instance.");
    // }
    //break;
  }
}

Step 3: Creating our Action Menu

Now that we’ve created our class, we need to create a new action menu to actually run the code. Please create a new action menu and use the property settings shown below:

Now add this action menu to the form whose workflow should be deleted (I assume you know how to do this). This will produce a button that looks like this. It is your own responsibility to ensure that the proper security is applied to this button.

June10_02

Step 4: Putting it All Together

Now we’re ready to test our solution. As an example, the object below has an existing workflow instance (click it to zoom in). Note that the Workflow Status is In Progress, and note the Step 1 details listed above the selection.

June10_04A

We press our action button and we are prompted to continue. As always, confirming with the user is best practice, particularly when we are about the delete data.

June10_03

Press the button labeled YES. Microsoft Dynamics AX 2012 deletes the selected workflow instance from the object:

June10_05

After we refresh the listpage, we see that our object has been reset back to its original state, with no workflow association! (Click the image below to zoom in.) The workflow status has been reset to Not Submitted. Since our workflow instance has been deleted, we can now initiate a new workflow instance at any time.

June10_04B

Summary

I hope you find this exercise useful. Due to the amount of spam I receive, I no longer allow comments on my posts, but I do accept LinkedIn connections. Please connect with me here if you’re interested. If you like photography, please visit my main website.

Happy coding!

Tutorial: Moving past the Default Workflow Definition

Skill level: Advanced

Hi everyone,

Today we’ll continue our examination of workflow in Microsoft Dynamics AX 2012. One of the basic rules for workflow is that we may create as many definitions within a particular workflow type as want, but we’re allowed only one default workflow definition. For example, in the image below we see three workflow definitions (in blue) available for my custom workflow type zTURCKToolsDevProj. (See my post for more information on how to create custom workflow types.) However, only the third member of the list, WFI000015, is set as the default definition. This means that when workflow is initiated for this type, Microsoft Dynamics AX 2012 will always use workflow definition WFI000015.

While I have no complaint with being allowed only one default workflow definition, the downside is apparent: when we need to account for many business conditions and we’re allowed only one available definition, that workflow definition can become very complex and difficult to manage.

Wouldn’t it be nice to design as many workflow definitions as we want and then use X++ automation to decide which of these workflow definitions to initiate? Why be limited to a single available definition? Suppose there are cases when we don’t want to initiate workflow WFI000015, we want to initiate WFI000016 or WFI000032. Unfortunately, this flexibility is not available in out-of-the-box Microsoft Dynamics AX 2012.

In today’s post we’ll create an add-on to the workflow system which allows us to initiate a workflow instance using a condition of our own choosing, rather than being limited to using the default workflow definition. This gives us the freedom to design each workflow definition with a specific business condition in mind rather than having to use a single definition which contains very complex branching to meet every business condition.

Preparation:

As always, documenting our requirements is our first task. We will need the following:

> One new table for defining which workflow will be initiated under which condition
> One new form to provide an interface for the new table
> One new menu item to display the new form
> One new class and a method to return the appropriate workflow definition depending on condition
> A small, but significant, modification of our workflow type’s Submit Manager class

Step 1: Create a New Table

Using the AOT, create a new table. In my example, I use the name zTURCKToolsDevProjectWFChooserTable. The purpose of this table is to store the relationship of a known workflow definition and a custom Enum which stores project categories. The table looks like this:

The idea is that we’ll use the project category selection to determine which workflow definition should be initiated. For example, when the project category value is ‘CM’ we want the workflow engine to initiate workflow WFI000032. Similarly, when the project category is ‘PD’ we want to initiate workflow WFI000015 and so on.

I’m assuming that you already know how to create tables so we don’t need to waste time with that. Instead, here is an XPO of the table definition which you can import. This will save us time.

Step 2: Create a New Form

Using the AOT we will create a form which provides an interface to the table we created in Step 1. As before, I don’t want to waste time with form creation techniques, so here is an XPO you can import. The design of the form looks like this:

Step 3: Create a New Display Menu Item

As we know, the preferred method to display a form in Microsoft Dynamics AX 2012 is to use a display menu item. We’ll do that now. Please create a new display menu item and refer to the image below for the property settings. You can place the display menu wherever you wish, but best practices would suggest placing it under a Setup submenu.

Our new display menu

Our new display menu

Step 4: Create a New Class and Method

Now that we have the basics out of the way, we can start coding. We’ll create a new class called zTURCKToolsDevelopmentProjectWFChooser and add a new static method as shown below.

public static guid getRequiredWorkflowConfigurationID (str _projectCategory)
{
 guid                                 configurationID = str2guid(‘{00000000-0000-0000-0000-000000000000}’);

 WorkflowTable                        workflowTable;
 WorkflowVersionTable                 workflowVersionTable;

 zTURCKToolsDevProjectWFChooserTable  turckToolsDevProjectWFChooserTable;

 select workflowVersionTable where workflowVersionTable.Enabled == noyes::Yes
  join workflowTable where workflowTable.RecId == workflowVersionTable.WorkflowTable
  join turckToolsDevProjectWFChooserTable where
   turckToolsDevProjectWFChooserTable.WorkflowSequenceNumber == workflowTable.SequenceNumber
   && turckToolsDevProjectWFChooserTable.ProjectCategory == _projectCategory;

 if (workflowVersionTable)
  configurationID = workflowVersionTable.ConfigurationId;

 return configurationID;

}

Let’s take a look at what this code does. We join our zTURCKToolsDevProjectWFChooserTable table to two workflow definition tables, ensuring that we are using the enabled version of the workflow definition. We also ensure that our workflow definition actually exists (they can be deleted after all). If either of these conditions fails, we’ll return an empty GUID to the calling method, which will cause a STOP error.

Step 5: Identify the Submit Manager Class

The next step is to locate the Submit Manager class for the workflow type we’re dealing with. In my example, I’m working with a custom workflow definition, so you won’t recognize the name. However, the principle is exactly the same, so you should be able to figure it out if you’re working with one of the standard workflow types. Begin by locating the definition of the workflow type in the AOT. This is located at Workflow\Workflow Types. The workflow type I’m using is called zTURCKToolsDevProj, but these steps will be the same for any of the standard workflow types.

Locate the SubmitToWorkflowMenuItem property and note its value. It contains the name of the action menu item which performs the submission of our workflow. In the image below I’ve highlighted the property and its value (zTURCKToolsDevProjSubmitMenuItem).

Locate the SubmitToWorkflowMenuItem property

Locate the SubmitToWorkflowMenuItem property

Now we’ll locate the action menu object zTURCKToolsDevProjSubmitMenuItem (or whatever the name of the object in your own case may be). This action menu item is the definition of the little Submit button that we’re all familiar with:

The well-known "Submit" button

The Submit button

Action menu items are located in the AOT at Menu Items\Action. Note that the ObjectType property is set to Class, and the Object property is set to zTURCKToolsDevProjSubmitManager. This is the name of the class which defines what happens when you press the Submit button.

Action menu definition

Action menu definition

As I’ve noted earlier, my example is using a custom workflow type of my own design. The values in these properties will be different on your own system, but the principles are the same. For example, if we were working with the PurchReqReview workflow type, our SubmitToWorkflowMenuItem property value would be PurchReqSubmitToWorkflow. Likewise, the Submit Manager class is called PurchReqWorkflow.

What Do We Know Now?

Let’s stop a minute and summarize where we are.

> We’ve created a new table for storing the relationship between an existing workflow definition and a value called “project category”. The idea is that we’ll use this relationship to tell the workflow engine which workflow definition to initialize when the project category is one value or another. Your own design can be as complex and unique as you need. In the end, instead of having to use the default workflow definition, we’re going to tell the workflow engine which definition to use. I hope you’re getting an idea of the power and flexibility of this concept!

> We’ve identified the class which is responsible for the submission of our workflow instances. This is a very important piece of information. Now that we’ve located this class, we’ll make one small modification to a single method and our new solution will work.

Step 6: activateFromWorkflowType vs. activateFromWorkflowConfigurationId

The final step is modify the Submit method of our Submit Manager class. The Submit method stores the code that runs when we press the Submit button on the form. Most Submit Manager classes feature this method, but in some of them, the code is run in the Main method or even in a method with a different name. But it doesn’t matter. If your Submit Manager class doesn’t have a Submit method, simply search the class for the word “activateFromWorkflowType”. Once you’ve found the method which includes this text, edit the code. The call to activateFromWorkflowType takes the form of a static method within the Workflow class, as in the example below:

workflowCorrelationId = Workflow::activateFromWorkflowType(workflowTypeName, recId, note, NoYes::No);

According to Microsoft’s documentation, the activateFromWorkflowType method “Activates a workflow from a specified workflow template name”. It always uses the default workflow definition when it’s called. The method is defined as follows:

server public static WorkflowCorrelationId activateFromWorkflowType(
    WorkflowTypeName _workflowTemplateName,
    RecId _recId,
    WorkflowComment _initialNote,
    NoYes _activatingFromWeb,
   [WorkflowUser _submittingUser])

Our job is to replace this call to activateFromWorkflowType with a call to one of the other methods in the Workflow class, activateFromWorkflowConfigurationId. That method is defined as follows:

server public static WorkflowCorrelationId activateFromWorkflowConfigurationId(
    WorkflowConfigurationId _workflowConfigurationId,
    RecId _recId,
    WorkflowComment _initialNote,
    NoYes _activatingFromWeb,
   [WorkflowUser _submittingUser])

Comparing the two methods, we find that the only difference is the first argument (WorkflowTypeName versus WorkflowConfigurationId). Both return a WorkflowCorrelationId, and this is how the workflow engine determines which definition to instantiate.

Microsoft’s documentation tells us that the activateFromWorkflowConfigurationId method “Activates a workflow based on a workflow configuration ID”. What this means is that instead of relying on activateFromWorkflowType to determine which workflow definition to use (and it will always use the default), we can directly inform the workflow engine to initialize a workflow instance using a specific Workflow Configuration ID.

And from where will we get this specific Workflow Configuration Id? From our custom table, of course!

All we need to do is change one line of code, replacing the out-of-the-box call to activateFromWorkflowType to a custom call to activateFromWorkflowConfigurationId. In the example below, I’ve done exactly that PLUS I’ve added a call to a table method which returns the correct Workflow ID for the parameter I’ve provided – in this case, that parameter is ‘CM’.

workflowCorrelationId = Workflow::
                        activateFromWorkflowConfigurationId(
                        zTURCKToolsDevelopmentProjectWFChooser::getRequiredWorkflowConfigurationID(‘CM’),
                        recId,
                        note,
                        NoYes::No);

Note that I’m keeping my example simple, so I hard-coded the value of “project category” in the code sample above (i.e. the ‘CM’). In actual practice you will pass a field value from the table which is associated to the workflow instance. To make that clearer – imagine that your workflow definition is related to a table called Table1 and it contains the project category Enum as a field. In place of the ‘CM’ above, you will pass Table1.ProjectCategory. I did this to simplify the example, and I hope my explanation is clear. Let me know if it isn’t.

Putting It All Together

Once our work in the AOT is complete, all we have to do is select an object related to our workflow definition. As you can see in the image below, when I submitted a workflow on an object with a project category of ‘CM’, the workflow engine initiated an instance of workflow WFI000032. Likewise, for objects with a project category of ‘PD’, our workflow instance is based on workflow WFI000015. Beautiful!

Summary

Today we created an add-on to the workflow engine which allows us to move past the default workflow definition. It gives us the ability to choose which workflow definition to instantiate based on a condition under our control. The flexibility and power of this solution makes the job of designing workflows much easier because we don’t need to account for every single business scenario. Instead, we can design our workflows to meet each specific need and then tell the workflow engine which definition to use.

I hope you find this exercise useful. Due to the amount of spam I receive, I no longer allow comments on my posts, but I do accept LinkedIn connections. Please connect with me here if you’re interested. If you like photography, please visit my main website.

Happy coding!

Tutorial: Creating a Listpage to display custom workflow types

Skill level: Intermediate

Hi everyone,

In today’s post we’ll dig a little deeper into workflow in Microsoft Dynamics AX 2012. As you might know, the software ships with sixty different workflow types, ranging from Accounts Payable to Budgeting, from Human Resources to Travel and Expense.

Anticipating the high level of demand for workflow, Microsoft has done a great job preparing these workflow types. They provide excellent functionality and are very sophisticated, particularly those that can be used as subworkflows (see my post for more information). But there are some missing workflow types, particularly those for managing new product creation. If your company requires that certain standard operation procedures be followed for new product creation (such as testing or tooling development) then you’re out of luck, at least if you’re dealing with out-of-the-box Microsoft Dynamics AX 2012.

But – there’s no reason why we should have to live with out-of-the-box Microsoft Dynamics AX 2012! The Application Object Tree (AOT) is a powerful friend and we can use it to create our own workflow types which can be attached to any form we want. Today’s post will demonstrate how to create a listpage to manage your own workflow type.

Part One: ModuleAxapta

The first step in the process is to tell Microsoft Dynamics AX 2012 about it. Workflow types are stored in the AOT as a base enum called ModuleAxapta, so let’s add a new workflow type as a member of this enumeration. See below for an example where I’ve added a new member called “ColinWorkflowType”.

The ModuleAxapta base enum

The ModuleAxapta base enum

Note: since we don’t know what Microsoft is going to do in the future, I’d recommend using an EnumValue that’s considerably higher in value than the highest EnumValue. For example, RetailTerminal has a value of 30, so it might not be a great idea to use 31 because Microsoft might use that value in the next release of Dynamics AX.

Part Two: Display the WorkflowTableListPage form

Now that we’ve added our custom workflow type to the ModuleAxapta base enum, we’ll create a menu item to display a listpage for managing our custom workflow type. To accomplish this, simply create a new Display menu. I’ve named my new display menu ColinWorkflowTypeDisplayMenu. The nice thing about Display menus is that we can use the ObjectType property. In our case, we’re going to choose Form. In the Object property, select the object named WorkflowTableListPage. This form is set up by Microsoft to accept an argument as to which type of workflow it should display.

To display our new custom workflow type in the WorkflowTableListPage form, highlight the EnumTypeParameter property and select ModuleAxapta. What do you think we’ll choose for the EnumParameter property? If you guessed ColinWorkflowType then you’re correct!

The AOT properties for our new display menu

The AOT properties for our new display menu

Part Three: Putting it Together

After we create our display menu we need to place it in an area page. It’s up to you to decide where to put your own version, but for this example I’ll put the menu item in the Project Management and Accounting Setup area. To do that, I simply drag my display menu item to the appropriate area in the menu and set the IsDisplayedInContentArea property to Yes. An illustration of that is shown below.

Adding our display menu to the Project Management and Accounting menu

Adding our display menu to the Project Management and Accounting menu

When we load a fresh instance of the rich client, we see our new menu item as shown below:

SetupMenu

Go ahead and click the menu. Behold! We see a new list page, all set up to manage our custom workflows.

The new list page set up to manage our custom workflow types

The new list page set up to manage our custom workflow types

Part Four: This Is Only The Beginning

Today’s post describes how to setup a listpage to manage custom workflow types. The work of actually creating the tasks and approvals is not completed! As a developer, you are still responsible for creating the workflow categories, approvals, tasks, etc. in the AOT. Microsoft has several good tutorials on how to do that, but their examples use existing workflow types. Now that you know how to create a new workflow type using the ModuleAxapta base enum, you can modify Microsoft’s examples and take a step in a new direction!

I hope you find this exercise useful. Due to the amount of spam I receive, I no longer allow comments on my posts, but I do accept LinkedIn connections. Please connect with me here if you’re interested. If you like photography, please visit my main website.

Happy coding!

Tutorial: Configuring Subworkflows in Microsoft Dynamics AX 2012

Skill level: Intermediate

Hi everyone,

In today’s post we’ll take a look at workflow in Microsoft Dynamics AX 2012 with the goal of enabling subworkflows. I’ve done a deep-dive into the workflow engine and am very impressed by its flexibility and features, so I was keen to see if I could enable subworkflows too. In other posts I will dig even further into workflow and demonstrate how to delete a workflow instance from an object (despite claims that it can’t be done, it can) and how to use X++ to choose which workflow definition to attach to an object.

But that’s for later. Let’s get back to subworkflows.

If you’re like me, you’ve searched around the internet and have found very little information on how to configure subworkflows. Microsoft’s page on configuring subworkflows is quite thin, so if you follow their directions without knowing the secret you’ll probably end up with a rather unhelpful error which tells you:

Subworkflow document key: The document key selected for sub-workflow element is not valid.

It is frustrating to receive this error and not know how to proceed. The good news is that there is a secret piece of knowledge which will help you enable subworkflows. The bad news is that subworkflows take some planning ahead to work correctly – but once implemented they integrate seamlessly into their primary workflow host.

What Are Subworkflows?

Before we go much further, let’s understand what subworkflows are. Simply put, they are independent workflow definitions with a data relation to the primary workflow definition. When I say “independent” I mean that you are not limited to using them solely as subworkflows – you can also use them as the primary workflow object definition for the business process they are related to. However, when used as a subworkflow, the subworkflow’s datasource must have a foreign key relationship to the primary workflow’s datasource. This is a very flexible and powerful concept!

Primary and subworkflow definitions

Primary and subworkflow definitions

From a technical standpoint, the association between a workflow type and a table is defined by the workflow type’s Document class, which links to a Query. The document key field defines which of the fields in the primary workflow’s table is the foreign key linking to the subworkflow’s table.

While subworkflows can be executed asynchronously or synchronously, they do not share business data with their primary host (unless specifically programmed to do so, which is possible but outside of the scope of this article). Thus, these objects are not “parent” and “child” in the sense that the “parent” knows what the “child” is doing.

Instead, the primary workflow initiates the subworkflow for a related business process and can either continue processing or wait until the subworkflow completes. The workflow engine supports the idea of nested subworkflows too, so in theory you could have a very deep workflow structure, with a parent workflow initiating a subworkflow, which in turn initiates a second subworkflow, which in turn — but you get the idea. As long as you know the secret of configuring them and you satisfy the data requirements within each nested level, they work very efficiently.

The Secret of Subworkflows

So what is the secret? Like everything else in Dynamics AX 2012, a valid data relationship must exist between the relevant objects. In the case of subworkflows, there must be a logical relationship between the table the primary workflow is associated with and the table the sub-workflow is associated with.

What does that mean? It means that if you don’t have the proper data relations you’ll get the document key error.

Now that we know the secret, let’s take a close look at a real world example. It’s one thing to try to decipher technical language and another to see it working.

Our example will use the workflow types PurchCORNotifyDeliveryDue and PurchTableTemplate. The image below shows a conceptual view of how these two workflow types relate to one another.

PurchTableAndPurchLine

Starting on the upper tier (labeled “Workflow Type”) and working downwards towards the Table objects, we can see that the Document property for each Workflow Type specifies a Class (PurchCORPurchLineDocument and PurchTableDocument). The Query object associated to the Class can be found in the getQueryName method of the Class. When we examine these Queries, we can easily find the Data Source (PurchLine and PurchTable). Looking within these tables, we find the relation (PurchLine.PurchID == PurchTable.PurchID) we’re expecting to find. Therefore, we know that subworkflows are supported for this relationship.

Making It Work: Step One

Let’s create a workflow definition for the subworkflow so we can use it in our primary workflow definition. We’ll use the workflow type PurchTableTemplate, also known as a “Purchase order workflow”. To create this type of workflow definition, go to Procurement and sourcing/Setup/Procurement and sourcing workflows. We don’t need to worry about workflow elements in this example, so just create a fully-resolved workflow and save it. You should end up with something like this:

Subworkflow definition using the PurchTableTemplate workflow type

Subworkflow definition using the PurchTableTemplate workflow type

Making It Work: Step Two

Now we can create a workflow definition for our primary workflow. We’ll use the workflow type PurchCORNotifyDeliveryDue, also known as a “Delivery due date notification workflow”. To create this type of workflow definition, go to Procurement and sourcing/Setup/Procurement and sourcing workflows. Add any miscellaneous workflow elements, then add a subworkflow element.

> Select the properties of the subworkflow element
> Under the “Subworkflow to invoke” heading, select the subworkflow we defined in Step 1

Sure enough, we can use workflow type PurchTableTemplate as a subworkflow object without receiving our document key error!

Defining a subworkflow without the document key error

Defining a subworkflow without the document key error

Summary

Today we implemented subworkflows based on the PurchLine and PurchTable table objects. If this is “new territory” for you, I’d suggest that you begin your own attempts using these objects. Once you become familiar with how they work you can move forward and create your own workflow definitions.

I hope you find this exercise useful. Due to the amount of spam I receive, I no longer allow comments on my posts, but I do accept LinkedIn connections. Please connect with me here if you’re interested. If you like photography, please visit my main website.

Happy coding!