Monday, 29 September 2025

How to encrypt parameter data with Global::editEncryptedField()

Sometimes there is a requirement to encrypt parameter data like passwords, API tokens or other types of secrets. The question is, how is that done in Dynamics 365 Finance and Operations (F&O) without using third party tools?

Well, it's actually straight forward in F&O. You can perform encryption on a table field by using the standard Global::editEncryptedField() method. The base data type of your table field must be of type container. Any other type won't work.

Important

Each F&O environment has it's own encryption key. So, when moving data from the production environment to UAT, Test or Dev, the enrypted data will be obsolete in the new environment. You will have to manually enter the data again in F&O, or export the encrypted data before moving the database, and import the encrypted data after the database movement. The reason for this is because the data was encrypted using the production environments encryption key. The encrypted data cannot be decrypted with the encrpytion key of another environment, only with the encryption key of the environment where it was encrypted.

Steps

1. Add a field of type container to your table. Set the extended data type to EncryptedField.

2. On your table, add an edit method, see code example below.

3. Add a password field to your form and set the data source to your table and data method to your edit method.

Code example

If you want to have a look at an example in the standard application. Have a look at the SysEmailSMTPPassword table and form.

public edit Password editUserPassword(boolean _set, Password _password)
{
    return Global::editEncryptedField(this, _password, fieldNum(YourTableName, Password), _set);
}

Saturday, 27 September 2025

X++ native types vs objects - the fundamental difference you need to understand

In X++ programming, there are simple variable types and more complex types. Native types are primitive variable types like int, str, real, etc. Simple types can be used to store specific variable values of types such as string, number, integer, and so on.

There are also more advanced types in X++. One of them is classes. A class is a representation of a real-life object, like a customer, vendor, product, bank, person, and so on. It can be anything that requires more than a single value, unlike a simple type. In fact, a class uses simple types to store the data in the object. A class can be instantiated to create an instance or object of that class. The object reference is stored in a variable just like simple types, but there is a fundamental difference between simple types and objects.

The biggest difference between these two types that I want to make clear is that object variables are references to objects. So, two different variables can point to the same object. If you change something in the object using the first variable, those changes will reflect in the second variable, and vice versa. It is key to understand this concept because in X++, this is something that is common in the application.

Code examples

Example 1. Native types

Two native type variables assigned the same value. One is changed but different values are printed to the output.

public static void main(Args _args)
{
    real len1, len2;

    len1 = 1.23;
    len2 = len1;
    len2 = 2;
    
    info(strFmt('len1: %1', len1));
    info(strFmt('len2: %1', len2));
}

// Generated output:
//    len2: 2
//    len1: 1.23

Example 2. Object types

Two variables pointing to the same object, or referencing the same object. I am using tables in this example to make it a bit simpler. Tables are actually classes and you don't have to instantiate them like classes.

public static void main(Args _args)
{
    CustTable custTable1, custTable2;

    custTable1.AccountNum = 'C1001';
    custTable1.CustGroup = 'ABC';
    
    custTable2 = custTable1; // assign object 1 to 2. object 2 is now referencing 1, both are pointing to the object 1.
    
    custTable2.CustGroup = 'XYZ'; // change customer group of object 2, object 1 will also reflect this change because it is pointing to the same object.
    
    info(strFmt('custTable1.AccountNum: %1', custTable1.AccountNum));
    info(strFmt('custTable1.CustGroup: %1', custTable1.CustGroup));
    info(strFmt('custTable2.AccountNum: %1', custTable2.AccountNum));
    info(strFmt('custTable2.CustGroup: %1', custTable2.CustGroup));
}

// Generated output:
//    custTable1.AccountNum: C1001
//    custTable1.CustGroup: XYZ
//    custTable2.AccountNum: C1001
//    custTable2.CustGroup: XYZ

Auto submit workflow using X++

Workflows in Dynamics 365 Finance and Operations (F&O) are used to add approval steps to a business process flow. For example, in the purchase-to-pay flow, if approval is enabled and the workflow is configured, assigning a new or different bank account to a vendor account is subject to approval. This means that in F&O, an approver must approve the new bank account before payments can be made to the vendor's new bank account. I highly recommend this vendor bank approval workflow to all my clients, by the way. So, if a user manually assigns a new or different bank account to a vendor in F&O, the system requires approval by an approver.

In the field, one of my clients has an integration (built by a partner, not me) from an external application where the vendors and bank accounts are created and then sent to F&O. The vendors and vendor bank accounts are imported into F&O using a custom framework. The custom framework in F&O creates the vendors and vendor bank accounts and assigns the vendor bank account to the vendor. After this assignment of the bank account to the vendor account, the vendor account is blocked for payment, and the system requires approval via workflow.

Because of the sheer volume that is processed, it's not feasible for someone to manually submit all those new vendors and vendor bank accounts to the workflow. It's not possible to submit them in bulk; they have to be submitted individually.

So, how do we solve this without increased risk? By creating a small customization using X++ and the SysOperation framework. The customization uses the X++ Workflow::activateFromWorkflowType method and automatically submits the vendor and vendor bank accounts to the approval workflow.

Note: The records are only submitted to the workflow, not approved. The approval part of the flow remains and has to be done by an authorized workflow approver in F&O. In this instance, we can identify the source of the data as "external," so we filter the data so that only the externally created data is handled by this customization. The approval is still manual not automatic due to the risk.

To conclude, data can be submitted to the workflow using the Workflow::activateFromWorkflowType method and approvals should not be automated because of the risk.

Code

The code below is the core of my solution. With the SysOperation framework, I always use a minimum of 3 classes. The code below extends the vendor and vend bank account tables and the methods are in the business logic class. If you need more detailed information about what I mean with "3 classes", have a look at Multi selection on forms with the SysOperation framework.

VendTable extension method

public boolean avCanSubmitToWorkflow()
{
    boolean ret = true;

    if (!this.RecId)
    {
        ret = false;
    }

    if (this.WorkflowState == VendTableChangeProposalWorkflowState::NotSubmitted)
    {
        VendTableChangeProposal changeProposal;
        
        changeProposal.disableCache(true);

        select firstOnly RecId from changeProposal
            where changeProposal.VendTable == this.RecId;

        if (!changeProposal)
        {
            ret = false;
        }
    }
    else
    {
        ret = false;
    }

    return ret;
}

VendBankAccount extension method

public boolean avCanSubmitToWorkflow()
{
    boolean ret = false;

    if (this.WorkflowState == VendBankAccountChangeProposalWorkflowState::NotSubmitted && this.requiresApproval())
    {
        ret = true;
    }

    return ret;
}

Vendor code

private Counter runVendors()
{
    Counter cntr;
    QueryRun queryRun;
    VendTable vendTable, vendTableUpdate;

    queryRun = new QueryRun(this.buildQueryVendTable());

    while (queryRun.next())
    {
        vendTable = queryRun.get(tableNum(VendTable));

        if (vendTable.avCanSubmitToWorkflow())
        {
            if (Workflow::activateFromWorkflowType(workFlowTypeStr(VendTableChangeProposalWorkflow), vendTable.RecId, "Auto submit approve manually", NoYes::No))
            {
                update_recordset vendTableUpdate
                    setting WorkflowState = VendTableChangeProposalWorkflowState::Submitted
                        where vendTableUpdate.RecId == vendTable.RecId  &&
                              vendTableUpdate.WorkflowState == VendTableChangeProposalWorkflowState::NotSubmitted;
                cntr++;
            }
        }
    }

    return cntr;
}

Vendor bank account code

private Counter runVendBankAccounts()
{
    Counter cntr;
    QueryRun queryRun;
    VendBankAccount vendBankAccount, vendBankAccountUpdate;

    queryRun = new QueryRun(this.buildQueryVendBankAcc());

    while (queryRun.next())
    {
        vendBankAccount = queryRun.get(tableNum(VendBankAccount));

        if (vendBankAccount.avCanSubmitToWorkflow())
        {
            if (Workflow::activateFromWorkflowType(workFlowTypeStr(VendBankAccountChangeProposalTemplate), vendBankAccount.RecId, "Auto submit approve manually", NoYes::No))
            {
                update_recordset vendBankAccountUpdate
                    setting WorkflowState = VendBankAccountChangeProposalWorkflowState::Submitted
                        where vendBankAccountUpdate.RecId == vendBankAccount.RecId &&
                              vendBankAccountUpdate.WorkflowState == VendBankAccountChangeProposalWorkflowState::NotSubmitted;
                cntr++;
            }
        }
    }

    return cntr;
}

Saturday, 30 August 2025

Custom menu items licensing caveat

In Dynamics 365 Finance and Operations (F&O) menu items are used to launch forms, operations or processes, reports and more. When a developer creates new form, runnable class or report, a custom menu item is also created to launch that particular new form, runnable class or report.

In the application, menu items have two licensing properties:

  • Maintain User License
  • View User License

When a new custom menu item is created, the default value of the Maintain User License and View User License properties defaults to None. At first glance, this fine and is not changed by most developers. In fact, this is dangerous because but the None value is automatically translated Enterprise or Operations in most cases unless specified otherwise by the underlying AOT elements. So for customisations, it always translates to Enterprise.

The important message here is: on your custom menu items, don't leave the value of the Maintain User License and View User License properties to None, always set the Maintain User License and View User License properties to Universal. Unless your custom menu item is referring to a standard element of course, then you can set it accordingly.

Saturday, 23 August 2025

How to view data in a temporary table using SQL Server Management Studio

Temporary tables are used quite often in Dynamics 365 Finance and Operations (F&O) and you can find them everywhere in the standard code and forms. There are different types of temporary tables but in this article we are using the TempDB type. The main advantage of the TempDB type is that you can join them with regular tables because they physically exist in the database.

Every time a temporary table is instantiated in F&O, an actual table is created in the SQL Server tempdb database. The name of the table is unique each time. When the temporary table buffer in F&O goes out of scope, the data in the temporary table is discarded and the table is automatically deleted at some point.

Sometimes when you are troubleshooting code that's using a temporary table, you need to view the contents of a temporary. This cannot be done easily like a regular table using the F&O table browser or SQL Server Management Studio (SSMS). So this begs the question, how do you actually view the data of a temporary table?

The answer is by using the X++ getPhysicalTableName method of the intantiated temporary table. This retrieves the physical SQL Server table name of the temporary table. The most basic method is to temporarily add a line of code using the info method to display the table name. You can then use the table name to query the database with SSMS and view the data as long as the temporary table buffer is still in scope in F&O.

Warning: don't do this in production! The above is only applicable for dev and sandbox environments. This article is for education purposes and does not explain the best possible way to get the table name for your situation. To make thing a bit more elegant, you could write the table name to a log table (e.g. SysExceptionTable) instead of displaying a message in the infolog and you could do execute the line of code for your user account only. This way users won't notice it's there.

Get the physical SQL table name in code

public class MyTmpTableTest
{
    public static void main(Args _args)
    {
        TmpRecIdFilter tmpRecId; //note: TableType must be TempDB
    
        //add some dummy records to the temp table.
        for (Counter cntr = 1; cntr < 50; cntr++)
        {
            tmpRecId.RefRecId = cntr;
            tmpRecId.insert();
        }

        //show the temp table name. this is the only line of code you need,
        //the rest of the code here is just for this test class.
        info(tmpRecId.getPhysicalTableName());

        //for this test class we use a work around to keep temp table bufffer
        //in scope so that the data can be viewed using SSMS.
        //as soon he main method exists and the temp table buffer is out of
        //scope, the data will be lost in the table.
        Box::infoOnceModal('Table name', 'Table name', tmpRecId.getPhysicalTableName(), curUserId());
    }
}

Viewing the data in SQL Server Management Studio

  • Get the table name.
  • Connect to the F&O database using SSMS.
  • Open a new query with the table name form the first step e.g. "select * from [your_temp_table_name_here]"

Saturday, 9 August 2025

Get the last workflow comment

The code snippet below retrieves the last workflow comment of a workflow using the workflow correlation ID.

One of my customers requested a custom workflow with a separate approval e-mail message in Outlook using a custom template in F&O. In the email message, they wanted the full details from F&O including the comment of the person who submitted the workflow for approval.

To answer a question about this on the Microsoft F&O forum, I found this code in my code archive and thought it might be worth sharing.

public static str getLastTrackingComment(WorkflowCorrelationId _workflowCorrelationId)
{
    WorkflowTrackingTable           trackingTable;
    WorkflowTrackingCommentTable    trackingCommentTable;

    if (_workflowCorrelationId)
    {
        trackingTable = Workflow::findLastWorkflowTrackingRecord(_workflowCorrelationId);

        if (trackingTable)
        {
            trackingCommentTable = WorkflowTrackingCommentTable::findTrackingRecId(trackingTable.RecId);
        }
    }

    return trackingCommentTable.Comment;
}

Sunday, 13 July 2025

Use a custom form as a dialog in a custom runnable class

Sometimes there is a requirement to perform a simple operation in Dynamics 365 Finance and Operations using some limited input from the user before the operation is executed.

A real world example of custom records and a custom form to view, edit and process "records" Due to customer privacy, I can't use the real names of the records and will just refer to the data a "record". The requirement is to update a single field of a read only record. The record is not allowed to be edited in the form because of its status. In the form datasource active method, there is code to check the status of the record and sets the datasource allowEdit property according to the status of the record. However, due to certain factors, sometimes the record is not processed in time and the date of the record is not current anymore, it's now in the past. This creates a catch 22. This record cannot be processed because the date is in the past and the date cannot be updated because it's read only due to the status. The solution is, the clicks on a menu item button on the form to change the date of the record. When the user click the menu item button, a dialog is displayed showing a date field. The use can only choose a date in the future. The validation on the form makes sure a valid date is selected when the user clicks in the OK button.

This article recommends some basic level of code knowledge and desing patterns and doesn't go into detail and inner workings of the code. However, it demonstrates a simple pattern for a runnable class using a custom form dialog.

Recommended code knowlege:

  • You know what a runnable class is
  • You have basic knowledge of how the runnable class pattern works
  • You know how to create a form of type dialog and know ho to add input fields on the form
  • You know how to add validation on the form to validate the user input data
  • You know how to ge variable values in code from a form object of the input fields of the form

What you need to do to implement the pattern (in a nutshell):

  • Create your own runnable class with a static main method, run and prompt methods
  • Create your own form (type dialog) with required input fields on the form.
  • Add an OK button to the form that calls the forms closeOK method.
  • Add an Cancel button to the form that just closes the form whithout doing anything
  • Copy the code below to your own runnable class.
  • Change the class name on the main method to your own class
  • Change the form name in the prompt method to your own own form name
  • Retrieve the input values from the form object in the prompt method if user has clicked the OK button
  • Finally, implement the business logic in the run method

Runnable class code

public final class MyCustomRunnableClassWithCustomFormAsDialog
{
    private FormRun mFormRun;
    //add input form class variables
    
    public void run()
    {
        //add logic here.
    }
    
    public boolean prompt()
    {
        Object obj;
        
        mFormRun = classFactory.formRunClass(new Args(formStr(MyCustomFormDialog))); // Change form name.
        mFormRun.init();
        mFormRun.run();
        mFormRun.wait();
        
        if (mFormRun.closedOk()) //did the user click OK?
        {
            obj = mFormRun;
            //retrieve form variables values from form obj and set class variable here.
        }
        
        return mFormRun.closedOk();
    }
    
    public static void main(Args _args)
    {
        MyCustomRunnableClassWithCustomFormAsDialog operation = new MyCustomRunnableClassWithCustomFormAsDialog();
        
        if (operation.prompt())
        {
            operation.run();
        }
    }    
}

Saturday, 12 July 2025

Extended Data Types

This is another article I wrote ages ago that was originally published on Axaptapedia, which no longer exists. I found a copy online thought it was worth sharing as a blog as most of it remains relevant for Dynamics 365 Finance and Operations.

I have not edited the content but I left out a part about creating EDTs in code by an another author who added that later.

Side note: An extended data type is also referred to a an EDT.

Okay, enough of that. Let's start!

Introduction

Extended data types are very important and if used correctly, very powerful. An extended data type is a user-defined definition of a primitive data type. The following primitive data types can be extended: boolean, integer, real, string, date and container.

Inheritance

Name is a standard string EDT that represents the name of a person, company or object. If a property on Name is changed, all EDT's that inherit from it will automatically reflect the changed value. For example if the standard length is not long enough, it can be increased and all child EDT's will automatically be increased along with it. All database fields, forms and reports where the EDT is used, will also reflect the changed property.

Properties

Some of the properties that can be modified are StringSize, Alignment, DisplayLength, Label and HelpText.

Number sequences

When creating a number sequence, an extended data type is required to associate the number sequence with.

Advantages

1. Consistency in data model, forms and reports.

2. Automatic lookups (if table relation property is set).

3. Improve readability in code.

Sunday, 29 June 2025

Use PowerShell to search for labels fast

Here is a quick and simple method I use a lot to search for labels using PowerShell. It's seriously fast!

Let's start.

1. Open PowerShell and change the current directory to your LocalPackages folder. Change the path according to your system. Make sure you have enough permission on the local packages directory. This is why I always open PowerShell as Administrator.

cd C:\Users\mrbean\AppData\Local\Microsoft\Dynamics365\10.0.1935.92\PackagesLocalDirectory

2. Let's search for the label "Invoice" using the PowerShell "findstr" command. The command below will search in all the en-US label files.

findstr /s /l /i /n /E /c:"=Invoice" *en-US.label.txt

Explanation of the command arguments:

  • /s: Searches subdirectories.
  • /l: Performs a literal search (case-sensitive by default).
  • /i: Makes the search case-insensitive.
  • /n: Displays line numbers in results.
  • /E: Matches the pattern at the end of a line.
  • /c:"=Invoice": Searches for the exact string =Invoice.
  • *en-US.label.txt: Targets all files with the en-US.label.txt extension.

Note: The "/E" argument in the PowerShell command returns all lines of text that end with "Invoice". Because we are dealing with Dynamics 365 Finance label files that have and ID and label text separated with an "=" I've included "=" in the search. The causes the command to return the exact labels we search for. Copy and paste the label ID from the results.



Thursday, 29 May 2025

Methods as table fields in workflow designer

The problem is that by default, only table fields are visible in the workflow designer and sometimes it is required to use business logic to drive the workflow.

The solution to this problem is to add custom parm methods to the workflow document class of the workflow in question. The example below is from one of my custom workflows. I have simplified the code a bit for this example.

If you are working with a standard workflow, you could add your own parm methods in a class extension of the workflow document class.

The parm methods must have three arguments. In the example below, the workflow is based on SalesTable (don't ask ne why). So the TableId and RecId passed to the parm method by the system is related to the current SalesTable record. You will have to modify this to find the related record in your workflow of course.

Note:

  • Add methods to the workflow document class (class extending WorkflowDocument).
  • The parm method must have three arguments: CompanyId, TableId and RecId.
  • The method name must start with "parm".

Example


    public class MyApprovalWorkflowDocument extends WorkflowDocument
    {
        public MyEnum parmHasCreditLine(CompanyId _companyId, TableId _tableId, RecId _recId)
        {
            SalesTable salesTable;
            MyEnum hasCreditLine; //custom base NoYes enum with label which will be visible in the workflow designer

            changeCompany(_companyId)
            {
                salesTable = SalesTable::findRecId(_recId);

                if (salesTable)
                {
                    if (salesTable.type().myHasCreditLine())
                    {
                        hasCreditLine = MyEnum::Yes;
                    }
                    else
                    {
                        hasCreditLine = MyEnum::No;
                    }
                }
            }

            return hasCreditLine;
        }
    }