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