Implementing Staged Sync with Dataverse Plugins and Transaction Pipelines 

By Sandip Paul  •  August 31, 2026  •  81 Views

Building Staged Sync with Dataverse Plugins

Part 2 of 3: Technical Deep Dive – C# Plugins & Event Execution 

Introduction: Why Dataverse Plugins Power This Pattern 

In Part 1: Dynamics 365 Integration Pattern: How to Automate ERP/CRM Sync While Preventing Operational Chaos, we introduced the Staged Sync with Conditional Approval pattern. Now we’ll dive into the server-side implementation that makes it work: Dynamics 365 Dataverse plugins

Why plugins (not Power Automate):

Capability Plugins Power Automate 
Synchronous execution ✅ Real-time, transaction-safe ❌ Async only 
Transaction participation ✅ Rollback if validation fails ❌ Separate operations 
Pre-Image access ✅ Built-in old values ⚠️ Must query separately 
Complex decision logic ✅ Full C# capabilities ⚠️ Limited expressions 
Performance at scale ✅ Optimized for 1000s/day ⚠️ Can hit throttling limits 
Cascading updates ✅ Same transaction ❌ Multiple async operations 

Recommendation: Use plugins for transactional integrity and decision logic. Use Power Automate for orchestration and external integrations. 

For scenarios that need to integrate Dynamics 365 with external ERP systems, plugins can also initiate processing through Azure Functions.

Dataverse Plugin Architecture Overview

Execution Pipeline Stages 

The Dataverse plugin execution pipeline includes multiple stages where custom logic can run:

Dataverse plugin execution pipeline stages

⚠️ Transaction Safety: PreOperation and PostOperation execute within the same database transaction. If PostOperation throws an exception (e.g., cascade update fails due to missing work order), the entire transaction rolls back– including the PreOperation changes. This ensures atomicity: either all changes succeed, or none do. 

This type of interception is also useful beyond data synchronization, as demonstrated by this PreOperation plugin pattern in Dynamics 365 for controlling email-tracking behavior.

Our Two-Plugin Strategy 

Plugin 1: PreOperation (Decision Logic) 

  • Intercepts Update message on contoso_demand entity
  • Analyzes impact (bookings, labor gaps, urgency)
  • Decides: Auto-approve OR stage for manual review
  • Modifies transaction (stages values, sets flags, logs decision)

Plugin 2: PostOperation (Cascading Logic) 

  • Executes after demand is updated (approved)
  • Cascades changes to Work Order, Tier Dates, Tasks, Bookings
  • Recalculates dependencies
  • Logs success

💡 Why This Matters: If your cascade logic in PostOperation fails (e.g., query returns no work order), wrap your code in try/catch and decide: throw exception (rollback entire transaction) or log error and continue (partial success). For this pattern, we throw exceptions to maintain data consistency. 

Plugin 1: PreOperation Decision Logic 

Registration Configuration 

Entity: contoso_demand 
Message: Update 
Stage: PreOperation (20) 
Filtering Attributes: contoso_crd, contoso_mcsd, contoso_new_crd, contoso_new_mcsd, contoso_approval_flag 
Pre-Image: All attributes (alias: “PreImage”) 
Execution Mode: Synchronous 

Why filtering attributes: Plugin only executes when these specific fields change, optimizing performance. 

Why Pre-Image: Provides access to old field values before the update commits.

Core Implementation 

using Microsoft.Xrm.Sdk; 
using Microsoft.Xrm.Sdk.Query; 
using System; 
 
public class DemandUpdatePreOperation : IPlugin 
{ 
    public void Execute(IServiceProvider serviceProvider) 
    { 
        // 1. SETUP: Get context and services 
        var context = (IPluginExecutionContext)serviceProvider 
            .GetService(typeof(IPluginExecutionContext)); 
        var serviceFactory = (IOrganizationServiceFactory)serviceProvider 
            .GetService(typeof(IOrganizationServiceFactory)); 
        var service = serviceFactory.CreateOrganizationService(context.UserId); 
        var tracingService = (ITracingService)serviceProvider 
            .GetService(typeof(ITracingService)); 
 
        // 2. VALIDATE: Ensure we’re in the right context 
        if (context.MessageName != “Update” || 
            context.PrimaryEntityName != “contoso_demand”) 
            return; 
 
        // 2b. DEPTH GUARD: Prevent infinite loops from cascade updates 
        if (context.Depth > 1) 
        { 
            tracingService.Trace($”Depth > 1 ({context.Depth}). Skipping to prevent recursion.”); 
            return; 
        } 
 
        // 3. GET ENTITIES: Target and Pre-Image 
        Entity target = (Entity)context.InputParameters[“Target”]; 
        Entity preImage = context.PreEntityImages.Contains(“PreImage”) 
            ? context.PreEntityImages[“PreImage”] 
            : null; 
 
        if (preImage == null) 
        { 
            tracingService.Trace(“PreImage not found. Exiting.”); 
            return; 
        } 
 
        // 4. CONTEXT ANALYSIS: Who initiated this change? 
        Guid userId = context.InitiatingUserId; 
        bool isServiceAccount = IsServiceOrDataLoadAccount(userId, service); 
 
        // 5. FIELD DETECTION: Which fields are changing? 
        bool isCRDUpdate = target.Contains(“contoso_crd”); 
        bool isMCSDUpdate = target.Contains(“contoso_mcsd”); 
        bool isApprovalFlagUpdate = target.Contains(“contoso_approval_flag”); 
 
        // 6. APPROVAL FLOW: User approving staged changes 
        if (isApprovalFlagUpdate && target.GetAttributeValue<bool>(“contoso_approval_flag”)) 
        { 
            HandleApproval(target, preImage, service, tracingService); 
            return; 
        } 
 
        // 7. REJECTION FLOW: User declining staged changes 
        if (isApprovalFlagUpdate && !target.GetAttributeValue<bool>(“contoso_approval_flag”)) 
        { 
            HandleRejection(target, preImage, service, tracingService); 
            return; 
        } 
 
        // 8. SERVICE ACCOUNT FLOW: Conditional approval logic 
        if (isServiceAccount && (isCRDUpdate || isMCSDUpdate)) 
        { 
            ApplyConditionalApprovalLogic(target, preImage, service, tracingService); 
        } 
 
        // 9. USER EDIT FLOW: Allow immediate propagation 
        // (No special handling needed – user is approver) 
    } 
 
    // Helper: Identify service accounts 
    private bool IsServiceOrDataLoadAccount(Guid userId, IOrganizationService service) 
    { 
        // Query systemuser to check if user has specific role or name pattern 
        Entity user = service.Retrieve(“systemuser”, userId, 
            new ColumnSet(“fullname”, “domainname”)); 
 
        string domainName = user.GetAttributeValue<string>(“domainname”); 
 
        // Check for service account patterns (customize for your org) 
        return domainName.Contains(“svc_”) || 
               domainName.Contains(“integration”) || 
               domainName.Equals(“INTEGRATIONUSER”, StringComparison.OrdinalIgnoreCase); 
    } 
 
    // Core Logic: Conditional Approval 
    private void ApplyConditionalApprovalLogic( 
        Entity target, 
        Entity preImage, 
        IOrganizationService service, 
        ITracingService tracingService) 
    { 
        tracingService.Trace(“Applying conditional approval logic…”); 
 
        // Get demand ID and related work order 
        Guid demandId = preImage.Id; 
        Guid? workOrderId = GetRelatedWorkOrderId(demandId, service); 
 
        if (workOrderId == null) 
        { 
            tracingService.Trace(“No work order found. Auto-approving.”); 
            SetAutoApprovalFields(target); 
            return; 
        } 
 
        // IMPACT ANALYSIS 
        bool hasBookings = CheckForActiveBookings(workOrderId.Value, service); 
        bool hasLaborGaps = CheckForActiveLaborGaps(demandId, service); 
        bool isApproachingDeadline = CheckDeadlineProximity(workOrderId.Value, service); 
 
        tracingService.Trace($”Impact Analysis: Bookings={hasBookings}, Gaps={hasLaborGaps}, Urgent={isApproachingDeadline}”); 
 
        // DECISION LOGIC 
        bool autoApprove = (!hasBookings && !hasLaborGaps) || isApproachingDeadline; 
 
        if (autoApprove) 
        { 
            // AUTO-APPROVE: Set tracking fields 
            SetAutoApprovalFields(target); 
            LogAutoPropagationSuccess(preImage, target, tracingService); 
        } 
        else 
        { 
            // STAGE FOR REVIEW: Move to staging fields 
            StageForManualReview(target, preImage, service, tracingService); 
            LogAutoPropagationBlocked(preImage, target, tracingService); 
        } 
    } 
 
    // Impact Query: Active Bookings 
    private bool CheckForActiveBookings(Guid workOrderId, IOrganizationService service) 
    { 
        var query = new QueryExpression(“bookableresourcebooking”); 
        query.Criteria.AddCondition(“statecode”, ConditionOperator.Equal, 0); // Active 
        query.Criteria.AddCondition(“msdyn_workorder”, ConditionOperator.Equal, workOrderId); 
        query.TopCount = 1; // Only need to know if ANY exist 
 
        EntityCollection results = service.RetrieveMultiple(query); 
        return results.Entities.Count > 0; 
    } 
 
    // Impact Query: Active Labor Gaps 
    private bool CheckForActiveLaborGaps(Guid demandId, IOrganizationService service) 
    { 
        var query = new QueryExpression(“contoso_labor_gap”); 
        query.Criteria.AddCondition(“contoso_demand”, ConditionOperator.Equal, demandId); 
        query.Criteria.AddCondition(“contoso_gap_status”, ConditionOperator.NotIn, 
            new object[] { 808240002, 808240003 }); // Not Closed/Cancelled 
        query.TopCount = 1; 
 
        EntityCollection results = service.RetrieveMultiple(query); 
        return results.Entities.Count > 0; 
    } 
 
    // Impact Query: Deadline Proximity 
    private bool CheckDeadlineProximity(Guid workOrderId, IOrganizationService service) 
    { 
        Entity workOrder = service.Retrieve(“msdyn_workorder”, workOrderId, 
            new ColumnSet(“contoso_move_in_date”)); 
 
        if (!workOrder.Contains(“contoso_move_in_date”)) 
            return false; 
 
        DateTime moveInDate = workOrder.GetAttributeValue<DateTime>(“contoso_move_in_date”); 
        int daysUntilMoveIn = (moveInDate DateTime.UtcNow).Days; 
 
        return daysUntilMoveIn <= 15; // Configurable threshold 
    } 
 
    // Set Auto-Approval Tracking Fields 
    private void SetAutoApprovalFields(Entity target) 
    { 
        target[“contoso_auto_propagated”] = true; 
        target[“contoso_auto_propagated_date”] = DateTime.UtcNow; 
 
        // Increment counter (if exists in pre-image) 
        // target[“contoso_auto_propagation_count”] = currentCount + 1; 
 
        target[“contoso_approval_required”] = false; 
    } 
 
    // Stage for Manual Review 
    private void StageForManualReview( 
        Entity target, 
        Entity preImage, 
        IOrganizationService service, 
        ITracingService tracingService) 
    { 
        tracingService.Trace(“Staging for manual review…”); 
 
        // Move CRD to staging field 
        if (target.Contains(“contoso_crd”)) 
        { 
            DateTime newCRD = target.GetAttributeValue<DateTime>(“contoso_crd”); 
            target[“contoso_new_crd”] = newCRD; 
            target.Attributes.Remove(“contoso_crd”); // Prevent operational field update 
        } 
 
        // Move MCSD to staging field 
        if (target.Contains(“contoso_mcsd”)) 
        { 
            DateTime newMCSD = target.GetAttributeValue<DateTime>(“contoso_mcsd”); 
            target[“contoso_new_mcsd”] = newMCSD; 
            target.Attributes.Remove(“contoso_mcsd”); // Prevent operational field update 
        } 
 
        // Set notification flag (triggers UI alert) 
        target[“contoso_approval_required”] = true; 
 
        // Send notification (optional: App Notification API or custom logic) 
        // SendApprovalNotification(preImage.Id, service); 
    } 
 
    // Handle User Approval 
    private void HandleApproval( 
        Entity target, 
        Entity preImage, 
        IOrganizationService service, 
        ITracingService tracingService) 
    { 
        tracingService.Trace(“User approved staged changes.”); 
 
        // Copy staged values to operational fields 
        if (preImage.Contains(“contoso_new_crd”)) 
        { 
            target[“contoso_crd”] = preImage.GetAttributeValue<DateTime>(“contoso_new_crd”); 
            target[“contoso_new_crd”] = null; // Clear staging field 
        } 
 
        if (preImage.Contains(“contoso_new_mcsd”)) 
        { 
            target[“contoso_mcsd”] = preImage.GetAttributeValue<DateTime>(“contoso_new_mcsd”); 
            target[“contoso_new_mcsd”] = null; // Clear staging field 
        } 
 
        // Clear notification flag 
        target[“contoso_approval_required”] = false; 
 
        tracingService.Trace(“Approval complete. Cascade will occur in PostOperation.”); 
    } 
 
    // Handle User Rejection 
    private void HandleRejection( 
        Entity target, 
        Entity preImage, 
        IOrganizationService service, 
        ITracingService tracingService) 
    { 
        tracingService.Trace(“User declined staged changes.”); 
 
        // Clear staging fields only (keep operational unchanged) 
        target[“contoso_new_crd”] = null; 
        target[“contoso_new_mcsd”] = null; 
        target[“contoso_approval_required”] = false; 
 
        tracingService.Trace(“Rejection complete. No cascade needed.”); 
    } 
 
    // Logging: Success 
    private void LogAutoPropagationSuccess( 
        Entity preImage, 
        Entity target, 
        ITracingService tracingService) 
    { 
        tracingService.Trace(“=== AUTO-PROPAGATION SUCCESS ===”); 
        tracingService.Trace($”Demand: {preImage.Id}”); 
        if (target.Contains(“contoso_crd”)) 
        { 
            DateTime? oldCRD = preImage.GetAttributeValue<DateTime?>(“contoso_crd”); 
            DateTime newCRD = target.GetAttributeValue<DateTime>(“contoso_crd”); 
            tracingService.Trace($”CRD: {oldCRD} → {newCRD}”); 
        } 
        tracingService.Trace(“=== END AUTO-PROPAGATION SUCCESS ===”); 
    } 
 
    // Logging: Blocked 
    private void LogAutoPropagationBlocked( 
        Entity preImage, 
        Entity target, 
        ITracingService tracingService) 
    { 
        tracingService.Trace(“=== AUTO-PROPAGATION BLOCKED ===”); 
        tracingService.Trace($”Demand: {preImage.Id}”); 
        tracingService.Trace(“Reason: Has active bookings or labor gaps”); 
        tracingService.Trace(“Action: Staged to new_crd, notification sent”); 
        tracingService.Trace(“=== END AUTO-PROPAGATION BLOCKED ===”); 
    } 
 
    // Helper: Get Related Work Order 
    private Guid? GetRelatedWorkOrderId(Guid demandId, IOrganizationService service) 
    { 
        var query = new QueryExpression(“msdyn_workorder”); 
        query.Criteria.AddCondition(“contoso_demand”, ConditionOperator.Equal, demandId); 
        query.ColumnSet = new ColumnSet(“msdyn_workorderid”); 
        query.TopCount = 1; 
 
        EntityCollection results = service.RetrieveMultiple(query); 
        if (results.Entities.Count > 0) 
            return results.Entities[0].Id; 
 
        return null; 
    } 
} 

⚠️ Critical: Depth Guard Protection 

Why context.Depth > 1 check is essential

Scenario: PostOperation plugin updates Work Order → Work Order plugin fires → Work Order plugin updates Demand → This plugin fires again = infinite loop

Solution: Depth property tracks how many times the execution pipeline has been entered. 
Depth = 1: Initial user/service account update (process normally) 
Depth = 2+: Plugin triggered by another plugin (skip to prevent recursion) 

Alternative Approach: Use context.ParentContext to check if the initiating user is the same as the current plugin execution. But Depth guard is simpler and more reliable. 

Debugging Tip: If you see Depth = 8 in Plugin Trace Log, you have an infinite loop. Check your PostOperation cascade logic for circular references. 

Plugin 2: PostOperation Cascade Logic 

Registration Configuration 

Entity: contoso_demand 
Message: Update 
Stage: PostOperation (40) 
Filtering Attributes: contoso_crd, contoso_mcsd, contoso_asd 
Execution Mode: Synchronous 

Core Implementation 

public class DemandUpdatePostOperation : IPlugin 
{ 
    public void Execute(IServiceProvider serviceProvider) 
    { 
        var context = (IPluginExecutionContext)serviceProvider 
            .GetService(typeof(IPluginExecutionContext)); 
        var serviceFactory = (IOrganizationServiceFactory)serviceProvider 
            .GetService(typeof(IOrganizationServiceFactory)); 
        var service = serviceFactory.CreateOrganizationService(context.UserId); 
        var tracingService = (ITracingService)serviceProvider 
            .GetService(typeof(ITracingService)); 
 
        if (context.MessageName != “Update” || 
            context.PrimaryEntityName != “contoso_demand”) 
            return; 
 
        // Get updated demand (Post-Image) 
        Guid demandId = context.PrimaryEntityId; 
        Entity demand = service.Retrieve(“contoso_demand”, demandId, 
            new ColumnSet(“contoso_crd”, “contoso_mcsd”, “contoso_asd”)); 
 
        tracingService.Trace($”Cascading updates for demand {demandId}…”); 
 
        // Get related work order 
        Guid? workOrderId = GetRelatedWorkOrderId(demandId, service); 
        if (workOrderId == null) 
        { 
            tracingService.Trace(“No work order found. Exiting cascade.”); 
            return; 
        } 
 
        // UPDATE WORK ORDER 
        Entity workOrderUpdate = new Entity(“msdyn_workorder”, workOrderId.Value); 
 
        // Copy dates from demand to work order 
        if (demand.Contains(“contoso_crd”)) 
            workOrderUpdate[“contoso_crd”] = demand[“contoso_crd”]; 
        if (demand.Contains(“contoso_mcsd”)) 
            workOrderUpdate[“contoso_mcsd”] = demand[“contoso_mcsd”]; 
        if (demand.Contains(“contoso_asd”)) 
            workOrderUpdate[“contoso_asd”] = demand[“contoso_asd”]; 
 
        // Calculate shipping date (MCSD or CRD, whichever is populated) 
        DateTime? shippingDate = CalculateShippingDate(demand); 
        if (shippingDate.HasValue) 
            workOrderUpdate[“contoso_ship_date”] = shippingDate.Value; 
 
        // Calculate move-in date (ship date + estimated ship days) 
        DateTime? moveInDate = CalculateMoveInDate(workOrderUpdate, workOrderId.Value, service); 
        if (moveInDate.HasValue) 
            workOrderUpdate[“contoso_move_in_date”] = moveInDate.Value; 
 
        service.Update(workOrderUpdate); 
        tracingService.Trace(“Work order updated.”); 
 
        // RECALCULATE TIER DATES 
        RecalculateTierDates(workOrderId.Value, moveInDate, service, tracingService); 
 
        // UPDATE RESOURCE WORK ORDER SERVICE TASKS (RWOSTs) 
        UpdateRWOSTDates(workOrderId.Value, service, tracingService); 
 
        tracingService.Trace(“Cascade complete.”); 
    } 
 
    private DateTime? CalculateShippingDate(Entity demand) 
    { 
        // Priority: MCSD > CRD 
        if (demand.Contains(“contoso_mcsd”)) 
            return demand.GetAttributeValue<DateTime>(“contoso_mcsd”); 
        if (demand.Contains(“contoso_crd”)) 
            return demand.GetAttributeValue<DateTime>(“contoso_crd”); 
        return null; 
    } 
 
    private DateTime? CalculateMoveInDate( 
        Entity workOrderUpdate, 
        Guid workOrderId, 
        IOrganizationService service) 
    { 
        if (!workOrderUpdate.Contains(“contoso_ship_date”)) 
            return null; 
 
        DateTime shipDate = workOrderUpdate.GetAttributeValue<DateTime>(“contoso_ship_date”); 
 
        // Get estimated ship days from work order 
        Entity workOrder = service.Retrieve(“msdyn_workorder”, workOrderId, 
            new ColumnSet(“contoso_estimated_ship_days”)); 
 
        int shipDays = workOrder.GetAttributeValue<int>(“contoso_estimated_ship_days”); 
        if (shipDays == 0) 
            shipDays = 14; // Default 
 
        return shipDate.AddDays(shipDays); 
    } 
 
    private void RecalculateTierDates( 
        Guid workOrderId, 
        DateTime? moveInDate, 
        IOrganizationService service, 
        ITracingService tracingService) 
    { 
        if (!moveInDate.HasValue) 
        { 
            tracingService.Trace(“No move-in date. Skipping tier date calculation.”); 
            return; 
        } 
 
        // Get tier durations from incident type or work order 
        // (Simplified – actual logic queries incident type) 
        int tier0Duration = 7;  // days 
        int tier1Duration = 14; // days 
        int tier2Duration = 7;  // days 
 
        Entity workOrderUpdate = new Entity(“msdyn_workorder”, workOrderId); 
 
        // Tier 2 starts on move-in date 
        DateTime tier2Start = moveInDate.Value; 
        workOrderUpdate[“contoso_tier2_planned_start_date”] = tier2Start; 
 
        // Tier 1 starts (tier2Duration) days before Tier 2 
        DateTime tier1Start = tier2Start.AddDays(-tier2Duration); 
        workOrderUpdate[“contoso_tier1_planned_start_date”] = tier1Start; 
 
        // Tier 0 starts (tier1Duration) days before Tier 1 
        DateTime tier0Start = tier1Start.AddDays(-tier1Duration); 
        workOrderUpdate[“contoso_tier0_planned_start_date”] = tier0Start; 
 
        service.Update(workOrderUpdate); 
        tracingService.Trace($”Tier dates updated: T0={tier0Start}, T1={tier1Start}, T2={tier2Start}”); 
    } 
 
    private void UpdateRWOSTDates( 
        Guid workOrderId, 
        IOrganizationService service, 
        ITracingService tracingService) 
    { 
        // Query all RWOSTs for this work order 
        var query = new QueryExpression(“msdyn_resourcerequirement”); 
        query.Criteria.AddCondition(“msdyn_workorder”, ConditionOperator.Equal, workOrderId); 
        query.ColumnSet = new ColumnSet(“msdyn_resourcerequirementid”, “contoso_tier_type”); 
 
        EntityCollection rwosts = service.RetrieveMultiple(query); 
        tracingService.Trace($”Found {rwosts.Entities.Count} RWOSTs to update.”); 
 
        // Get tier start dates from work order 
        Entity workOrder = service.Retrieve(“msdyn_workorder”, workOrderId, 
            new ColumnSet(“contoso_tier0_planned_start_date”, 
                          “contoso_tier1_planned_start_date”, 
                          “contoso_tier2_planned_start_date”)); 
 
        foreach (Entity rwost in rwosts.Entities) 
        { 
            int tierType = rwost.GetAttributeValue<OptionSetValue>(“contoso_tier_type”)?.Value ?? 1; 
 
            DateTime? tierStartDate = null; 
            if (tierType == 0) // Tier 0 
                tierStartDate = workOrder.GetAttributeValue<DateTime?>(“contoso_tier0_planned_start_date”); 
            else if (tierType == 1) // Tier 1 
                tierStartDate = workOrder.GetAttributeValue<DateTime?>(“contoso_tier1_planned_start_date”); 
            else if (tierType == 2) // Tier 2 
                tierStartDate = workOrder.GetAttributeValue<DateTime?>(“contoso_tier2_planned_start_date”); 
 
            if (tierStartDate.HasValue) 
            { 
                Entity rwostUpdate = new Entity(rwost.LogicalName, rwost.Id); 
                rwostUpdate[“msdyn_fromdate”] = tierStartDate.Value; 
                // Calculate end date based on task duration 
                // rwostUpdate[“msdyn_todate”] = tierStartDate.Value.AddDays(duration); 
                service.Update(rwostUpdate); 
            } 
        } 
 
        tracingService.Trace(“RWOST dates updated.”); 
    } 
 
    private Guid? GetRelatedWorkOrderId(Guid demandId, IOrganizationService service) 
    { 
        var query = new QueryExpression(“msdyn_workorder”); 
        query.Criteria.AddCondition(“contoso_demand”, ConditionOperator.Equal, demandId); 
        query.ColumnSet = new ColumnSet(“msdyn_workorderid”); 
        query.TopCount = 1; 
 
        EntityCollection results = service.RetrieveMultiple(query); 
        return results.Entities.Count > 0 ? results.Entities[0].Id : null; 
    } 
} 

Key Takeaways 

  • PreOperation for decision logic: Intercept, analyze, stage or approve
  • PostOperation for cascading: Propagate approved changes to dependencies
  • Pre-Image essential: Access to old values for comparison and logging
  • Filtering attributes: Performance optimization, only execute when relevant fields change
  • Comprehensive logging: Plugin Trace Log is your audit trail and debugging tool
  • Service account pattern: Critical for distinguishing system vs. user changes

Next Steps 

Continue Reading

The Part-3 article is coming soon.

  • Part 3: Building an Impact-Aware Approval PCF Control with React & Fluent UI 

Download Full Blueprint

The full downloadable blueprint is coming soon.

  • Enterprise Integration Blueprint (PDF) – Complete implementation guide with advanced topics 

Join the Conversation

Sandip Paul

Sandip Paul

Sandip Paul is a Technical Architect at Netwoven based in the bay area. He has over 13 years of experience in software development and consulting working with both large and small customers. He is experienced in all the three Microsoft clouds: Office 365, Dynamics 365 and Azure. Sandip has worked with Netwoven for over 10 years building scalable systems using Microsoft technologies. He specializes in design and implementation of SharePoint, .NET, and Frontend technologies. Sandip holds a Bachelor of Technology degree in Computer Science from West Bengal University of Technology, Kolkata.

Leave a comment

Your email address will not be published. Required fields are marked *