Thursday, June 26, 2025

Moving Business Process Flow Stages Backward in Dynamics 365 with a Plugin

A step-by-step guide to programmatically move BPF stages backward in Dataverse using a C# plugin

Introduction

In Microsoft Dynamics 365 and Dataverse, Business Process Flows (BPFs) guide users through predefined stages, typically moving forward. But what if you need to move backward? For example, from stage 2 to stage 1 based on a field value like "No"? The BPF designer doesn’t support backward movement natively, but a server-side plugin can make it happen. In this post, we’ll walk through how to create a plugin that moves a BPF stage backward when a condition is met, complete with code and setup instructions.

Why This Matters: Moving BPF stages backward allows dynamic process control, enabling scenarios like revisiting earlier steps based on user input, enhancing flexibility in workflows.

The Requirement

Imagine you’re working with an account entity and a BPF called "Account Process Flow." At stage 2, a choice field (new_decisionfield) determines the next step:

  • If the value is "Yes," move to stage 3.
  • If the value is "No," move back to stage 1.

Since connectors in Power Automate and the BPF designer only support forward movement, we’ll use a plugin to update the activestageid field of the BPF instance.

Step-by-Step Guide

Step 1: Gather Required Information

Before coding, collect these details:

  • BPF Stage IDs: Find the GUIDs for stage 1 and stage 3 in the processstages table using XrmToolBox or FetchXML. Example:
    • Stage 1: 12345678-1234-1234-1234-1234567890ab
    • Stage 3: 98765432-4321-4321-4321-9876543210ba
  • BPF Instance Table: Identify the logical name (e.g., accountprocessflow).
  • Field Name: Confirm the logical name of the choice field (e.g., new_decisionfield) and its option set values (e.g., Yes = 100000000, No = 100000001).

Step 2: Set Up the Development Environment

You’ll need:

  • Visual Studio (2019 or 2022).
  • Microsoft Power Platform CLI or Dynamics 365 SDK for Microsoft.Xrm.Sdk.
  • Plugin Registration Tool (via XrmToolBox or Power Platform CLI).

Create a Class Library (.NET Framework 4.6.2 or 4.7.2) and add the Microsoft.CrmSdk.CoreAssemblies NuGet package:

Install-Package Microsoft.CrmSdk.CoreAssemblies

Step 3: Write the Plugin

The plugin triggers on the Update message for the account entity, checks the new_decisionfield value, and updates the BPF’s activestageid. Here’s the code:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;

namespace BPFStageMover
{
    public class MoveBPFStage : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            // Obtain the execution context
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

            try
            {
                // Check if the event is an update on the account entity
                if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
                {
                    Entity target = (Entity)context.InputParameters["Target"];

                    // Check if decision_field is in the target entity
                    if (!target.Contains("new_decisionfield"))
                    {
                        tracingService.Trace("decision_field not updated, exiting plugin.");
                        return;
                    }

                    // Get the decision field value (OptionSetValue)
                    OptionSetValue decisionValue = target.GetAttributeValue("new_decisionfield");
                    if (decisionValue == null)
                    {
                        tracingService.Trace("decision_field is null, exiting plugin.");
                        return;
                    }

                    // Define stage GUIDs (replace with actual stage IDs from processstages table)
                    Guid stage1Id = new Guid("12345678-1234-1234-1234-1234567890ab"); // Stage 1 GUID
                    Guid stage3Id = new Guid("98765432-4321-4321-4321-9876543210ba"); // Stage 3 GUID

                    // Get the BPF instance for the account
                    Guid accountId = context.PrimaryEntityId;
                    QueryExpression query = new QueryExpression("accountprocessflow")
                    {
                        ColumnSet = new ColumnSet("activestageid"),
                        Criteria = new FilterExpression
                        {
                            Conditions =
                            {
                                new ConditionExpression("regardingobjectid", ConditionOperator.Equal, accountId)
                            }
                        }
                    };

                    EntityCollection bpfInstances = service.RetrieveMultiple(query);
                    if (bpfInstances.Entities.Count == 0)
                    {
                        tracingService.Trace("No BPF instance found for account {0}.", accountId);
                        return;
                    }

                    Entity bpfInstance = bpfInstances.Entities[0];
                    Guid currentStageId = bpfInstance.GetAttributeValue("activestageid")?.Id ?? Guid.Empty;

                    // Only proceed if the current stage is stage 2 (optional: add stage 2 GUID check)
                    // Update activestageid based on decision_field value
                    Guid newStageId = decisionValue.Value == 100000000 ? stage3Id : stage1Id; // 100000000 = Yes, 100000001 = No

                    if (currentStageId != newStageId)
                    {
                        Entity bpfUpdate = new Entity("accountprocessflow")
                        {
                            Id = bpfInstance.Id,
                            ["activestageid"] = new EntityReference("processstage", newStageId)
                        };
                        service.Update(bpfUpdate);
                        tracingService.Trace("BPF stage updated to {0} for account {1}.", newStageId, accountId);
                    }
                    else
                    {
                        tracingService.Trace("No stage change needed.");
                    }
                }
            }
            catch (Exception ex)
            {
                tracingService.Trace("Error in MoveBPFStage plugin: {0}", ex.Message);
                throw new InvalidPluginExecutionException($"Plugin error: {ex.Message}");
            }
        }
    }
}

Step 4: Build and Register the Plugin

  1. Build: Compile the solution in Visual Studio to generate the DLL (e.g., BPFStageMover.dll).
  2. Register: Use the Plugin Registration Tool to:
    • Upload the DLL and select the MoveBPFStage class.
    • Register a step:
      • Message: Update
      • Primary Entity: account
      • Filtering Attributes: new_decisionfield
      • Stage: PostOperation, Synchronous

Step 5: Test the Plugin

Update the new_decisionfield on an account record and verify the BPF stage moves to stage 3 (Yes) or stage 1 (No). Check trace logs in Dataverse if issues arise.

Pro Tip: Enable plugin tracing in Dataverse to debug issues. Use XrmToolBox to view logs in the Plugin Trace Log table.

Key Notes

  • Replace GUIDs: Update the stage GUIDs in the code with actual values from your processstages table.
  • BPF Table: Use the correct logical name for your BPF instance table (e.g., accountprocessflow).
  • Option Set Values: Adjust 100000000 (Yes) and 100000001 (No) to match your field’s values.
  • Security: Ensure the plugin runs under a user with permissions to update BPF instances.

Conclusion

By using a plugin to update the activestageid, you can overcome the BPF designer’s forward-only limitation, enabling dynamic stage movement in Dynamics 365. This solution is robust, scalable, and perfect for scenarios requiring flexible process control. Try it out, and let us know in the comments if you have questions or need further customization!

© 2025 Your Name. All rights reserved.

No comments:

Post a Comment

Power Automate Optimization: Filter Rows vs Trigger Conditions - When and Why to Use Each

Filter Rows vs Trigger Conditions in Power Automate Filter Rows vs Trigger Conditions in Power Automate Why your flow...