Learn how to design and implement a complete Dataverse audit strategy that satisfies GDPR, SOX, and HIPAA requirements. This expert lesson covers the three-layer audit architecture, Web API queries, real-time alerting, storage management, and the common gaps that leave organizations exposed during audits.

Picture this: it's 9 AM on a Tuesday and your legal team is on the phone. A customer record was modified — the credit limit was quietly raised from $50,000 to $500,000 three weeks ago, and nobody can say who did it or when. The deal that followed went sideways. Now the auditors want a full account of every change to that record for the past six months. Can you produce it?
If you've configured Dataverse auditing correctly, the answer is yes — and you can pull that report in minutes. If you haven't, you're in for an uncomfortable conversation. Dataverse's built-in audit framework is one of the most powerful and underappreciated capabilities in the platform. It's deeply integrated, surprisingly granular, and — when designed correctly — capable of meeting GDPR, SOX, HIPAA, and ISO 27001 traceability requirements without a single line of custom code. But "built-in" doesn't mean "automatic." You have to understand how auditing works at each layer, make deliberate design choices, and avoid the gaps that trip up even experienced architects.
By the end of this lesson, you'll understand the complete Dataverse audit architecture — from the environment-level switch all the way down to individual column tracking. You'll know how to query audit data programmatically, surface it in model-driven apps, manage storage and retention, and design your audit strategy around real compliance requirements rather than theoretical best practices.
What you'll learn:
This lesson assumes you're already comfortable with the core Dataverse object model — tables, columns, relationships, and environments. If you need a refresher on how tables and columns are structured, start with Dataverse Fundamentals: Tables, Columns, and Rows Explained for Power Apps Makers before continuing. You should also have a working understanding of security roles and how they gate access in Dataverse, since auditing and access control are deeply intertwined. The Dataverse Security: Business Units, Security Roles, and Teams lesson is a strong complement to this one.
You'll need System Administrator or System Customizer privileges in your environment to configure audit settings. Some sections of this lesson also use the Dataverse Web API, so familiarity with REST calls and JSON will help.
Before you touch a single setting, you need to understand the mental model. Dataverse auditing is governed by three nested layers:
Think of these as nested gates. If the environment switch is off, nothing is audited, regardless of table or column settings. If environment auditing is on but table auditing is off for your Accounts table, changes to account records produce no audit logs. And if both environment and table auditing are on, but you haven't enabled audit on the "Credit Limit" column, modifications to that field won't appear in the log.
This layered design is intentional and smart — it lets you be surgical about what you track, which matters enormously when you consider that Dataverse audit logs consume storage from your organization's Dataverse capacity.
Key insight
The three-layer architecture means you can audit selectively. An organization storing 50 million contact records doesn't need to audit every phone number edit. You audit what has compliance value — financial fields, status changes, personally identifiable information — and leave commodity fields alone.
When an audited column on an audited table in an audited environment changes, Dataverse writes a record to the internal audit table. Each audit record contains:
Notice what's not on that list: IP addresses, device information, or session context. If your compliance framework requires client IP capture (some do), you'll need to supplement Dataverse auditing with Azure Active Directory sign-in logs or Microsoft Purview.
Warning
The "user" captured in an audit record is the Entra ID identity making the API call — not necessarily the person sitting at the keyboard. If a Power Automate flow runs under a service account, all changes attributed to that flow will show the service account's identity. For compliance purposes, your automation designs need to account for this: either run flows under per-user connections, or capture the originating human context in a separate column.
Access auditing (tracking reads, not just writes) is a separate option. Because read operations are vastly more frequent than writes, enabling access auditing can generate enormous log volumes and significant storage consumption. Most organizations enable access auditing only for their most sensitive tables — medical records, compensation data, financial forecasts.
To enable auditing at the environment level, navigate to the Power Platform admin center (admin.powerplatform.microsoft.com), select your environment, then open Settings → Audit and logs → Audit settings.
You'll see three main controls here:
Turn on "Start Auditing." Leave "Log access" off unless you have a specific requirement — we'll revisit this when discussing storage management.
Within the same settings panel, you'll find the retention period dropdown. This is critically important. Options typically include 30 days, 90 days, 1 year, and — in some licensing tiers — indefinite. The retention period controls how long audit records persist before the system automatically purges them. If your compliance framework requires 7-year retention (as SOX can require for certain records), you need a separate archival strategy, not just the default Dataverse retention setting.
Warning
Retention period settings take effect immediately and apply retroactively to existing audit data. If you accidentally set retention to 30 days in an environment that's been accumulating audit logs for two years, you will lose historical data. Treat this setting with the same care as a production database truncation.
Navigate to make.powerapps.com, select your environment, then open the Data section and find the table you want to configure. Open the table properties and select the "Advanced options" section (in the classic editor, this is the table Properties dialog; in the modern editor, it's within the table settings panel).
You'll find the "Audit changes to its data" checkbox. Enabling this begins capturing audit records for all subsequently configured columns on this table. It does not retroactively create audit records for past changes.
Two important things to know about table-level audit configuration:
First, certain system tables have auditing pre-configured and cannot be disabled — the audit log itself, security role assignments, and a handful of others are always tracked by the platform.
Second, table auditing behavior on related records is independent. If you audit the Account table and a user changes a contact's email address on a subgrid within an account form, the audit record for that change is attached to the Contact table, not the Account. Understanding this parent-child boundary prevents gaps in your audit trail.
Within the same table editor, navigate to each column and open its properties. Under "Advanced options" (or in the column's properties in the classic editor), you'll find "Enable auditing."
Column-level auditing defaults vary by column type. Primary name columns are often audited by default. System columns like createdon, modifiedon, and owner fields may or may not be audited by default, depending on the table.
The strategic question is: which columns do you actually need to audit? Here's a practical framework:
Always audit:
Audit selectively based on risk:
Usually skip:
Tip
When in doubt, err toward auditing more columns during initial configuration. You can always remove audit from a low-value column later (though you'll lose future log entries, not past ones). It's much harder to explain to an auditor why critical column changes weren't tracked than to manage slightly larger storage consumption.
Understanding the internal structure of audit data helps you write better queries and reason about performance.
Dataverse stores audit records in the audit system table (logical name: audit). You can't add columns to this table, but you can query it through the Web API and the SDK. Each row has these key attributes:
auditid — unique GUID for the audit recordobjectid — the GUID of the record that changedobjecttypecode — the entity type code of that record's tableuserid — the GUID of the user who made the changecreatedon — when the audit record was writtenaction — an integer representing the operation type (see below)operation — another integer for the operation category (Create=1, Update=2, Delete=3, Access=4)changedata — a JSON blob containing the old and new values for changed columnsThe changedata column is where the actual diff lives. It's stored as serialized JSON, which means:
$filter against nested JSON)This has significant implications for how you build audit reports. You can filter audit records by objectid, userid, action, createdon, and objecttypecode server-side. But if you want to find "all records where the Credit Limit changed by more than 20%," you need to pull the relevant audit records client-side and parse changedata yourself.
The action values cover more ground than you'd expect. Beyond Create/Update/Delete, you'll encounter values for:
These meta-audit entries are enormously valuable for compliance: they tell you if someone tried to cover their tracks by disabling auditing or deleting logs.
Key insight
Dataverse audits the audit system itself. Action codes 64 and 65 mean that if someone disables auditing or purges audit records, that act is itself recorded — unless, of course, they delete those records too. A complete compliance implementation should alert on action codes 64, 65, and 101 (delete audits) using a Power Automate flow.
The Power Apps model-driven UI shows audit history on individual records, which is useful for day-to-day use. But for compliance reporting, investigation, and bulk analysis, you need the Web API.
Here's a query that retrieves all audit records for a specific account, ordered newest-first:
GET https://yourorg.crm.dynamics.com/api/data/v9.2/audits?
$filter=objectid/accountid eq 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
&$orderby=createdon desc
&$select=auditid,createdon,userid,action,operation,changedata
&$top=50
Accept: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0
This returns a paged JSON response. The changedata property in each record contains the before/after snapshot.
To get audit records for all changes to the Account table across your environment:
GET https://yourorg.crm.dynamics.com/api/data/v9.2/audits?
$filter=objecttypecode eq 'account'
&$orderby=createdon desc
&$select=auditid,createdon,userid,action,operation,objectid,changedata
Note
The objecttypecode filter uses the logical name of the table (e.g., account, contact, opportunity), not the display name or schema name.
For HR investigations or user offboarding reviews, you'll want all changes made by a specific user:
GET https://yourorg.crm.dynamics.com/api/data/v9.2/audits?
$filter=userid/systemuserid eq 'user-guid-here'
&$orderby=createdon desc
The Web API also exposes a special action for retrieving formatted audit history: RetrieveRecordChangeHistory. This is the same action the model-driven app calls internally when you view audit history on a form. It returns a more structured response than the raw audits endpoint, including formatted column labels and human-readable values.
POST https://yourorg.crm.dynamics.com/api/data/v9.2/RetrieveRecordChangeHistory
Content-Type: application/json
{
"Target": {
"accountid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"@odata.type": "Microsoft.Dynamics.CRM.account"
},
"PagingInfo": {
"PageNumber": 1,
"Count": 50
}
}
The response includes an AuditDetailCollection with typed AttributeAuditDetail entries, each containing the OldValue and NewValue as full entity representations. This is significantly easier to parse for display purposes than raw changedata.
When you need to analyze changedata at scale — say, identifying all records where a particular field changed — you'll need a parsing strategy. In Power Automate, this typically looks like:
// Parse changedata JSON in a Power Automate Compose action
@{json(triggerBody()?['changedata'])}
Then access individual old/new values with expressions like:
outputs('Parse_ChangeData')?['changedAttributes'][0]['oldValue']['Value']
The structure of changedata is:
{
"changedAttributes": [
{
"logicalName": "creditlimit",
"oldValue": { "Value": 50000 },
"newValue": { "Value": 500000 }
},
{
"logicalName": "paymentterms",
"oldValue": { "Value": 2 },
"newValue": { "Value": 3 }
}
]
}
For everyday business users, the right interface for audit data is the record form itself. Model-driven apps expose audit history through a built-in mechanism — no custom development required.
When auditing is configured on a table, the form editor gives you access to a special "Audit History" section. To add it:
In the classic form editor: go to the form's Insert tab and choose "Related Data Panel" or use the "Audit History" control. In the modern form editor (Power Apps Studio): open the form, select the component palette, and add the Audit History component to a new or existing tab.
This renders an embedded subgrid on the form that shows the audit history for the current record. Users with the "View Audit History" privilege in their security role will see this; users without it will see an empty panel or nothing at all.
Tip
Don't put the Audit History component on the main form tab — it creates visual clutter for users who check records frequently and don't care about history. Instead, create a dedicated "Audit & Compliance" tab on the form and place it there. This is a simple form design pattern that keeps the primary data-entry experience clean while keeping audit history accessible.
Audit access is governed by two distinct privileges in the security role editor:
The "View Audit Summary" privilege is powerful and should be granted sparingly — typically only to compliance officers, system administrators, and your SOC team. A sales user should be able to see the audit history for their own account records, but not for accounts they don't own.
These privileges sit in the "Audit" section of the security role editor. They're often overlooked during role design because they're not part of the main entity privilege grid. Make sure your security role design process explicitly evaluates these audit permissions.
This is where the lesson moves from configuration to architecture. Different compliance frameworks have different requirements, and a one-size-fits-all "audit everything forever" approach is usually both too expensive and too noisy to be operationally useful.
GDPR creates a tension in audit design. On one hand, you need audit trails to demonstrate accountability — proving you followed your stated data handling procedures. On the other hand, audit logs contain personal data, and personal data must be handled according to GDPR's data minimization and right-to-erasure principles.
This creates a specific problem: if a user exercises their right to erasure (Article 17) and you delete their contact record, audit logs may still contain that person's name, email, and other PII in the changedata JSON. Legally, this is nuanced — audit logs may be retained under the "legitimate interest" or "legal obligation" legal bases, which can override the right to erasure — but you need to document your legal basis explicitly.
Practically speaking: ensure your data retention and erasure policies address audit logs specifically, and that your legal team has signed off on the retention basis. Do not treat audit logs as an afterthought in your GDPR data map.
GDPR also requires you to track access to personal data (Article 30 record of processing activities). This is where Dataverse's Access Auditing option becomes important for sensitive tables — enabling it gives you a log of who accessed which contact or patient record, even if they only read it.
Sarbanes-Oxley focuses on financial controls and the integrity of financial reporting. For organizations using Dataverse to manage financial data (accounts receivable, revenue records, pricing), SOX requires:
Dataverse auditing handles the first point natively. For the second and third, you need Business Process Flows and Power Automate approval workflows to generate the approval evidence that lives alongside the audit trail.
SOX also typically requires extended retention — often 7 years for financial records. Since Dataverse's maximum native retention is typically 1 year (with some licensing providing "forever" retention), SOX-compliant organizations need an archival pipeline. We'll cover this in the storage management section.
HIPAA's Security Rule requires audit controls that "record and examine activity in information systems that contain or use electronic Protected Health Information" (ePHI). For Dataverse environments storing patient data:
Warning
HIPAA is explicit that audit controls must cover access by internal users, not just external threats. The most common HIPAA violation scenario is a curious employee browsing records they have no business reason to access. Dataverse Access Auditing, combined with Power Automate alerting, is your defense here.
Audit logs consume Dataverse database storage. In large environments with aggressive audit configurations, this can become substantial — hundreds of gigabytes over time. Understanding storage mechanics helps you balance compliance coverage with operational cost.
Each audit record is roughly 1-4 KB, depending on the number of columns changed and the size of their values. As a rough estimate:
At those rates, a 1-year retention policy for a mid-sized organization generates 30-40 GB of audit data. That's not alarming, but it's not free either — it consumes from your Dataverse database capacity allocation.
You can check current audit storage consumption from the Power Platform admin center under Environment → Resources → Capacity.
For compliance frameworks requiring retention beyond what Dataverse natively supports, the standard approach is to pipe audit records to an external store before they're purged.
The architecture looks like this:
audits endpoint for records created in the past 24 hoursFor the Blob Storage path, the flow looks like:
/api/data/v9.2/audits?$filter=createdon ge YESTERDAY)audit-{date}-{page}.jsonFor the SQL path, you'd parse changedata in the flow and insert rows into a structured AuditLog table. The SQL approach makes investigative queries far faster but requires more ETL work and a schema definition.
Tip
Even if your compliance framework doesn't require extended retention, archiving audit data to a cheaper storage tier (blob vs. Dataverse database) reduces your Dataverse capacity consumption. Archive after 90 days to blob, and you might save significant money while technically being able to produce 7 years of history if needed.
Dataverse allows System Administrators to manually purge audit records. The UI is in the Power Platform admin center: Environment → Settings → Audit and logs → Audit Log Management.
Here you can delete audit records older than a specified date. This is a destructive, irreversible operation. Before executing a manual purge:
The fact that purge operations are themselves audited is intentional. If a bad actor with System Administrator access tries to cover their tracks, the deletion of audit records leaves a meta-trail. A sophisticated attacker would need to delete the purge audit records too — which creates yet more audit records, and so on. This self-referential property is a genuine security feature.
Static audit logs answer "what happened?" but they don't prevent damage or trigger response. For a mature compliance posture, you need real-time alerting on high-risk events.
The simplest alerting architecture uses a Dataverse trigger on the audit table. Since audit is a system table and not directly available as a Power Automate trigger entity, you have a few options:
Option 1: Scheduled polling A flow runs every 15 minutes, queries for new audit records matching specific criteria (action code 64 = auditing disabled, or changes to financial columns above a threshold), and sends alerts.
Option 2: Webhook via Azure Event Grid
Power Platform publishes a Microsoft Dataverse connector event for Record Created on system entities including audit. This triggers a flow in near-real-time. Configure the trigger on the Audit table and filter for high-priority action codes.
Option 3: Microsoft Sentinel Integration For enterprise SOC environments, Dataverse audit logs can be forwarded to Microsoft Sentinel via the Microsoft Sentinel for Dynamics 365 data connector. Sentinel's KQL-based analytics rules give you industrial-strength pattern detection — for example, alerting when a user accesses more than 100 records in 10 minutes, or when records in a restricted table are accessed outside business hours.
Here's a simple Power Automate flow outline for credit-limit-change alerting:
Trigger: When a row is added (Audit table) — Use a scheduled approach
OR: Recurrence every 15 minutes
Actions:
1. List rows from Audit where:
- createdon > utcNow(-15 minutes)
- objecttypecode = 'account'
- operation = 2 (Update)
2. For each audit record:
a. Parse changedata JSON
b. Condition: Does changedAttributes contain 'creditlimit'?
c. If yes:
- Get account name
- Compose alert message with old/new values, timestamp, user
- Send Teams notification to Finance Compliance channel
- Create a record in custom "Compliance Alert" table
Key insight
The most valuable alerts aren't just about what changed — they're about who changed it and when. A sales manager adjusting credit limits during business hours is routine. The same change made by an IT service account at 2 AM on a Sunday is an anomaly worth investigating immediately.
Beyond individual record history, compliance teams need org-wide audit reports. The right tool for this is a dedicated model-driven view on the audit table, combined with Advanced Find.
The audit table is accessible in the model-driven app experience through the legacy Audit Summary View, but it's limited. For power users, a better approach is to use the Dataverse Advanced Find (or its Fetch XML equivalent) to build saved views.
Navigate to Advanced Find in your model-driven app (the funnel icon in the top navigation). Select "Audit History" as the entity. You can filter by:
Save this as a shared view and give it to your compliance team.
For more polished reporting, consider building a custom page in your model-driven app using canvas app components. A canvas custom page can query the audit Web API, parse changedata client-side, and present a filterable, formatted compliance report — far more usable than raw audit subgrids.
An alternative approach for data-heavy audit reporting is to connect Power BI to Dataverse using the Dataverse connector and build a dedicated Audit Dashboard. Power BI can query the audit table, apply transformations with Power Query to parse changedata, and produce visualizations showing change volume by user, table, and time period.
This exercise builds a complete audit configuration for a fictional financial services company's "Loan Application" table. Work through each step in a sandbox environment.
You're an architect for Contoso Financial. They've built a model-driven app for loan origination using a custom contoso_loanapplication table. The CISO has requested that the following changes be auditable for 2 years, with real-time alerting if a loan amount is changed after initial submission:
contoso_loanamount)contoso_creditscore)statuscode)contoso_approvaldecision)contoso_loanapplicationFor each of these columns, open the column properties and enable auditing:
contoso_loanamountcontoso_creditscorestatuscode (this is the status reason system column — it's usually available for audit configuration)ownerid (the owner column — this may be enabled by default; verify)contoso_approvaldecisionSave and publish after each change, or batch your saves.
GET /api/data/v9.2/audits?$filter=objecttypecode eq 'contoso_loanapplication' and operation eq 2 and createdon ge TIMESTAMPformatDateTime(addMinutes(utcNow(), -15), 'yyyy-MM-ddTHH:mm:ssZ')changedata contains contoso_loanamountConfiguration changes in Dataverse require publishing to take effect. If you enable audit on a table or column but don't publish the customization, the change is stored in your solution but not active in the environment. Always publish after configuring audit settings. In the modern maker experience, this happens automatically on save for some changes but not all — develop the habit of explicitly publishing.
Many architects enable auditing on a parent table and assume that auditing cascades to child records. It doesn't. Audit is configured per-table. If you audit the Account table but not the Contact table, changes to contacts related to that account are not captured. Map your entire data model against compliance requirements and configure each table independently. This connects directly to your data model design — the entity-relationship structure determines which tables need audit coverage.
When a lookup column changes — for example, a contact's parent account is changed from Contoso to Fabrikam — the audit record captures the GUID of the new and old related records, not their display names. When you later read the audit log, those GUIDs need to be resolved to names for human readability. Build this resolution step into any audit report or alert flow you create. Attempting to audit reports that show raw GUIDs will not satisfy compliance auditors.
Power Automate flows, plugins, and system jobs make changes to records, and those changes appear in the audit log attributed to the user context of the process — often a service account or application user. If you see a large number of mysterious audit entries from a user named "SYSTEM" or a service account name, that's expected. Document which automated processes run under which identity, so investigators can quickly distinguish human changes from system changes.
The audit history subgrid on a form is only visible to users with the "View Audit History" privilege. It silently shows nothing (or is hidden entirely) for users without that privilege. Before your external audit, verify with a test user in each relevant role that they can actually see what you expect them to see. An auditor who sits down at a compliance officer's workstation and sees blank panels will lose confidence in your implementation immediately.
If you make a change to a record and no audit entry appears, check in this order:
Tip
A quick diagnostic: make a change to a column you're confident is audited, then immediately query /api/data/v9.2/audits?$orderby=createdon desc&$top=5 via Postman or browser (with appropriate authentication). If a record appears there but not in the UI, you have a security role / view issue. If no record appears at all, you have a configuration issue.
If you receive capacity alerts related to Dataverse database storage and audit logs are a contributor, your options are:
You now have a complete picture of Dataverse auditing — from flipping the environment switch, through thoughtful column selection, all the way to storage management, compliance framework alignment, and real-time alerting.
The key architectural insights to carry forward:
Audit is a three-layer system. Environment → table → column. All three must be active for a field to be captured. Gaps at any layer create gaps in your compliance evidence.
The audit table is queryable and filterable, but changedata is opaque to server-side filtering. Design your investigative workflows around this constraint — filter by record, user, table, and time range at the API level, and parse change details client-side.
Compliance frameworks differ in what they require. GDPR cares about access tracking and right-to-erasure tensions. SOX cares about financial data integrity and segregation of duties. HIPAA cares about ePHI access by insiders. Know which framework drives your requirements before you design your audit configuration.
Storage and retention require active management. Default retention periods are rarely sufficient for enterprise compliance. Build an archival pipeline early, before you need it.
Audit without alerting is reactive. A mature implementation monitors the audit stream in real time and generates alerts for anomalous changes. Power Automate and Microsoft Sentinel are your primary tools here.
With auditing in place, your next priorities for a complete compliance posture are column-level security (restricting who can even see sensitive fields, complementing audit trails with preventive controls) — covered in Column-Level Security and Record Sharing in Dataverse. You should also revisit your security role design with fresh eyes: now that you understand what audit captures and who can view it, you may identify gaps in your privilege assignments. The Dataverse Security: Business Units, Security Roles, and Teams lesson provides the framework for that review.
Finally, if your compliance use cases require immutable evidence — not just audit trails but cryptographically signed records that can survive legal scrutiny — explore Microsoft Purview's integration with Power Platform. Purview extends beyond what Dataverse native auditing provides, offering eDiscovery workflows, legal hold capabilities, and audit log export to formats acceptable in judicial proceedings.
Your audit system is only as good as the last time you tested it. Put a recurring calendar reminder to validate audit capture monthly — spot-check three records across your most sensitive tables, confirm the history is accurate, and verify your alerting flows are still active. Compliance is a discipline, not a one-time configuration.