Implementing Audit Trails and Change Tracking in Dynamics 365 Finance: Compliance-Ready Governance Without Performance Degradation

Regulated industries operate under increasingly strict audit and compliance requirements. Financial institutions, healthcare organizations, and public companies face regular audits from regulators demanding comprehensive proof of who changed what, when, and why in their core systems. Dynamics 365 Finance provides native audit trail capabilities, but many organizations implement them incorrectly, turning audit trails into performance bottlenecks that slow down critical finance operations. Solution architects and developers need to understand how to enable governance without crippling system performance.

Understanding Dynamics 365 Native Audit Trail Capabilities

Dynamics 365 Finance provides two distinct audit mechanisms: field-level change tracking and form-level audit trails. Change tracking operates at the database level, recording every modification to tracked tables with timestamp, user identity, and old/new values. This happens automatically without application involvement. Audit trails, conversely, run at the form and business logic level, capturing business transactions rather than raw field changes. Understanding which mechanism solves which problem is foundational.

The Field-Level Change Tracking feature monitors specific columns you designate and logs modifications to the DocuRef system table. For finance, this typically means tracking General Ledger account master records, Customer master changes, Vendor master changes, and key financial transaction types. However, enabling change tracking on heavily used tables (like SalesLine or PurchLine) can create significant performance impact because the system must record every change in addition to processing the business transaction.

Form-level audit trails capture business events: posting a journal, posting an invoice, posting a shipment receipt, approving a purchase order. These events matter more for compliance purposes because they represent intentional business actions rather than incidental field changes. Most auditors care more about “who approved this invoice” than about “who changed the currency code in the address.”

Performance Implications of Audit Trail Configuration

Organizations frequently activate comprehensive audit trails expecting minimal impact, then discover unacceptable slowdowns during month-end close periods or large batch imports. The performance hit comes from multiple sources working together. Each tracked field change triggers a database insert operation, creating additional I/O during write-heavy operations. During financial period close, organizations post hundreds or thousands of adjusting journal entries, statistical adjustments, and reclassifications. When change tracking is enabled on Journal tables, every entry triggers audit logging overhead.

Batch operations amplify this impact. A single API call importing 10,000 purchase line items in a single batch transaction creates 10,000 separate audit trail entries. Without proper configuration, batch import jobs can increase runtime from 20 minutes to 90 minutes simply due to audit logging overhead. The solution is not to disable auditing but to architect it thoughtfully: segregate audit-heavy operations into distinct batches, schedule them outside peak business hours, and monitor query execution plans to understand where slowdowns occur.

Designing an Audit Architecture That Works at Scale

Effective audit implementations follow three principles: selective tracking rather than comprehensive tracking, asynchronous audit processing where possible, and clear retention policies. Rather than enabling audit trails on every table, identify which fields and transactions actually matter for your compliance requirements. This typically means tracking master record changes (GL account hierarchy, cost center masters, legal entity configurations) and transaction approvals (purchase order approvals, invoice postings, period close confirmations), while avoiding comprehensive tracking of transactional line items.

Asynchronous processing separates the audit capture from the business transaction. Instead of logging synchronously during the transaction, stage the audit record for background processing. Dynamics 365 provides audit event policies that allow you to defer audit trail generation to background batch jobs. This approach trades real-time audit visibility for system performance: the audit record exists, but it may not be immediately available for query. For most organizations, a 15-minute delay in audit trail availability is acceptable in exchange for preventing month-end close delays.

Retention policies prevent audit tables from growing unbounded. A multi-year-old implementation without retention policies can accumulate millions of audit records, slowing down queries against the audit tables themselves. Define a retention period (typically 3 to 7 years depending on industry regulations), purge audit records older than that period on a scheduled basis, and test purge jobs on production-like databases before executing them against live systems. The purge operation itself can be resource-intensive, so schedule it during maintenance windows.

Implementation Patterns for Specific Scenarios

For organizations migrating from legacy financial systems, importing historical transactions often violates audit trail expectations: the audit trail will show “migrated by user X” rather than capturing who originally created and approved the transaction. Implement historical transactions in a separate legal entity or depreciated ledger rather than mixing historical and operational data, and exclude historical import batches from comprehensive audit logging.

Purchase approval workflows benefit from form-level audit trails that capture approval gates without tracking every field change. Configuring workflow notifications to automatically create audit records when approval thresholds are reached provides audit visibility aligned with actual governance gates. This is cleaner than attempting to infer approval from change tracking data.

Multi-subsidiary consolidations and elimination transactions create audit trail complexity. When a consolidation routine automatically generates offsetting entries across multiple legal entities, the audit trail should reflect the consolidation process rather than detailed field-by-field tracking. Custom audit logic (using plugins or event handlers) can record the consolidation as a single auditable business event rather than hundreds of individual field changes.

Monitoring and Troubleshooting Audit Trail Performance

Enable audit trail logging incrementally, measuring performance impact at each step. Establish baseline metrics for period close duration, batch import duration, and API response times before enabling auditing. Enable auditing on a single high-risk table, rerun tests, and measure the performance delta. If the delta is acceptable (less than 10 percent increase), proceed to the next table. If impact exceeds tolerance, adjust the audit configuration (reduce tracked fields, enable asynchronous processing) before continuing.

Use the audit viewer in Dynamics 365 Finance to verify that audit records are being captured correctly. The audit viewer shows not just what changed, but the sequence of changes, making it possible to reconstruct user actions. If audit records show unexpected gaps (changed records with no audit trail), check whether those tables are included in your audit configuration and whether the audit policy is active for those tables.

Identify performance bottlenecks using SQL execution plans and database wait statistics. Query slowdowns during audit logging typically indicate either lock contention (multiple users simultaneously triggering audit writes to the same table) or disk I/O saturation (audit logging outpacing disk write speed). Asynchronous audit processing and scheduled maintenance windows address both causes.

Governance Without Grinding

Compliance is non-negotiable for regulated organizations, but compliance should not require sacrificing operational performance. Dynamics 365 provides the mechanisms to achieve both: selective auditing of high-risk transactions, asynchronous processing to prevent real-time performance impact, and thoughtful architecture that captures business intent rather than attempting comprehensive field-level surveillance. Organizations that architect audit trails in alignment with actual governance requirements, test performance impact, and monitor ongoing behavior deliver compliant systems that finance teams can actually use.

#DynamicsAuditTrail #FinanceCompliance #Dataverse #ChangeTracking #ERP #Governance #Dynamics365 #DynamicsFinance #EnterpriseSecurity #ComplianceArchitecture

Building Custom Dataverse Plugins for Real-Time Business Logic: Architecture Patterns and Deployment Strategies

Custom plugins remain one of the most powerful yet misunderstood levers in the Microsoft Dataverse platform. While Power Automate handles lightweight workflows and Power Apps provides UI automation, plugins solve a different problem: they execute business logic in response to Dataverse operations with the performance and security model of server-side code.

The challenge most organizations face isn’t whether to build plugins. It’s determining which problems *require* plugins versus which can be solved with lower-code alternatives. When plugins are the right answer, the next challenge becomes: how do you design, test, and deploy them in a way that scales without becoming a maintenance nightmare?

**The Real-Time Constraint Problem**

Dataverse operations happen in a transactional context. When a record is created, updated, or deleted, plugins can intercept that operation at two points: pre-operation (before the database transaction) or post-operation (after commit). This matters because pre-operation plugins can modify the data before it’s saved, validate input with business rules, or prevent the operation entirely. Post-operation plugins can trigger downstream processes once the change is safely committed.

Many organizations treat these as interchangeable. They aren’t. A pre-operation plugin that makes an external API call can block the user’s operation for as long as that call takes. A 5-second timeout becomes a frozen user interface. Scale that across hundreds of concurrent users, and you’ve created a denial-of-service vulnerability in your own system.

The architecture decision here determines whether your plugin becomes a reliable part of the system or a bottleneck that gets disabled during peak load because it’s failing too many operations.

**Three Plugin Execution Patterns**

The first pattern is synchronous validation. Pre-operation plugins should validate, transform, and enforce business rules before data is saved. They should execute in milliseconds. Any operation that takes longer—external API calls, complex calculations on large datasets, cross-system lookups—should be moved to a separate execution path. This pattern prevents bad data from ever entering the system. A pre-operation plugin validating that a customer’s credit limit isn’t exceeded, or that a purchase order references an active supplier, should complete in 50-200ms. If your logic can’t hit that target, it belongs in an async plugin.

The second pattern is asynchronous post-operation actions. After a record is safely committed, a plugin can trigger downstream effects without blocking the user’s operation. Sending notifications, updating related records in other tables, integrating with external systems, or kicking off approval workflows all belong in post-operation async plugins. These execute outside the user’s transaction and can tolerate higher latency. A 5-second external API call in a post-operation async plugin is acceptable. The same call in a pre-operation plugin is a performance problem.

The third pattern is event-driven architecture. Rather than having plugins orchestrate complex multi-step processes, plugins emit events that external systems subscribe to. A change to an opportunity triggers a plugin that publishes the event. A separate microservice (Azure Function, Logic App, or external system) consumes that event and decides what happens next. This decouples your Dataverse plugins from downstream business logic and lets you evolve downstream systems without changing your Dataverse model.

Each pattern solves a different problem. Synchronous validation makes the data layer trustworthy. Asynchronous actions keep the user’s experience snappy while still triggering necessary side effects. Event-driven architecture makes the system resilient to change.

**Registration and Filtering Strategy**

A common mistake is registering plugins on every attribute change when only a few attributes matter. If your plugin only needs to react when the Status field changes, don’t register on “all attributes.” Instead, register specifically on the Status attribute. This cuts the plugin’s execution count dramatically and improves overall system performance.

Filtering also applies to message types. A plugin doesn’t need to fire on every Create, Update, and Delete. It can fire on Update only. Combine message filtering with attribute filtering, and you’re executing your logic only when it’s actually needed.

Stage selection matters too. Pre-operation runs before validation, so it’s useful for modifying data before validation rules apply. Pre-operation 10 (earliest) runs before pre-operation 20 (later). Post-operation runs after the transaction commits and is useful for downstream actions. Each stage has a purpose; using the wrong stage adds latency where it isn’t needed.

**Testing and Deployment**

Plugin code runs server-side, with access to the organization’s data and permissions. A poorly written plugin can corrupt data at scale. Testing is not optional.

Unit tests should cover the logic of your plugin independent of Dataverse. Integration tests should run the plugin against a Dataverse environment and verify it behaves correctly. Deployment should happen through an Application Lifecycle Management (ALM) pipeline: develop in a sandbox, test in a staging environment, deploy to production only after validation.

One pattern that simplifies testing is moving the core business logic out of the plugin class and into a separate, testable library. The plugin becomes a thin wrapper: extract the input from the Dataverse SDK, call your logic library, and apply the result back to the execution context. This makes unit testing trivial and keeps your plugin logic independent of Dataverse framework details.

**Common Mistakes to Avoid**

Unintended recursion is a classic issue. A plugin modifies a record, which triggers the same plugin again, which modifies the record again, and so on until the system reaches a depth limit and throws an error. Prevention requires tracking execution depth and skipping logic if the current execution is a plugin-triggered update rather than a user-initiated change.

Another mistake is sharing state across plugin executions. Plugin instances aren’t persistent; each execution creates a new instance. Trying to use static variables to pass data between plugin invocations leads to race conditions and unpredictable behavior.

Excessive synchronous API calls create bottlenecks. If your plugin makes 10 external API calls sequentially, and each takes 500ms, your plugin runs for 5 seconds. Users experience 5-second operations for record updates. Move those calls to post-operation async or to a separate integration service.

**Moving Forward**

Dataverse plugins are not obsolete. They’re essential for enforcing business logic at the data layer, but they’re not the only tool. Modern Dataverse implementations combine plugins for critical synchronous validation, Power Automate flows for multi-step post-operation actions, and external microservices for complex integrations. The plugin’s job becomes focused: validate and transform data in real time. Anything else should run asynchronously or outside the transaction.

Routeget Technologies’ experience scaling plugin architectures across large Dynamics 365 deployments shows that the teams that succeed are those that treat plugin development as seriously as they treat any server-side code—with rigorous testing, clear ownership, and architectural intent behind every decision.

#DataversePluginDevelopment #DynamicsArchitecture #CloudIntegration #ServerSideLogic #ALMPipeline #PluginArchitecture #EnterpriseDataManagement #Extensibility

Power BI RLS at Scale: When Dynamic Row-Level Security Becomes a Bottleneck

Row-level security in Power BI seems straightforward during a pilot. You create a role-based filter on a User table, join it to your fact data, and suddenly users see only their assigned data. It works flawlessly with 10 users and a few hundred thousand rows. Then you move to production with 2,000 users and suddenly your dashboards refresh in 4 hours instead of 30 minutes, and the data warehouse query that used to run in 3 seconds now times out.

The culprit is almost always RLS implementation design, not hardware constraints or data volume. Most teams build RLS the way tutorials show: dynamic username matching against a lookup table. This pattern works until you hit scale. Once you do, the filter becomes computationally expensive, query engines struggle with the cardinality of the role definition, and your entire semantic model refresh becomes a bottleneck.

This isn’t a limitation of Power BI itself. It’s an architectural decision hiding inside seemingly innocent DAX formulas and role definitions. Get the architecture right early, and RLS scales smoothly. Get it wrong, and scaling becomes a major undertaking midway through your rollout.

How RLS Performance Actually Degrades

Most Power BI RLS implementations work by joining a Users table (containing user ID, email, username, and assigned business unit, region, or account) to your fact tables through a filtering relationship. In the role definition, you write something like:

[User_Email] = USERNAME()

This tells Power BI to filter the fact table to rows where the User_Email column matches the currently logged-in user’s email. Straightforward. But what happens underneath is what matters.

When a user opens a dashboard, Power BI doesn’t execute the RLS filter once and cache the result. Instead, the filter is embedded into every query the dashboard runs. If your dashboard has five visualizations, each visualization generates a separate query, and each query includes the RLS filter evaluated against your role definition. If your role evaluates USERNAME() against 50,000 user records to find a match, that’s 50,000 comparisons per query, times five queries, times however many concurrent users are refreshing reports at the same time.

This scales linearly downward with user count. Ten users means fifty thousand comparisons times five queries times ten simultaneous users. Two thousand users means fifty thousand comparisons times five queries times two thousand simultaneous users. The mathematics break quickly.

Additionally, most organizations layer RLS filters. You might have one RLS role for regional filtering, another for account-level filtering, and another for team assignment. Each layer adds another lookup and another set of comparisons. Three layers of RLS against large lookup tables is not uncommon, and it’s where the model refresh starts to visibly slow.

The Hidden Cost of Dynamic Username Matching

The USERNAME() function is convenient, but it’s also the root of many performance problems. USERNAME() returns the current user’s email or ID from Azure Active Directory (or whatever authentication system you’re using). That’s fine at a conceptual level. But in practice, USERNAME() is evaluated on every single row context change inside your queries.

Consider a common scenario: you have a Salesforce sync where you pull the current User table nightly. 30,000 user records. Each user has an assigned Region and an assigned Account list. Someone opens a Power BI dashboard with four visualizations. Power BI needs to execute four queries. Each query runs with RLS applied:

[UserEmail] = USERNAME()

Power BI’s query engine translates this into a filter condition in the underlying SQL or MDX query sent to your data source (whether that’s Azure SQL, Synapse, or a direct lake connection). The data engine evaluates this for every partition, for every table relationship, for every row that might match. With thirty thousand users and large fact tables, this isn’t trivial.

Then someone else logs in, and the same process repeats with a different USERNAME() value. In production, you might have hundreds of concurrent users, and each is generating this lookup and comparison overhead.

Design Patterns That Scale

The solution isn’t to eliminate RLS. It’s to change how you implement it so the filtering happens efficiently.

Pattern 1: Pre-computed Role Assignment in the Fact Table

Instead of dynamic lookup at query time, assign roles when data is loaded. Before your fact table lands in Power BI, compute which users have access to which rows and store that assignment as a column in the table itself. For example, add a column called AllowedUsers containing the user IDs or emails who can see that row. Then, in your RLS role:

[AllowedUsers] IN VALUES(...)

This is faster because you’re filtering on a pre-computed, indexed column rather than doing a lookup join at query time. The downside is that changes to user access require a full data refresh, not a runtime reevaluation.

Pattern 2: RLS at the Aggregation Layer, Not the Fact Layer

Many teams apply RLS to their most detailed fact tables. This is the most expensive place to filter. Instead, build your RLS semantics against aggregated or summarized tables. For example, if your fact table has order line-item level detail with thousands of rows per user, create a summary table at the order level, apply RLS there, and relate the detailed facts read-only. Users can’t drill below what they’re allowed to see.

Pattern 3: Role-Based Access Through a Mapping Table, Not a Lookup

Build a dedicated Role Mapping table that pre-computes which users belong to which roles. Populate it at load time, not query time. Then apply RLS against this mapping table using a relationship-based filter rather than a DAX formula:

Relationships: RoleMapping[UserID] -> FactTable[UserID]<br />RLS: [Role] = "SalesRep"

This delegates the filtering to relationship traversal, which database engines optimize heavily.

Pattern 4: Object-Level Security for Entire Semantic Model Sections

If individual row filtering becomes too expensive even with optimizations, consider object-level security. Hide entire tables, measures, or columns from certain roles. A regional sales manager might not see cost of goods or procurement details at all, not because the rows are filtered, but because those tables are invisible to them. This is blunter than row-level filtering, but it’s far more performant for large organizations.

Implementation Checklist

Before deploying RLS to production, verify:

1. Measure baseline query performance without RLS

Run your dashboards against a copy of the model with RLS disabled. How long does a refresh take? If it takes more than 30 minutes, RLS is not your bottleneck—your data model or query patterns are. Fix those first. If baseline is acceptable, proceed.

2. Identify your largest lookup table

User tables, regions, accounts, teams—whichever table you’ll use for role filtering. Count its rows. If it’s over 50,000, consider Pattern 1 or Pattern 3. If it’s under 10,000, standard dynamic username matching might be acceptable, but test at production scale first.

3. Test RLS with production user counts, not pilot counts

Set up a test model with realistic user counts. If you’ll have 2,000 users in production, create test roles for 500 of them and measure refresh time and query response. Don’t assume linear scaling. Cardinality issues often behave worse than linear.

4. Plan for re-architecture if needed

If you’re currently using dynamic USERNAME() matching against large tables, plan a migration to one of the patterns above. Don’t do this on the fly in production. Test the new architecture on a copy of the model first, measure performance improvement, then cut over.

5. Monitor refresh time trend

As your user base grows, your refresh times should not grow proportionally. If they do, your RLS implementation is hitting a wall. Catch this early, before users start complaining about stale data.

Conclusion

Power BI RLS is essential for organizations sharing a single semantic model across teams with different access rights. But RLS performance degradation is one of the most common production failures in Power BI deployments, and it’s entirely preventable with the right design pattern from the start. The difference between a dynamic username lookup against a 50,000-user table and a pre-computed role assignment is the difference between a 30-minute refresh and a 4-hour refresh once you scale.

Choose your RLS pattern based on your expected user count and role complexity before you build. Test at production scale before you roll out. And if you inherit a struggling RLS implementation, re-architecture isn’t a failure—it’s a necessary step toward a sustainable system.

#PowerBIRLS #RowLevelSecurity #PowerBIPerformance #EnterpriseAnalytics #DataSecurity #DAXOptimization #Dataverse #PerformanceOptimization

Dynamics 365’s Free Sentiment Analysis Has a Blind Spot. AI Builder Sentiment Analysis Can Close It, at a Cost.

Customer experience team reviewing a sentiment analytics dashboard on a large monitor

A customer experience director at a mid-market distributor recently asked her Dynamics 365 partner for something that sounded simple: one ranked list of accounts trending toward churn, built from every piece of customer feedback the company already collects. Support conversation transcripts, service case notes, post-delivery satisfaction surveys, and the freeform comments account managers leave on Opportunity records all already exist somewhere in her Dataverse environment. She assumed the sentiment analysis her team had seen in Customer Service demos would simply read all of it. It doesn’t. Dynamics 365’s built-in sentiment analysis is real, it works well, and it costs nothing extra to use, but it only sees what happens inside a case or an email thread. Everything else on her list, the surveys, the account notes, the reseller complaints logged in a spreadsheet, sits entirely outside its reach. Closing that gap means turning to AI Builder sentiment analysis, a separate tool, and that is a deliberate build, not a setting an admin flips on.

That distinction gets lost in a lot of planning conversations, because “Dynamics 365 has AI-powered sentiment analysis” sounds like one complete capability. It is actually two separate tools, the sentiment monitoring built into Customer Service and the standalone AI Builder sentiment analysis model, with different reach, different cost structures, and different governance obligations. Confusing the two is how a CX program either overpromises what a pilot will cover or underuses a tool that is already paid for.

What Dynamics 365 Already Gives You, Free

Native sentiment analysis in Customer Service is enabled by default and configured through the admin center’s Insights settings, where a supervisor can turn on real-time monitoring in a few clicks. Once it’s on, service representatives see a live sentiment reading during an active omnichannel conversation, with configurable alert thresholds ranging from “slightly negative” up to “very negative,” and supervisors get their own notification settings so they can step in on a conversation that’s deteriorating before it becomes an escalation. Historical scoring rolls up into Omnichannel Insights dashboards for after-the-fact analysis, and the feature covers more than forty languages out of the box. As of a May 2026 release, Microsoft extended the same underlying capability to email: incoming customer messages now get scored for tone before a representative drafts a reply, and that email-level sentiment rolls into a unified case sentiment score alongside conversation data. None of this requires AI Builder, a Power Automate flow, or a separate licensing conversation. It is part of the platform a Customer Service organization is already paying for.

Where the Built-In Tool Stops Seeing Customers

The scope of that native feature is narrower than most stakeholders assume. It is wired specifically to the Case, Conversation, and Email Message records inside Customer Service, and nothing that lives outside that data model gets touched by it. A satisfaction survey captured through a Power Pages portal or a Customer Voice form sits in its own tables. Freeform notes an account manager keeps on an Opportunity or Account record are just text fields, invisible to a sentiment engine that only looks at cases. A complaint a reseller emails to a partner manager, rather than filing through a support channel, never becomes a case at all. Social commentary or review-site feedback pulled in through a connector has no case or email record to attach a score to. In the distributor’s situation, most of the feedback her CX team actually cared about lived in exactly these gaps, which is why the demo she’d seen didn’t match the reality of her own data.

Solution architect building an automated sentiment scoring flow on a laptop

What AI Builder Sentiment Analysis Adds That the Native Tool Doesn’t

This is the actual role for AI Builder sentiment analysis, and it is worth being precise about what kind of tool it is. It’s a standalone prebuilt model, not a Customer Service feature, callable from a Power Automate flow or through a Power Fx formula in a canvas app, and it has no built-in awareness of cases or email at all. It accepts up to 5,120 characters of text per call and returns a document-level classification of positive, negative, neutral, or mixed sentiment, along with a confidence score and, if needed, a sentence-by-sentence breakdown. It supports a solid set of languages, including English, Spanish, French, German, Italian, Portuguese, Dutch, Japanese, Korean, Chinese, Hindi, Turkish, and Norwegian. Because it’s just a callable action with no opinion about where its input comes from, a flow can point it at a Customer Voice survey response table, a custom “Account Note” column, a SharePoint list, or an inbox that never turns into a formal case. That flexibility, not any difference in accuracy, is the actual reason to reach for it: it lets an organization build the one unified sentiment signal the CX director wanted, rather than settling for whatever slice of the customer relationship happens to fall inside a Case record.

The Rate Limit and Credit Shift Worth Budgeting For

Two constraints deserve attention before anyone commits a rollout date. The first is technical: the sentiment analysis, language detection, and key phrase extraction prebuilt models share a single throttle of 400 calls per 60 seconds within an environment. That ceiling is comfortable for scoring cases and emails as they trickle in throughout the day, but it gets hit fast the moment someone tries to backfill a year of Opportunity notes or process five thousand quarterly survey responses in one overnight batch. This is usually the first thing that breaks when a “score everything” mandate meets real data volume, and it should shape the flow design from the start, through pacing, delay steps, or triggering only on new and changed records instead of reprocessing history, rather than getting discovered during a failed pilot.

The second constraint is financial, and it connects to a transition already underway across AI Builder. Sentiment analysis is now billed under Microsoft’s “text and generative AI tools (Basic)” capability, and its licensing is migrating from AI Builder credits to Copilot Studio credits, with AI Builder capacity add-ons and any seeded credits in premium licenses reaching end of life on November 1, 2026. Any budget built for a sentiment-expansion project running into next year should be modeled against Copilot Studio credit consumption, not against a credit pool that Microsoft has already scheduled to stop selling.

A Practical Pattern for Extending Coverage

The build itself is not complicated once the source data is identified. A Power Automate flow triggers on new or changed rows in whichever table holds the text, whether that’s a survey response, a custom account-note table, or a Power Pages-submitted complaint form, calls the AI Builder sentiment action, and writes the resulting classification and confidence score back to a column on that record. From there, a condition branch can raise a Dataverse task for the account owner when a result comes back strongly negative above a chosen confidence threshold, and a Power BI report can rank accounts by a rolling sentiment average pulled from every connected source, not just cases. That report is what actually answers the CX director’s original request. The one governance step teams tend to skip is consent: because this reaches customer-identifiable text outside the flow Customer Service already governs for case and email sentiment, legal and IT should extend the same notice-and-consent practice to whatever new source gets added, rather than assuming existing sentiment monitoring approvals already cover it.

What to Confirm Before Committing

Three things are worth verifying before committing. The inventory of where sentiment-bearing text actually lives outside the Case and Email Message tables determines whether this is a two-week Power Automate build or a multi-source integration project, so it belongs at the start, not mid-pilot. Realistic call volume needs to be estimated against that 400-call, 60-second shared limit before a rollout date gets promised to a business sponsor, because a mismatch there turns a promising pilot into an incident in its first week of production use. And finance should weigh whether the Copilot Studio credit model makes sense at the organization’s expected volume compared with calling Azure AI Language directly through a custom connector, since at higher volumes the direct route sometimes costs less and gives more control over batching, even though it takes more setup than the point-and-click AI Builder action.

Dynamics 365’s native sentiment analysis, especially now that it reaches into email as well as live conversations, already covers most of what a typical service organization needs, and it’s worth using as it stands before building anything on top of it. The gap only becomes a problem when an organization wants one sentiment signal spanning every channel a customer actually uses, most of which never turns into a case. AI Builder sentiment analysis is the right tool for that expansion, but it comes with a real throughput ceiling and a licensing model mid-transition, not a checkbox next to a feature Microsoft already includes. Teams at firms like Routeget Technologies that have wired sentiment scoring into Dataverse tables outside the standard Customer Service model tend to start with that inventory step, because it’s usually where the real scope of the project gets decided, before anyone opens Power Automate.


#AIBuilder #DynamicsCustomerService #PowerAutomate #ChurnPrevention #CustomerExperience #EnterpriseAI #Dataverse

Power Apps Delegation Limits Don’t Throw an Error. They Just Give You the Wrong Answer.

Solution architect reviewing a data table dashboard on a large monitor, illustrating enterprise data query review

A finance team at a mid-size distributor built a canvas app to flag overdue receivables. It worked perfectly in every demo. Then it went into production against a SharePoint list that had grown past 4,000 rows, and the “overdue balance” total the app displayed started coming in a little low every week, never wildly wrong, just quietly off by a shrinking or growing margin depending on how the underlying data happened to sort. Nobody got an error message. Nobody got a warning banner in the running app. The gallery rendered, the total summed, and the number was simply incomplete, because Power Apps had silently stopped looking at the data after the first 500 records.

That is the practical shape of a delegation problem, and it is one of the more consequential gaps between how canvas apps behave in the maker’s testing environment and how they behave once real data volume shows up. Power Apps delegation limits determine whether a formula gets pushed down to the data source, which processes it server-side and returns only the matching rows, or whether Power Apps has to pull a bounded chunk of records locally and evaluate the formula against that chunk alone. When delegation fails and the underlying table is larger than the configured limit, the app does not stop working. It keeps working, on a subset of the data, without saying so.

Why Power Apps delegation limits fail silently instead of crashing

A crash gets fixed immediately because someone notices. A silently truncated query gets fixed only when someone happens to reconcile the app’s output against the source system and finds a discrepancy, which in the receivables example took several weeks. For a technical team supporting Dynamics 365 or Power Platform deployments, this is the argument for treating delegation as an architecture decision made at design time, not a performance tweak applied after a slow gallery gets noticed.

The default data row limit in a canvas app is 500 records, adjustable up to a hard ceiling of 2,000 in the app’s Settings under General. Raising that number is a common instinct and a bad one to rely on as a fix, because it just moves the cliff edge further out rather than removing it; a table with 50,000 rows behaves identically whether the limit is set to 500 or 2,000; it only changes how much data has to grow before someone notices something is wrong. Microsoft’s own guidance for this reason recommends the opposite move during development: set the limit down to 1 in a test environment, which forces every non-delegable formula to reveal itself immediately with obviously wrong results, rather than leaving that discovery to production data volume.

What actually gets delegated, and what does not

Power Fx delegates a meaningful set of operations against supported data sources, including Dataverse, SharePoint Online, SQL Server, and Salesforce. Filter, Search, LookUp, and First all delegate, as do the standard comparison operators, And, Or, Not, StartsWith, EndsWith, and the core aggregates: Sum, Average, Min, Max, CountRows, and Count. Sort and SortByColumns delegate as well, with an important caveat covered below for SharePoint. The In operator delegates too, but only against a column on the base table; the moment a formula tests In against a related or lookup column, delegation breaks and the query falls back to local evaluation.

Developer typing Power Fx formula logic on a laptop with a monitor showing abstract code in the background

The non-delegable list is longer and easier to trip over than most makers expect. FirstN, Last, and LastN do not delegate. Neither does If, which means embedding conditional logic inside a Filter predicate is a common way a formula that looks delegable quietly is not. String manipulation functions including Left, Mid, Len, Lower, and Upper are non-delegable, along with Concatenate and the & operator, GroupBy, Ungroup, and type-casting functions like Text and Value. Collect and ClearCollect are non-delegable by nature, since they materialize data locally rather than querying it. A formula like Filter(Fruit, Mid(FruitName, 1, 1) = “A”) looks reasonable and reads cleanly, but because Mid cannot be pushed to the data source, it only ever evaluates against the first page of records pulled locally, meaning a record named “Pineapple” sitting past row 500 will never match a search for names starting with “P,” not because the logic is wrong, but because the function never sees that row.

SharePoint makes this worse than Dataverse does

Anyone who has built canvas apps against both SharePoint lists and Dataverse tables has felt the difference in how forgiving each source is, and the gap is documented, not just anecdotal. SharePoint does not delegate relational comparison operators (less than, greater than, not-equal) on plain text fields at all, only equality. IsBlank does not delegate against text fields either, and the commonly suggested workaround, comparing to Blank() directly, does not behave identically for genuinely empty strings versus null values, which is a subtle correctness gap worth documenting in code review rather than discovering in production.

The bigger gap sits in SharePoint’s complex column types: Choice, Lookup, Person, Group, Task Outcome, Managed Metadata, and External Data. None of these support delegated sorting, so Sort or SortByColumns against a Choice or Person field will silently fall back to sorting only the locally retrieved page. StartsWith does not work against subfields of Choice or Lookup columns. Person and Group fields only delegate on their Email and DisplayName subfields, not on other properties. A set of SharePoint system fields, including ContentType, ModerationStatus, VersionNumber, and several path-related fields, do not delegate under any function at all. UpdateIf and RemoveIf against SharePoint are handled through a batching simulation bounded by the same 500/2,000 record limit rather than a true server-side delegated operation, which matters for any flow that bulk-updates list items from within the app rather than through Power Automate.

Dataverse does not eliminate delegation limits, but it closes most of these specific gaps: relational operators work against text columns, In delegates on base-table columns consistently, and sorting against lookup and choice columns behaves as makers expect. For any canvas app expected to scale past a few thousand records with complex filtering or sorting, that difference is a legitimate factor in the SharePoint-versus-Dataverse decision, not just a licensing or governance one.

Designing around it instead of hoping around it

The most reliable pattern is to push complexity toward the data source rather than the formula bar. Where a filter needs to test a derived value, calculated columns created in Dataverse (or, with more limitation, in SharePoint) let the comparison happen against a pre-computed, delegable field instead of a runtime string function the client has to evaluate locally. Where a search needs to match across multiple text fields, Search() itself delegates and is usually a better first attempt than a hand-built Filter with concatenated conditions, since it is one of the few functions built specifically to delegate substring matching.

For teams supporting these apps in production rather than just building them, the practical safeguard is process, not just formula discipline: run the app with the row limit forced to 1 during test cycles, review any gallery, dropdown, or KPI tile driven by a filter or sort against a table that could plausibly exceed a few hundred rows in production, and treat a yellow delegation warning triangle in Power Apps Studio as a defect ticket rather than a cosmetic nag to dismiss. The warning is not a suggestion about performance. It is Power Apps telling the maker, in the only language it has, that the number on screen may not represent all of the data.

We’ve walked enough Dynamics 365 and Power Platform clients through exactly this kind of discovery, usually during a post-go-live support engagement rather than a planned architecture review, to treat delegation as a first-week design conversation on every canvas app project involving a table that is expected to grow. It is a cheap conversation to have early and an expensive one to have after a finance report has been quietly wrong for a month.


#PowerApps #PowerFx #CanvasApps #Dataverse #SharePointOnline #EnterpriseArchitecture #LowCodeDevelopment

Dataverse Plugin Packages Are Code-First Now. Your Azure DevOps Pipeline Still Has to Script Around Them.

Solutions architect and developer reviewing a Dataverse deployment pipeline diagram on an office monitor

A solutions architect on a mid-size D365 F&O implementation recently ran into a problem that had nothing to do with business logic. Her team had finally moved their plugin assemblies into proper NuGet-based Dataverse plugin packages, the newer code-first model that the Power Platform CLI now supports directly. The build worked. The package generated correctly. And then the pipeline stalled, because nobody could answer a simple question: how do you tell Azure DevOps to actually deploy the thing you just built, when Microsoft’s own build tools extension has no task for it?

That gap is worth understanding in detail, because it sits at the exact point where Dataverse plugin development is genuinely improving and where its ALM tooling has not caught up.

Solutions architect and developer reviewing a Dataverse deployment pipeline diagram on an office monitor

From Manual Registration to Code-First Packages

For most of the last decade, deploying a Dataverse plugin meant compiling a class library in Visual Studio, opening the Plugin Registration Tool, and manually selecting an assembly to register or update. It worked, but it kept plugin deployment outside the discipline that governs everything else in a mature ALM pipeline: predictable artifacts, versioned builds, and a repeatable path from commit to environment.

The Power Platform CLI changes that equation with two commands: pac plugin init, which scaffolds a plug-in class library project, and pac plugin push, which imports the resulting artifact into Dataverse. The output of a build is no longer a loose assembly file. It is a NuGet package, generated automatically in the bin\outputPackages folder on every build, containing the plugin assembly and its dependencies bundled together. That single change, treating the plugin as a proper package rather than a file you happen to compile, is what makes plugin code finally look like the rest of a modern .NET pipeline.

What Dataverse Plugin Packages Actually Solve

The practical win is dependency handling. Under the old model, any third-party or shared library your plugin referenced had to be merged into the assembly or registered separately, and both approaches created their own headaches around versioning and duplication across solutions. A plugin package bundles dependent assemblies inside the same NuGet artifact, so the whole thing deploys and updates as one unit.

Signing works differently here too, and it is worth getting the policy decision right early rather than discovering it mid-project. Microsoft’s own guidance is explicit that signing is all-or-nothing: if the primary plugin assembly is strong-name signed, every dependent assembly in the package must be signed as well. Most teams find this impractical once they start pulling in third-party libraries they do not control, which is why pac plugin init exposes a --skip-signing flag, and why it has effectively become the default choice for anyone using dependent assemblies rather than a single self-contained plugin.

The Immutability Trap

Here is where the model’s real character shows up, and where a team can get burned if nobody flags it in advance. Once a plugin package is created in Dataverse, its name and version are fixed. An attempt to change either afterward, including through the API, simply fails. Every build produces a package with a new version embedded in its filename, which means there is no such thing as “redeploy the same package again.” Each build is a new, uniquely identified artifact from Dataverse’s point of view, whether you intended that or not.

That has two consequences that matter for pipeline design. First, if a plugin update removes an assembly or a type that is still referenced by an active plugin step registration, Dataverse rejects the update outright. You have to go remove or repoint those step registrations first, then update the package, not the other way around. Second, you cannot unregister a package at all while any of its assemblies still have active step registrations attached to them. Both rules are sensible from a data-integrity standpoint, but they mean plugin retirement has to be sequenced deliberately: deregister the steps, confirm nothing else depends on the assembly, and only then remove or replace the package.

One independent practitioner account, documented on the .NET Dust blog in 2024, captured how disruptive the versioning behavior can be during ordinary iterative development, well before any pipeline is involved. Because pac plugin push has no built-in notion of “the current build,” the developer had to script around it: an MSBuild target increments the file version before compilation, a post-build event persists that value into the project file through a small helper utility, the NuGet package picks up the new version automatically, and only then does an automated pac CLI call push the exact resulting filename. What should be a single command turned into a four-step workaround, built by one engineer, to get a repeatable local deployment loop. That is a reasonable thing for an individual developer to solve on their own machine. It is not something you want reinvented independently by every team building a Dataverse ALM pipeline.

The Build Tools Gap

That last point is the one worth taking to whoever owns your ALM strategy. Microsoft’s Power Platform Build Tools extension, the standard toolkit for Dataverse pipelines in Azure DevOps and GitHub Actions, covers an extensive set of operations: importing, exporting, packing, and unpacking solutions, applying upgrades, managing environments, running the Power Platform Checker, and even a preview set of catalog-submission tasks. Reviewing the current task reference, plugin packages and pac plugin push are not among them. There is no first-class, Microsoft-supported pipeline task for deploying a plugin package, in contrast to the mature tooling that already exists for solutions.

In practice, that means every team adopting code-first plugin packages has to write its own pipeline stage: typically a script task that installs a pinned version of the PAC CLI, authenticates against the target environment (a service principal, not a personal login, for anything beyond a developer sandbox), locates the build output, and issues the push. None of that is exotic engineering. It is, however, unsupported custom scripting sitting next to a set of Microsoft-maintained tasks that get security updates, documentation, and predictable behavior across CLI versions. That asymmetry is easy to miss until a CLI update quietly changes an argument name and a script that nobody has looked at in eight months starts failing in production.

Developer typing on a laptop with a monitor showing an abstract NuGet package deployment graphic

Building This Into an ALM Pipeline Properly

A few decisions are worth making explicit rather than leaving to whichever developer happens to write the first plugin deployment script. Pin the PAC CLI version in the pipeline definition itself, not just on developer machines, so a CLI upgrade cannot silently change push behavior in production. Treat the plugin push step as its own reviewed pipeline stage with the same environment gating and approval flow as solution deployment, rather than a script someone bolted on during a sprint to unblock a release. Decide the assembly-signing policy at the team or center-of-excellence level, since the all-or-nothing signing rule effectively locks in a convention for every future package once dependent assemblies enter the picture.

Sequence retirements carefully. Before removing or consolidating an old plugin package, confirm which step registrations still point at it and clear those first, since Dataverse will otherwise block the change regardless of what your pipeline script assumes should happen. And build the versioning logic into the pipeline itself, following the pattern the .NET Dust workaround demonstrated, rather than trusting a developer’s local environment to produce a consistently versioned artifact that a shared pipeline can then find and deploy.

None of this makes the code-first plugin package model a bad idea. It is a genuine improvement over registering assemblies by hand through a desktop tool, and it brings plugin code closer to how the rest of a .NET codebase is built and shipped. The catch is that Microsoft shipped the authoring half of that story before the deployment half, and the gap between them is exactly where implementation teams are currently improvising. Firms that have already built Dataverse ALM pipelines from the ground up, Routeget Technologies among them, have generally closed that gap with a small, explicitly maintained script stage rather than an ad hoc one, which is the difference between a pipeline that survives a CLI version bump and one that quietly breaks the next time someone updates it.


#Dataverse #DataversePlugins #PowerPlatformALM #AzureDevOps #ExtensibilityAndProDev #EnterpriseArchitecture

Dataverse’s Recycle Bin Restore Reached GA. Your Business Logic Isn’t Included.

Solution architect reviewing a data recovery and system administration dashboard on a large monitor in a modern office

A bulk delete job runs at 2 a.m. against the wrong view filter. By the time anyone in operations notices, eleven thousand account records are gone, and the only recovery plan anyone can point to is a nightly export sitting in a storage account nobody has tested restoring from in two years. That scenario is why Microsoft spent nearly two years moving deleted record recovery in Dataverse from public preview to general availability in late April 2026. For solution architects and Dynamics 365 technical teams, a native Dataverse recycle bin restore capability closes a gap that has existed since the platform’s earliest days: there was never a supported, in-product way to undo a delete. Now there is one. The problem is that plenty of teams will configure it, assume it behaves like the Recycle Bin on a Windows desktop, and discover the gap between what it restores and what they actually needed restored only during a real incident.

The feature’s actual behavior is narrower and more procedural than its name suggests, and the places where it falls short map directly onto the customizations most F&O, Business Central, and Customer Engagement implementations already carry.

Solution architect reviewing a data recovery and system administration dashboard on a large monitor in a modern office

What the Dataverse Recycle Bin Restore Actually Covers

The capability itself is straightforward to enable. From the Power Platform admin center, under an environment’s Settings, Product, Features, there is a “Keep deleted Dataverse records” toggle with a retention window configurable from one to thirty days. Turn it on, and Dataverse begins tracking rows that get deleted so a System Administrator can view them under Data management, Deleted Records, and issue a Restore request against the ones worth recovering. The setting lives at the environment level, not the tenant level, so a sandbox and its corresponding production environment can carry entirely different retention windows, which matters if you’re testing destructive changes in a lower environment that doesn’t need the same thirty-day buffer production does.

The detail that trips up almost every team on first contact is that the feature is not retroactive. Only rows deleted after the setting is enabled and has finished propagating, which the documentation notes can take up to thirty minutes, become recoverable. A record deleted an hour before someone flips the toggle is gone exactly as it always was. If you’re reading this and haven’t enabled retention on a production environment yet, that thirty-minute propagation window is the argument for doing it now rather than after the incident that makes you wish you had.

Restoration happens through the Restore message, available in the SDK for both early-bound and late-bound entities, or as an action call through the Web API. The request takes a full target record rather than a bare reference, so you can override specific column values as part of the restore rather than being forced to bring back an exact snapshot. Deleted rows are also queryable directly: a FetchXml query with datasource='bin', or a QueryExpression with its DataSource property set the same way, returns rows sitting in the bin rather than the live table, which is enough to build a lightweight internal tool that surfaces what’s recoverable without routing every check through the admin UI.

The Tables That Never Show Up in the Bin

The exclusion list is where the feature’s real limits show up, and it is long enough that assuming universal coverage is a mistake worth catching before an incident rather than during one. Virtual tables are excluded entirely: if a record surfaced from an external system through a virtual table disappears, there was never anything in the bin to restore, since that row was never really “deleted” from Dataverse’s perspective so much as it stopped resolving. Elastic tables, increasingly common for high-volume telemetry, IoT-style logging, or activity history where standard table row limits become a constraint, are excluded as well. So are solution component tables, and so is any table that has grown past six hundred attributes, a ceiling that heavily customized standard entities in mature Dynamics 365 implementations can approach faster than teams expect.

Beyond those structural exclusions, Microsoft maintains a list of roughly two hundred and fifty system tables that don’t support deleted-record retention regardless of configuration: async operation records, background operation records, bulk delete failure logs, Power Automate flow run and flow log tables, solution and solution component tables, plugin assembly tables, and the full family of msdyn_ and mspp_ prefixed tables tied to first-party Dynamics 365 apps and Power Pages. None of that is arbitrary; those tables tend to be high-volume, system-managed, or tightly coupled to processes where a stale restored row could reintroduce more problems than it solves. But it does mean that if your disaster recovery narrative for a Power Pages portal assumes the recycle bin has you covered, it’s worth checking the actual exclusion list against your table inventory before that assumption ends up in a postmortem.

Abstract illustration of glowing data records being restored from an archive bin into an active data grid

Cascade Deletes Restore in Order. Custom Logic Doesn’t.

Where this feature gets genuinely interesting for anyone who has built out relationship cascading behavior is how it treats dependent records. Rows removed because of a configured cascade delete relationship, the kind set up through standard relationship behavior rather than custom code, are restorable, but not independently. You have to restore the parent record first; attempting to restore a cascaded child before its parent throws RefCannotBeRestoredRecycleBinNotFound, and the platform expects you to work back up the dependency chain in the correct order rather than picking records to restore arbitrarily.

Custom logic is a different story, and it’s the part of the feature architects need to internalize before promising anyone a one-click recovery story. If a plugin or a Power Automate flow deletes related records as a side effect of deleting the primary record, none of that custom-triggered deletion is automatically reversed by a Restore call. The recycle bin captures what Dataverse’s own relationship engine did, not what your business logic did on top of it. That means any implementation with plugins that clean up child records, cancel related cases, or cascade status changes on delete needs a mirrored restore path: a plugin registered against the Restore message, in the PostOperation stage specifically since the restored record isn’t available yet during PreOperation, that re-creates or re-links whatever the delete-time logic tore down. Skip this step and a team restores the parent record, then discovers days later that none of the related activity history, case associations, or downstream integration records came back with it, because nothing ever told them to.

A handful of other failure modes are worth knowing before you hit them live. Restoring a record whose ID has since been reused by a new record throws DuplicateExceptionRestoreRecycleBin, since Dataverse preserves the original primary key on restore rather than generating a new one. If a deleted record referenced an option set value that has since been removed from the choice definition, the restore fails with PicklistValueOutOfRangeRecycleBin. Both are recoverable situations, but only if whoever is running the recovery understands what the error is actually telling them instead of treating it as a generic restore failure and giving up.

Building the Restore Path for Real Incidents

None of this argues against enabling the feature. It argues for treating it as infrastructure that needs its own runbook rather than a switch you flip and forget. A reasonable rollout enables retention on every production environment now, sets the window to match your actual incident response SLA rather than defaulting to the maximum thirty days without weighing the storage tradeoff, and documents exactly which tables in your specific implementation fall outside the coverage: your virtual tables, any elastic tables you’ve adopted, and anything near the six-hundred-attribute ceiling.

The more valuable investment is building and testing the restore path before you need it: registering the mirrored Restore-stage plugin logic anywhere delete-time custom logic exists, confirming cascade restore order against your actual relationship map rather than assuming it will be intuitive mid-incident, and running a deliberate test delete-and-restore cycle in a sandbox so the first time anyone executes this procedure isn’t during a live outage. It’s also worth being explicit internally that this is record-level recovery, not a replacement for environment-level point-in-time restore, which remains the right tool for a broader corruption event or a bad deployment rather than a scoped accidental deletion. Teams that conflate the two end up with a false sense of coverage in exactly the scenario where they can least afford one.

The recycle bin is a genuinely useful addition, and it’s the kind of foundational reliability work that doesn’t generate much attention next to agent and Copilot announcements but matters more day to day for the teams operating these environments. Routeget has walked several clients through exactly this kind of gap analysis between what a new platform capability claims to cover and what a specific implementation’s customizations actually need, and the pattern holds here too: the feature is only as good as the runbook built around it. Treat this GA date as the trigger to write that runbook, not as the moment the problem quietly solves itself.


#Dataverse #DataverseRecycleBin #DataRecovery #PowerPlatformGovernance #ITDisasterRecovery #CloudArchitecture

Dataverse Capacity Got Bigger This Year. Your Storage Governance Still Needs to Catch Up

IT director reviewing a Dataverse storage capacity dashboard showing database, file, and log usage

An IT director we spoke with recently pulled up the Power Platform admin center in January expecting the same tired warning banner about Dataverse capacity that had been sitting there since October. It was gone. Database storage for their Dynamics 365 Finance and Supply Chain Management environment had jumped from 90 GB to 125 GB overnight, with no purchase order, no change request, and no email from their partner. The natural conclusion was that the capacity problem had quietly solved itself. Four months later, the same environment tripped a new kind of alert: a file storage overage notice, even though database usage looked flat. Nothing had been deleted or migrated. The ground underneath the numbers had simply moved.

That is roughly where most Dynamics 365 and Power Platform customers stand right now with Dataverse storage. Microsoft raised default capacity entitlements significantly in December 2025, restructured how that capacity is reported and managed, and then made a second, narrower adjustment in the spring that reclassifies certain metadata between storage types. Individually, each change is well documented. Together, they change what a capacity plan built a year ago actually means, and CIOs and IT directors who treat the December Dataverse capacity increase as a problem solved rather than a system redesigned are going to be surprised by their own admin center in a quarter or two.

What changed in Dataverse capacity this December

The default database capacity increase was substantial and varied by product. Copilot Studio, Power Apps per-app plans, and Power Automate process licenses moved from a 5 GB to a 15 GB baseline. Dynamics 365 CRM applications, including Sales, Customer Service, Field Service, and the Contact Center, moved from 10 GB to 30 GB. Dynamics 365 Customer Insights went from 25 GB to 45 GB. On the ERP side, Standard tier products including Finance, Supply Chain Management, Project Operations, Commerce, and Human Resources rose from 60 GB to 90 GB, while the Premium tier for Finance and Supply Chain Management increased from 90 GB to 125 GB. File storage roughly doubled across most of these same products, though Microsoft did not publish the same level of per-product detail it did for database capacity. Log storage was left untouched.

The more structurally important change sits underneath those numbers. Finance and Operations customers previously managed two separate storage pools: one for the Operations database and one for Dataverse. As of December, those pools merged into a single combined entitlement. For an organization that had been carefully tracking two distinct capacity ceilings, this is not a cosmetic simplification. It changes how consumption trends read, and it means a team that spent the last two years building spreadsheets or Power BI reports to track Operations and Dataverse separately needs to rebuild that tracking around a single number.

Three capacity types, not one

Alongside the entitlement increase, Microsoft has been rolling out a new capacity reporting model that separates Dataverse storage into three distinct types: database, file, and log. This is not universal yet. Organizations still on the legacy model see a single blended capacity figure, while those on the new model get per-type visibility along with a mechanism called cross-capacity-type borrowing. Under that mechanism, unused database capacity can offset a log or file overage, and unused log capacity can offset a file overage, but file capacity cannot offset shortfalls anywhere else. A tenant only registers as officially over capacity after all of that eligible borrowing has already been applied.

Abstract illustration of database, file, and log storage blocks connected by data flow lines representing Dataverse capacity

This matters for budget owners specifically because it changes what an overage notification actually tells you. A file storage warning on an environment that appears to have plenty of headroom in its combined number is not necessarily a sign that the tenant needs more storage overall. It may simply mean the tenant has database or log capacity sitting unused that isn’t automatically flowing to cover the file shortfall, either because the environment hasn’t moved to the new model yet or because the borrowing rules genuinely don’t apply in that direction. Before anyone approves a purchase of additional capacity, it is worth confirming which reporting model the tenant is actually on and whether the specific storage type in the alert is one that borrowing rules can even help with.

The April reclassification nobody budgeted for

In April 2026, Microsoft moved solution-aware tables and related metadata out of database storage and into file storage. Total consumption for an organization does not change as a result of this move, but the split between the two numbers does, and it does so without any corresponding change in what the organization is actually storing. A team that set a database utilization threshold as an internal early-warning trigger, say 70 percent of entitlement, may find that number drop for reasons that have nothing to do with actual data growth, while file utilization climbs for the same non-reason. Anyone reviewing a capacity trend line that spans this period without accounting for the reclassification is going to draw the wrong conclusion about whether their data growth rate is accelerating or slowing down.

Sales Premium customers got a further, separate adjustment in the same window: per-user file storage entitlement doubled from 250 MB to 500 MB, and the underlying tenant base storage figures for those environments moved as well. Microsoft’s own framing for these changes has been candid about the driver: AI-assisted features, Copilot experiences inside Dynamics 365 applications, and the broader push toward agentic workflows all generate metadata, context, and interaction history that traditional transactional storage planning never had to account for. Storage capacity planning built around transaction volume alone is now measuring the wrong thing.

What a capacity plan actually needs to do differently

None of this means the December increase was a bad deal for customers. Bigger entitlements at no additional cost are, on their own terms, a straightforward win, and organizations that were previously purchasing supplemental capacity to cover a gap that the new baseline now closes should go back and cancel or right-size that spending. The mistake is treating a larger number as the end of the exercise rather than the start of a different one.

The more durable response is to rebuild storage governance around the actual mechanics Microsoft has put in place rather than around the old assumption of one number that only ever goes up when you buy more. That starts with confirming, environment by environment, whether the tenant is on the legacy single-figure model or the new three-type model, since the two require genuinely different monitoring approaches. It continues with capturing a clean baseline now, broken out by database, file, and log where that visibility exists, so that the April reclassification and any future ones can be isolated from genuine consumption growth rather than blended into it. It also means understanding the capacity extension option Microsoft makes available once an environment crosses 80 percent utilization: a 45-day window to reduce usage or purchase additional capacity, capped at three extensions in a rolling 365-day period, which is a real safety valve but not one designed to be leaned on indefinitely. And it means deciding, deliberately and in advance rather than reactively during an incident, whether pay-as-you-go billing tied to an Azure subscription is the right overage mechanism for a given environment or whether reallocating from tenant pool capacity is preferable.

For finance and IT leaders who fund these environments, the practical takeaway is that Dataverse storage has moved from a mostly static line item to something closer to a cloud consumption model, with the reporting granularity and billing flexibility that implies, but also the volatility. That is a governance conversation as much as a technical one, and it is worth having before the next reclassification lands rather than after a budget line quietly moves for reasons nobody remembers agreeing to. At Routeget, the engagements that go smoothest are the ones where a client’s storage monitoring gets rebuilt around the new model proactively, rather than the ones where the first real conversation about it happens after an unexpected overage notice.


#Dataverse #DataverseStorage #PowerPlatformLicensing #ITGovernance #DynamicsFinanceOps #CloudCostManagement

Your Dataverse Customer-Managed Key May Have Silently Reverted in January

IT security professional reviewing a data encryption key management dashboard in a modern operations center

A compliance team preparing for a SOC 2 renewal pulls up its Power Platform admin center to confirm what it has told auditors for two years running: that its Dataverse customer-managed key, stored in the organization’s own Azure Key Vault, keeps its data encrypted under keys Microsoft cannot touch without their say. The screen says something different. The environment shows “Microsoft Managed key.” Nobody remembers approving that change, because nobody did. It happened automatically, on a date that passed eight months earlier, and the only trace of it is a single line in the environment’s admin history.

This is not a hypothetical. Microsoft retired its original bring-your-own-key (BYOK) encryption feature for Dataverse on a hard schedule: new production environments could no longer be enrolled in BYOK after June 1, 2025, and any environment still running on BYOK that had not completed migration to the newer customer-managed key (CMK) model by January 6, 2026 reverted automatically to a Microsoft-managed key. Encryption continued without interruption, so nothing broke. That is precisely the problem. An organization can lose the control it has been representing to regulators, customers, and its own board, and the system will not tell anyone unless someone goes looking.

For CIOs and IT Directors who treat encryption key ownership as a checkbox on a compliance questionnaire rather than a control that needs periodic verification, this is worth an afternoon of attention right now.

Why a Dataverse customer-managed key was worth having in the first place

Customer-managed key was never about making Dataverse more secure in a technical sense. Microsoft-managed keys use the same underlying encryption standards either way. What CMK provides is contractual and operational leverage: the ability to revoke Microsoft’s access to the underlying data by disabling a key in Azure Key Vault, independent of any support ticket or service request. For organizations in regulated industries, defense-adjacent supply chains, or jurisdictions with data sovereignty requirements, that revocation capability is often the specific control a customer contract, a government agency, or an internal risk committee asked for by name.

That is also why the BYOK-to-CMK transition matters more than a typical feature deprecation. A lapsed migration does not remove a nice-to-have. It quietly removes the exact control that a Business Associate Agreement, a defense contract, or a data processing addendum may reference. If that control disappeared in January and the organization’s compliance narrative has not been updated since, the gap is not a technical debt item. It is a misrepresentation risk sitting in a vendor security questionnaire that was answered honestly a year ago and has not been true since.

Checking whether this happened to you

The check itself takes minutes. In the Power Platform admin center, under Security, then Data and Privacy, then Customer-managed encryption key, any enterprise policy still listed will show its current status against each enrolled environment. An environment that reverted during the January cutoff will show “Microsoft Managed key” rather than “Encrypted.” The same information is visible at the environment level: open the environment’s history and search for “Update Customer Managed Key,” which will show the reversion event and its timestamp if one occurred.

Organizations that never adopted BYOK or CMK in the first place have nothing to check here, since they were never claiming otherwise. The exposure is specific to organizations that adopted BYOK, told someone about it, and then let the migration window close without follow-through, which is a more common outcome than it should be given how disruptive CMK migrations used to be to plan around.

Compliance officer and IT director reviewing a security audit and cloud governance dashboard in a meeting room

What changed that makes CMK worth reconsidering now

The reason many organizations dragged their feet on migrating from BYOK to CMK in the first place was operational risk, not indifference. Applying a customer-managed key used to mean an environment going fully offline for the duration of encryption, and if a Key Vault permission got revoked by accident, whether through an access review, a policy change, or a departing employee’s role cleanup, the environment locked and stayed locked until Microsoft support intervened. For an IT Director who has lived through that kind of outage once, avoiding a second one is a rational, if costly, choice.

Microsoft’s updates to CMK over the past year address both of those specific failure modes. Core Dataverse services now come back online once the primary encryption pass finishes, with secondary services continuing to encrypt in the background under an “Encrypting – online” status, so the outage window during initial enrollment is shorter than it used to be. More significantly, environment administrators can now restore access themselves once a revoked Key Vault permission is fixed, rather than opening a support case and waiting. That single change removes the scenario that made a lot of security teams nervous about CMK in the first place: an internal mistake in Azure RBAC no longer means the environment stays dark until Microsoft gets involved.

CMK has also expanded into GCC High, giving U.S. government entities and defense contractors the same encryption-key control in a sovereign cloud environment that commercial customers have had, and Azure Key Vault Managed HSM support for Dataverse has moved into preview, which will matter to organizations whose compliance framework specifically calls for hardware security module protection rather than software-protected keys.

Solution architect configuring cloud key vault access controls at a workstation

The coverage gap that still needs to be in the compliance narrative

None of this means CMK covers everything, and any accurate representation to an auditor or a customer needs to say so. Applying a customer-managed key to a Managed Environment encrypts Dataverse data and most of the Dynamics 365 apps built on it, but Power Automate flow data tied to that environment continues to be encrypted with a Microsoft-managed key regardless of the environment’s CMK status. Connector connection settings, environment configuration metadata, and a handful of other categories Microsoft classifies separately are also excluded. A CIO who tells a customer “our Dataverse environment is fully under customer-managed key” is accurate about the data that matters most in nearly every case, but a security questionnaire that asks about encryption coverage across the entire Power Platform estate deserves the more precise answer, not the simplified one.

What this means for planning the next quarter

For organizations that discover a silent reversion, the fix is a re-enrollment, not a support escalation: create or reuse an Azure Key Vault with soft-delete and purge protection enabled, generate or reuse an RSA key, stand up an enterprise policy, and add the environment back in through the admin center, with the expectation that core encryption completes within about four days and the environment stays reachable for most of that window under current behavior. Licensing needs a second look too, since CMK still requires a qualifying Microsoft 365 or Office 365 compliance-tier license (the E5, A5, F5, or G5 compliance and information protection SKUs) on top of the Managed Environments entitlement that comes with standard Power Platform and Dynamics 365 licensing.

For organizations that have been putting off CMK adoption entirely, the calculus has genuinely shifted. The two operational objections that used to show up in every internal debate, extended downtime and support-dependent recovery from an access mistake, have both been addressed directly. What has not changed is the governance discipline CMK requires: separating the person who administers the Key Vault from the person who administers the Power Platform environment, setting expiration alerts on the key well before it lapses, and documenting the enterprise policy and resource IDs somewhere that survives staff turnover. CMK gives an organization real control over its own encryption keys. It also gives that same organization the ability to lock itself out if that control is managed carelessly, and the discipline required to avoid that outcome has not gotten any less important just because the recovery path got shorter.

Routeget Technologies has walked several clients through exactly this kind of CMK re-enrollment after a lapsed migration, and the pattern is consistent: the technical fix is straightforward once someone notices the gap, and the harder part is almost always internal, tracking down who last touched the enterprise policy and confirming what was actually represented to auditors or customers in the meantime. That reconciliation work is worth doing before the next audit cycle asks the question first.


#Dataverse #CustomerManagedKey #EncryptionKeyManagement #DataSovereignty #ComplianceRisk #CloudGovernance