What are deep links and how to generate them in Dynamics 365 Finance & Operations?

December 19, 2024

Deep links in Dynamics 365 Finance & Operations offer a powerful way to enhance navigation and user experience. By creating URLs that directly lead to specific forms, records, or pages within the application, deep links save time, reduce complexity, and enable seamless access to critical data.

Whether you’re an administrator configuring workflows or a developer streamlining processes, understanding how to use deep links effectively can significantly improve efficiency and usability.

This blog explores the concept of deep links, how they are created, and practical applications of deep links in Dynamics 365 Finance and Operations, along with best practices to maximize their impact.

Deep links are URLs designed to take users directly to specific pages or functionalities in Dynamics 365 F&O. Unlike generic links to the system’s homepage, deep links incorporate parameters that point to specific forms, records, or actions. They are essential for:

  • Reducing time spent navigating menus.
  • Enhancing productivity in workflows and reporting.
  • Facilitating integrations between D365 and external systems.

Deep links can optionally include a data context, allowing the form to display filtered or specific data when accessed. The URL Generator facilitates scenarios such as embedding links in reports, emails, or external applications, enabling users to quickly navigate to the desired forms or data using the generated links.

How do deep links work in Dynamics 365 Finance and Operations?

A typical deep link in Dynamics 365 F&O consists of:

  1. Base URL: The environment URL
    (e.g., https://myd365environment.cloud.dynamics.com).
  2. Menu Item (mi): The name of the form or action
    (e.g., CustTable for the customer form).
  3. Parameters: Additional query string parameters to filter or locate specific records
    (e.g., CustAccount=1234). 

Example structure: https:///?cmp=&mi=&

Example link:
https://myd365environment.cloud.dynamics.com/?cmp=USMF&mi=CustTable&CustAccount=001234

This step-by-step guide walks you through the process of creating deep links in Dynamics 365 Finance and Operations for seamless navigation to specific forms and data.

Step 1: Identify the target form or action

Determine the form you want the link to open. Use the AOT in Visual Studio or the Application Explorer to locate the relevant menu item name. For example, to link to the Customer Details form, the menu item name is CustTable.

Step 2: Construct the URL

Combine your environment’s base URL with the required parameters. Here’s how:

  • cmp: The legal entity or company code.
  • mi: The menu item name.
  • Additional parameters as needed, such as CustAccount for a customer account or VendAccount for vendors.

For example: https://myd365environment.cloud.dynamics.com/?cmp=USMF&mi=CustTable&CustAccount=001234

Step 3: Test thelLink

Paste the constructed URL into a browser. Then, verify that it navigates to the intended form with the correct filters applied.

Here are some examples of different types of deep links:

  1. Workflow notifications
    Provide approvers with a direct link to their pending tasks.
    Example: https://myd365environment.cloud.dynamics.com/?mi=SysWorkflowWorkItemAction&WorkItemId=5678
  2. Emailalerts
    Include deep links in notification emails to direct users to specific records or reports.
  3. Custom reports
    Use deep links to share filtered reports directly with team members.
    Example: https://myd365environment.cloud.dynamics.com/?mi=CustAgingReport&CustGroup=Retail
  4. External integrations
    Share links with external stakeholders for direct access (ensuring proper security settings).

Best practices for generating deep links

Here are some of the best practices for generating deep links in Dynamics 365 Finance and Operations to ensure efficiency, security, and optimal performance.

  1. Maintain security: Ensure users have permissions to access the forms or records linked. Avoid exposing sensitive information in the URL parameters.
  2. Use descriptive parameters: Keep parameters meaningful to make links easy to read and maintain.
  3. Regularly validate links: Test links periodically to ensure they function after system updates or changes.
  4. Troubleshooting common issues in deep link creation: Learn how to identify and resolve frequent challenges when generating deep links in Dynamics 365 Finance and Operations for seamless navigation:
  5. Incorrect form or no access: Verify the mi value matches the menu item in the AOT. Ensure the user has necessary permissions.
  6. Missing data: Check that all required parameters are included in the URL.
  7. Environment URL issues: Confirm the base URL matches the current environment (e.g., production or sandbox).

Practical Implementation of deep links

Here is an example of how to add deep links to customer account IDon excel sheet.

First, create a batch job class in Dynamics 365 Finance and Operations that generates the Excel file with customer details and includes deep links for customer account IDs.

public class ExportCustomerDetailsToExcelBatch extends SysOperationServiceController
{
private str fileGenerationMessage;

public void createAndSendExcelFile(str fileName, Container headers, Container data)
{
    System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
    boolean dataFound = conLen(data) > 0;

    if (dataFound)
    {
        try
        {
            // Create an ExcelPackage using the memory stream
            using (var package = new OfficeOpenXml.ExcelPackage(memoryStream))
            {
                // Initialize row count
                int row = 1;

                // Add worksheet name
                OfficeOpenXml.ExcelWorksheet worksheet = package.Workbook.Worksheets.Add(fileName);
                OfficeOpenXml.ExcelRange cell;

                // Write headers using get_Item approach
                int headerIndex = 1;
                while (headerIndex <= conLen(headers))
                {
                    cell = worksheet.Cells.get_Item(row, headerIndex);  // Accessing cell using get_Item
                    cell.Value = conPeek(headers, headerIndex);
                    headerIndex++;
                }

                // Write data using get_Item approach
                int dataIndex = 1;
                while (dataIndex <= conLen(data))
                {
                    row++;
                    container rowData = conPeek(data, dataIndex);
                    int columnIndex = 1;
                    while (columnIndex <= conLen(rowData))
                    {
                        cell = worksheet.Cells.get_Item(row, columnIndex);  // Accessing cell using get_Item
                        cell.Value = conPeek(rowData, columnIndex);

                        if (columnIndex == 1) 
                        {
                            str recordId = conPeek(rowData, 1); // Customer ID
                            str url = CFZURLGeneration::generateDynamicsURL(
                                menuItemDisplayStr(CustTable), 
                                MenuItemType::Display, 
                                formDataSourceStr(CustTable, CustTable), 
                                fieldstr(CustTable, AccountNum), 
                                recordId);
                            cell.Hyperlink = new System.Uri(url); // Convert string to System.Uri
                        }

                        columnIndex++;
                    }
                    dataIndex++;
                }

                // Save the Excel package to memory stream
                package.Save();
            }

            // Reset memory stream position to 0 before sending
            memoryStream.Position = 0;

            // Send the Excel file to the user
            File::SendFileToUser(memoryStream, fileName);

            // Set a message indicating that the file has been generated and sent to the user
            this.fileGenerationMessage = "Please see the downloaded file for progress.";
        }
        catch (Exception::Error)
        {
            // Set an error message if file generation fails
            this.fileGenerationMessage = "Failed to generate the file.";
        }
    }
    else
    {
        // Set a message indicating no data found
        this.fileGenerationMessage = "No data found for the given criteria.";
    }
}

public str getFileGenerationMessage()
{
    return this.fileGenerationMessage;
}

}

For downloading the excel file you need to write this class:


//downloading an Excel file containing customer data
// TODO: Comment out this class in the future. The current requirement is to open the form in the same tab with the same filtration logic, instead of downloading data in Excel.

/// /// This class represents a Copilot action for downloading an Excel file containing customer aging data /// based on a specified number of days. It retrieves customer aging data, inserts relevant account numbers /// into the CFZCopilotCustomerOnHold table, and generates a CSV file for download. ///
[DataContract]
[SysCopilotChatGlobalAction]
[SysCopilotChatActionDefinition(
‘MS.PA.RA.Copilot.CFZCustomerAging’, // It has to start with “MS.PA” for Identifier
‘Download Excel File containing Customer Aging by specified Days’, // Name for the copilot action
‘Present user with Download Excel File containing Customer Aging by specified Days’, // Description for the Action
menuItemActionStr(CFZCustomerAging), MenuItemType::Action)] // Secured by Menu Item
public class CFZCustomerAging extends SysCopilotChatAction
{
private int days;
private str copilotTopicConversationId;
private str customerAging;

// Input parameter that is received from Copilot Studio.
[DataMember('days'),
    SysCopilotChatActionInputParameter('Days for which Data needed.', true)]
public int parmDays(int _days = days)
{
    days = _days;
    return days;
}

// Input parameter that is received from Copilot Studio.
[DataMember('copilotTopicConversationId'),
    SysCopilotChatActionInputParameter('Conversation ID for Copilot Topic Data.', true)]
public str parmCopilotTopicConversationId(str _copilotTopicConversationId = copilotTopicConversationId)
{
    copilotTopicConversationId = _copilotTopicConversationId;
    return copilotTopicConversationId;
}

// Output parameter that is received by Copilot Studio once the action is executed.
[DataMember('customerAging'),
    SysCopilotChatActionOutputParameter('Customer Aging Data')]
public str parmCustomerAging(str _customerAging = customerAging)
{
    customerAging = _customerAging;
    return customerAging;
}

/// <summary>
/// Executes the Copilot action to gather customer aging data.
/// </summary>
/// <param name="_actionDefinition">The action definition.</param>
/// <param name="_executionContext">The execution context.</param>
public void executeAction(SysCopilotChatActionDefinitionAttribute _actionDefinition, Object _executionContext)
{

    // Initialize variables
    CustTrans custTrans;

    CFZCopilotCustomerOnHold cfzCopilotCustomerOnHold;
    CFZExportFileData fileHandler = new CFZExportFileData();

    customerAging = '';

    int daysFromCopilot = this.parmDays();
    str conversationIdFromCopilot = this.parmCopilotTopicConversationId();
    if (daysFromCopilot)
    {

        Date dateThreshold = DateTimeUtil::date(DateTimeUtil::addDays(today(), -daysFromCopilot));

        str fileName = 'Customer Aging.xlsx';

        // Prepare headers as container
        container headers = ['Customer ID', 'Customer Name', 'Invoice Number','Amount','Amount Not Settled' ,'Date','Aging Days'];

        // Prepare data as container
        Container data;
        int dataIndex  = 1;

        // Delete all data from CFZCopilotCustomerOnHold table
        ttsBegin;
        delete_from cfzCopilotCustomerOnHold;
        ttsCommit;

        // Query to retrieve customer aging data
        while select custTrans
                order by custTrans.TransDate asc
                where
                custTrans.TransDate < dateThreshold
                    && custTrans.Invoice != ""


        {
            if (custTrans.remainAmountCur() !=0) // This represents Open invoices (when blance isnot equal to zero then it means invoice is open)
            {
                // Calculate aging days
                int agingDays = today() - custTrans.TransDate;

                // Insert the AccountNum into CFZCopilotCustomerOnHold table
                ttsBegin;
                cfzCopilotCustomerOnHold.clear();
                cfzCopilotCustomerOnHold.CFZAccountNum = custTrans.AccountNum;
                cfzCopilotCustomerOnHold.CFZCopilotConversationId = conversationIdFromCopilot;
                cfzCopilotCustomerOnHold.insert();
                ttsCommit;

                Container rowData = [custTrans.AccountNum,
                        custTrans.custTableName(),
                        custTrans.Invoice,
                        custTrans.AmountMST,
                        custTrans.remainAmountCur(),
                        any2str(custTrans.TransDate),
                        agingDays];
                data = conIns(data, dataIndex , rowData);
                dataIndex++;
                // Concatenate or format customerAging string with retrieved data
                //customerAging += strFmt("Customer ID: %1, Customer Name: %2, Invoice Number: %3, Amount: %4, Date: %5\n",
                //                  custTrans.AccountNum,custTrans.custTableName(), custTrans.Invoice, custTrans.AmountMST, custTrans.TransDate);
            }
        }

       fileHandler.createAndSendExcelFile(fileName, headers, data);
       this.parmCustomerAging(fileHandler.getFileGenerationMessage());
    }
    else
    {
        // Handle error if dates are not provided
        this.parmCustomerAging('@CFZCopilotLabels:ERRProvideDays');
    }
}

}

Use this class for creating deep links on Customer Account ID:


// Link genration Class
using Microsoft.Dynamics.AX.Framework.Utilities;

/// /// Provides methods for constructing URLs within the Dynamics 365 environment. /// Includes functionality for generating full URLs based on menu items and optional query parameters. ///
public class CFZURLGeneration
{

/// <summary>
/// Builds a full URL for a specified menu item in the Dynamics 365 environment.
/// </summary>
/// <param name="_menuItemName">The name of the menu item to include in the URL.</param>
/// <param name="_menuItemType">The type of the menu item (e.g., action, display).</param>
/// <param name="_dataSource">Optional data source for the URL query parameters.</param>
/// <param name="_field">Optional field name for the URL query parameters.</param>
/// <param name="_value">Optional value for the URL query parameters.</param>

public static str generateDynamicsURL(MenuItemName _menuItemName, MenuItemType _menuItemtype, DataSourceName _dataSource='', FieldName _field='', str _value='')
{
    try
    {
        str baseUrl = CFZURLGeneration::getBaseUrl();  // Retrieve the base URL of the Dynamics 365 environment

        // Instantiate a new Uri object with the base URL
        System.Uri currentHost = new System.Uri(baseUrl);

        UrlHelper.UrlGenerator generator = new UrlHelper.UrlGenerator();  // Create a URL generator instance

        generator.HostUrl = currentHost.GetLeftPart(System.UriPartial::Authority);  // Set the host URL
        generator.Company = curExt();  // Set the current company context
        generator.MenuItemName = _menuItemName;  // Set the menu item name
        generator.MenuItemType = _menuItemtype;  // Set the menu item type
        generator.Partition = getCurrentPartition();  // Set the current partition
        generator.EncryptRequestQuery = true;  // Enable encryption for the request query

        if (_dataSource != '')  // Check if data source is provided
        {
            UrlHelper.RequestQueryParameterCollection requestQueryParameterCollection;  // Create a collection for query parameters

            requestQueryParameterCollection = generator.RequestQueryParameterCollection;  // Retrieve the query parameter collection
            requestQueryParameterCollection.AddRequestQueryParameter(_dataSource, _field, _value);  // Add the query parameters
        }

        System.Uri fullURI = generator.GenerateFullUrl();  // Generate the full URL

        return fullURI.AbsoluteUri;  // Return the full URL as a string
    }
    catch (Exception ::Error)
    {
        // Log or handle the exception as needed
        throw error("An error occurred while generating the URL");  // Throw an error with the exception message
    }
}

/// <summary>
/// Retrieves the base URL of the Dynamics 365 environment.
/// </summary>
/// <returns>The base URL of the Dynamics 365 environment.</returns>
private static str getBaseUrl()
{
    try{
    var env = Microsoft.Dynamics.ApplicationPlatform.Environment.EnvironmentFactory::GetApplicationEnvironment();  // Get the application environment

    // Check if the environment object is not null
    if (env)
    {
        return env.Infrastructure.HostUrl;  // Return the host URL from the environment
    }
    else
    {
        // Handle the case where the environment object is null
        throw error("Failed to retrieve the host URL from the environment.");  // Throw an error if the environment URL cannot be retrieved
    }
}
catch (Exception::Error)
{
    // Log or handle the exception as needed

    throw error("An error occurred while retrieving the base URL");  // Throw an error with the exception message
}
}

}

Summing up

Deep links in Dynamics 365 Finance & Operations are powerful tools for streamlining navigation and enhancing user experience by directly linking to specific forms or records. By leveraging these links, businesses can improve efficiency and accessibility across teams. Generating deep links is straightforward, allowing users to quickly access the information they need with just a click.

For further assistance or inquiries, feel free to contact us at marketing@confiz.com.