Learn how to configure Dataverse audit logging at the organization, table, and column level — then surface that change history directly on model-driven app forms. This expert-level lesson covers storage architecture, security role configuration, API querying, and compliance patterns.

Picture this: a contract record shows a discount of 15%, but your sales manager swears it was supposed to be 8%. The deal closed last quarter. Nobody can remember who changed it, when, or why. You open the record, stare at the current value, and have absolutely nothing to go on. This is the kind of problem that gets people fired — or at least triggers some very uncomfortable meetings.
Dataverse auditing exists precisely for moments like this. When configured correctly, it creates a timestamped, user-attributed log of every field change on every audited record — who touched what, when they touched it, and what the previous value was. That log becomes a source of truth for compliance requirements, dispute resolution, regulatory audits, and just plain understanding how your data evolves over time. It's not glamorous infrastructure, but it's the kind of thing that transforms a system from "software we use" into "a system we trust."
By the end of this lesson, you'll know how to configure auditing at every level — environment, table, and field — understand the underlying storage mechanics and retention considerations, build views and queries that surface meaningful change history, and embed that history directly on model-driven app forms so users never have to dig through administrative screens to understand what changed. This isn't a checkbox feature; it's an architectural capability that deserves careful planning.
What you'll learn:
You should be comfortable with Dataverse fundamentals — tables, columns, and rows before working through this lesson. You should also understand how model-driven app forms are structured, since we'll be adding the audit history subgrid to a form. Familiarity with Dataverse security roles and business units is helpful because audit access is controlled through privileges, not just feature toggles.
Before you start clicking toggles, you need a mental model of the architecture. Misunderstanding this layer leads to frustrating gaps — audit logs that don't capture what you expected, or history that disappears without warning.
Dataverse auditing has three independent gates, and all three must be open for a change to be recorded:
This layered design gives you precise control over what gets logged, which matters for both storage cost and relevance. You don't need a timestamp every time a calculated status field refreshes — you need a log when a human changes a contract value.
When a change is captured, Dataverse writes a record to the audit system table. This isn't a table you can open in Power Apps Studio and customize — it's a system entity with a fixed schema. The key fields you'll work with are:
auditid — unique identifier for the audit recorduserid — who made the change (lookup to SystemUser)createdon — when the change occurredobjectid — the record that was changed (polymorphic lookup)objecttypecode — the entity type of the changed recordaction — numeric code for the operation (create = 1, update = 2, delete = 3, plus access and custom operations)changedata — the actual field changes, stored as structured XMLThe changedata field is where things get interesting. It contains old and new values for every audited column that changed in a given save operation. The format is XML, which means bulk analysis requires some parsing work if you're pulling directly from the API. The model-driven app UI handles this rendering for you, but when you query via the Dataverse Web API or Power Automate, you'll get raw XML.
Note
Audit records are written to a separate partition from your main Dataverse database. They do count against your storage consumption, but they're not included in the transactional database size — they appear in the "File and log storage" category in the Power Platform admin center capacity report.
The audit log captures data changes: creates, updates, deletes, and in some configurations, record access events. It does not capture:
Warning
If your organization uses bulk data migration tools or direct database access for ETL operations, those changes may not appear in the audit log. Always verify your data pipeline's audit footprint during implementation.
The master switch lives in the Power Platform admin center, not in Power Apps. This is an important distinction: a Power Apps maker with Environment Maker permissions cannot toggle organization-level auditing. You need the System Administrator role in the environment.
Navigate to the Power Platform admin center at admin.powerplatform.microsoft.com. Select Environments from the left navigation, choose your target environment, then select Settings. Expand the Audit and logs section. You'll see Start auditing — this is the organization-level toggle.
When you enable it, Dataverse doesn't immediately start logging everything. You still need to configure it at the table and column levels. But without this setting enabled, nothing else matters.
You'll also see two additional options here worth noting:
Log access — When enabled, Dataverse logs every time a user reads a record (not just writes). This is essential for strict compliance environments (HIPAA, GDPR access logs), but it generates an enormous volume of data. A 500-person team working with a busy table will produce tens of thousands of read audit records per day. Enable this deliberately.
Read audit logs — Controls whether the audit log is exposed to users in the model-driven app. This can be toggled independently of whether logging itself is active.
Key insight
The "Log access" setting is the single biggest driver of unexpected audit storage growth. Most organizations need write auditing (create, update, delete) but not read auditing. Leave "Log access" off unless you have a specific regulatory requirement for it.
Once auditing is enabled, audit records accumulate indefinitely unless you actively manage them. Dataverse doesn't automatically purge old audit records. The admin center gives you a Delete logs capability under Settings → Audit and logs → Delete logs, where you can delete all audit records older than a specified number of months.
In high-activity environments, this becomes a real operational concern. A table with 10,000 records updated 5 times per day will accumulate roughly 1.8 million audit records per year. That's not catastrophic, but it's worth tracking in your capacity planning.
Tip
Build a quarterly retention review into your ops calendar. Use the Power Platform admin center's capacity report to monitor audit log growth. If you need long-term retention beyond what Dataverse provides, export audit records to Azure Data Lake or Blob Storage using Synapse Link or a Power Automate scheduled export before purging.
With the organization switch on, you configure individual tables in Power Apps (make.powerapps.com) or through the legacy Solution Explorer. The modern approach uses the Power Apps tables experience.
Open make.powerapps.com, select your environment, and navigate to Dataverse → Tables. Find your target table — let's say you're working with a custom Contract table. Select it, then choose Properties (or Edit if using the unified interface).
In the table properties panel, look for the Advanced options section. Expand it and you'll see an Audit changes to its data checkbox. Enable it and save.
Alternatively, if you're working in Solution Explorer (which gives you a cleaner per-solution view), open the solution containing your table, select the table, and find the Auditing option in the Table properties section. The label varies slightly between interfaces but refers to the same setting.
This is a judgment call, but here's a framework for thinking about it:
Always audit: Tables containing financial data, legally binding records, personally identifiable information, and anything subject to regulatory review. In a sales app, this typically includes Opportunities, Contracts, Orders, and Account financial fields.
Audit selectively: Tables with high-volume transactional records where you care about outcomes but not every intermediate state. A support ticket table might need audit on status and resolution fields but not on internal notes.
Consider skipping: High-volume staging or integration tables, log tables, and tables that change programmatically thousands of times per day where user attribution isn't meaningful.
Warning
Enabling auditing on the Activity, Email, and Appointment tables (standard Dataverse tables) can generate enormous audit volume because these tables interact with Exchange sync and Power Automate flows constantly. Audit these tables only if you have a specific business requirement.
By default, when you enable auditing on a table, all columns on that table are included in the audit. This is often more than you need. A best practice is to explicitly configure which columns matter.
Consider a Contract table with 40 columns. You care about changes to: Contract Value, Discount Percentage, Status, Owner, Start Date, and End Date. You don't need audit history for internal notes, last refresh timestamp, or system-generated tracking fields. Auditing all 40 columns when only 6 are meaningful:
In the modern Power Apps tables editor, select a table, then navigate to Columns. Select a column to open its properties. Look for the Auditing option — it typically appears in the Advanced options of the column editor. Toggle it off for columns you don't need to track.
In the classic Solution Explorer, you open the table, navigate to Fields, select a field, and use the Auditing dropdown which allows Enable or Disable.
Note
Some column types cannot be audited regardless of your settings. These include virtual columns, formula columns, and some calculated columns. The platform silently ignores audit configuration on these column types — don't be alarmed if you enable auditing on a formula column and see no audit records generated.
Lookup columns — the foreign key fields that implement table relationships — are particularly important to audit. When a record's owner changes, or a contract's associated account is swapped, that's often exactly the kind of change you need to track.
When an audited lookup column changes, the audit log captures both the record ID and the display name of the referenced record at the time of the change. This is crucial: if the referenced record is later renamed or deleted, the audit history still shows what the lookup pointed to at the moment of change. The audit log doesn't hold a live reference — it captures the value as it was.
This is one area where Dataverse auditing is genuinely superior to building your own change history table: it handles lookup resolution automatically without extra work on your part.
The Power Platform admin center provides an org-wide audit log view. This is your first stop for investigation scenarios — compliance reviews, incident response, or just understanding the history of a specific record.
In the admin center, go to Environments → [your environment] → Settings → Audit and logs → Audit summary view.
This opens a grid of audit records. The default view shows: Date, User, Action, Object Type (table name), and the record that was changed. Without filtering, this is an overwhelming firehose — especially in active environments.
Use the built-in filters:
Once you identify the audit record you're looking for, clicking it opens the detail view showing the changedata content rendered as a readable before/after table: column name, old value, new value.
For programmatic access — building reports, exporting for compliance, or feeding audit data into a dashboard — you use the Dataverse Web API. The audit table is exposed at:
GET [organization URI]/api/data/v9.2/audits
A practical query to retrieve update events on the Contract table for the last 30 days:
GET [org]/api/data/v9.2/audits
?$filter=objecttypecode eq 'new_contract'
and action eq 2
and createdon ge 2024-01-01T00:00:00Z
&$select=auditid,createdon,action,changedata
&$expand=userid($select=fullname,domainname)
&$orderby=createdon desc
&$top=100
Breaking down the filter:
objecttypecode eq 'new_contract' — the logical name of your table (schema name)action eq 2 — update events (1=create, 3=delete)createdon ge — date filterThe response includes changedata as a string. You'll need to parse the XML to extract individual field changes programmatically. Here's the structure you'll encounter:
<AuditData>
<Entity name="new_contract" id="...">
<Attribute name="new_discountpercentage">
<OldValue>8</OldValue>
<NewValue>15</NewValue>
</Attribute>
<Attribute name="new_contractvalue">
<OldValue>250000</OldValue>
<NewValue>247500</NewValue>
</Attribute>
</Entity>
</AuditData>
Each Attribute element represents one field that changed. If five fields changed in a single save, you'll see five Attribute elements in one audit record. This is how Dataverse batches — one audit record per save operation, not one per field change.
Key insight
A single save operation that changes 10 fields produces exactly one audit record, not 10. This is important for storage estimation — audit volume correlates with the number of save operations, not the number of individual field changes. An app that auto-saves frequently or triggers updates via flows will generate more audit records than one that batches user saves.
You can trigger Power Automate flows based on audit events — for example, sending a notification when a specific field changes. However, you can't directly trigger a flow from an audit record creation. The more reliable pattern is to use a Dataverse row updated trigger with a specific column filter, which fires before the audit record is written and captures the change at the business logic layer.
For post-audit analysis flows — running nightly to generate compliance summaries — use the List rows action against the audits table with the same filters shown in the API example above.
The admin center audit view works fine for administrators, but end users — sales managers, contract administrators, support leads — need to see change history in context, on the record they're looking at. Embedding audit history on a form accomplishes this without requiring any special navigation or permissions beyond what's normal.
Model-driven apps include a built-in subgrid component specifically for displaying a record's audit history. This subgrid is connected to the system audit entity and automatically filters to show only the audit records for the parent record being viewed.
To add it to a form, open the form editor for your target table. In Power Apps, go to Dataverse → Tables → [your table] → Forms, and open the form you want to edit (typically the Main form).
In the form editor, add a new tab or select an existing tab where you want the history to appear. Add a section within that tab. In the section, you'll insert a Subgrid component (not a regular column).
When configuring the subgrid:
Save and publish the form. When you open a record, the subgrid will show the audit history in reverse chronological order — most recent change at the top — with columns for Date, User, Event (Create/Update/Delete), and a detail link.
Tip
Put the audit history on its own dedicated tab labeled "Change History" rather than cluttering it onto an existing data tab. Users who want to see it will navigate there intentionally, and it won't visually compete with the data entry fields that most users need most of the time.
The default Audit History view shows a limited set of columns. You can't directly edit the system Audit History view (it's a system view), but you can control how the subgrid presents information through the subgrid's own configuration options.
For more control, you can build a custom view against the audit entity for specific filtering scenarios — for example, a view that only shows Update events (filtering out Creates and Deletes). Create this view in the tables experience under the Audit entity's Views section, then reference it from your subgrid instead of the default view.
One practical customization: set the subgrid to show only 5 rows by default with a "See more" expand option. For records with years of history, loading 100+ audit rows every time someone opens a form is an unnecessary performance penalty.
This is where Dataverse security roles intersect with audit configuration in a way that surprises many makers.
Even if the audit history subgrid is on the form, a user won't see audit records unless their security role grants them the View Audit History privilege. This privilege lives under the Privacy Related Privileges section in the security role editor — not under the audit entity's standard table privileges.
Navigate to the security role editor: Settings → Security → Security Roles → [role] → Business Management tab. Scroll to the Privacy Related Privileges section. You'll find:
For most end users (sales reps, contract admins), you want View Audit History only — they can see the history of individual records but not browse the org-wide log. Reserve View Audit Summary for managers and administrators.
Warning
If you grant View Audit Summary to a broad user population, they can potentially browse audit records for tables they have read access to, across the entire organization. This can expose sensitive change history to users who shouldn't see it. Follow the principle of least privilege: grant View Audit History where you need form-level history, and gate View Audit Summary to your security or compliance team.
Once you have audit data flowing, the real work begins: making that data actionable. Raw audit records are useful for point-in-time investigation, but they don't naturally answer questions like "which fields change most frequently?" or "which users make the most modifications to contracts?"
For analytical reporting, the cleanest pattern is to connect Power BI to your Dataverse environment using the Dataverse connector and query the audit entity. The changedata XML field requires transformation — use Power Query's XML parsing functions to expand it into rows.
A simplified Power Query step to extract individual field changes from the changedata XML:
// After loading audit records, expand the changedata column
let
ParsedXml = Table.TransformColumns(
AuditSource,
{{"changedata", (xml) =>
if xml = null then null
else Xml.Tables(xml)
}}
),
ExpandedChanges = Table.ExpandTableColumn(
ParsedXml,
"changedata",
{"Attribute"},
{"Attributes"}
)
in
ExpandedChanges
This approach lets you build reports like:
Within the model-driven app itself, Advanced Find (accessible from the Settings gear in the classic interface, or the search icon in Unified Interface) allows you to query the audit entity directly. This gives non-developer administrators a UI-based way to investigate audit history without API access.
Search against the Audits entity. Apply filters for:
The results grid shows matching audit records. You can click any record to see the full before/after field comparison.
This is particularly useful for HR-adjacent investigations: "What did this departing employee change in their last week?" — apply a date filter for the notice period and a user filter for that person, and you have a complete picture without writing a single line of code.
A common requirement is: "Alert the compliance team immediately when the Contract Value field changes by more than 10%." You can't build this purely from the audit log (which writes after the fact), but you can combine the Dataverse row updated trigger with a column filter and a Power Automate expression:
// In Power Automate, after "When a row is updated" trigger
// Column filter: new_contractvalue
// Condition to check percentage change
div(
abs(
sub(
triggerOutputs()?['body/new_contractvalue'],
triggerOutputs()?['body/_new_contractvalue_old']
)
),
triggerOutputs()?['body/_new_contractvalue_old']
) > 0.10
If this condition is true, send a Teams notification or email to the compliance team with the record link, the old value, the new value, and the user who made the change. This is event-driven alerting on top of audit data — a pattern that turns passive logging into active governance.
Note
The _old suffix on field names in Power Automate triggers gives you the pre-change value. This is only available when you enable Select columns in the trigger configuration and the column is included in the change filter. Without this, you'd need to query the audit log after the fact to retrieve the previous value.
Auditing has real performance implications, especially at scale. Understanding where the costs appear helps you make informed trade-offs.
Every audited save operation incurs additional write cost: Dataverse must write the audit record to the audit partition synchronously with the main record save. In practice, this adds roughly 5–15ms to each save operation under normal conditions. For typical user-driven updates, this is imperceptible.
It becomes material in automation scenarios: a Power Automate flow that updates 10,000 records in a loop will take measurably longer with auditing enabled on the target table. If you're running bulk data remediation or ETL operations through standard Dataverse APIs, account for this overhead.
For bulk operations where audit isn't needed (initial data load, migration), you can temporarily disable auditing on the target table, perform the operation, and re-enable it. This is a legitimate operational pattern, but document it — your compliance team will want to know that the migration period has a gap in audit history.
The audit history subgrid on a form makes a live query against the audit entity every time the form loads (or when the user navigates to that tab). For records with extensive history — thousands of audit entries — this query can be slow.
Mitigation strategies:
As a rough benchmark: an audit record with two field changes averages about 2KB of storage (the XML structure has overhead). At 100,000 updates per month across audited tables, you're looking at roughly 200MB per month, or 2.4GB per year. At 1 million updates per month — a busy sales or service organization — that's 24GB per year of audit storage.
Storage isn't free in Dataverse. Monitor your audit growth in the Power Platform admin center and build a purge strategy into your governance plan before you need it, not after you've hit storage limits.
Different regulatory frameworks have different requirements, and Dataverse auditing can be configured to meet most of them. Here are the most common scenarios:
GDPR requires demonstrating that personal data was accessed and processed lawfully. This typically means:
If you're building for GDPR, also consider column-level security for the most sensitive fields — audit logging tells you that something changed, but column-level security controls who can change it in the first place. These two capabilities are complementary.
Sarbanes-Oxley typically requires audit trails for financial data modification. Key configuration for SOX:
For SOX, the audit summary export to external storage is non-negotiable. Dataverse itself doesn't guarantee 7-year retention without active management.
ISO 27001 may require demonstrating control over who accessed sensitive records. This is where the Log access setting becomes relevant — enabling read auditing on tables containing classified data. Be prepared for substantial storage growth if you take this route.
Work through this exercise to validate your understanding. Use a development or sandbox environment — don't practice on production.
Your organization manages a Project Budget table with the following custom columns: Budget Amount, Approved By, Status (choice: Draft, Submitted, Approved, Rejected), Contingency Percentage, and Last Modified Notes. You need to configure auditing that:
Budget Amount, Approved By, Status, and Contingency PercentageLast Modified Notes from auditing (it's informal commentary, not a compliance field)Step 1: Enable organization-level auditing Navigate to the Power Platform admin center, find your dev environment, go to Settings → Audit and logs, and enable Start auditing. Leave Log access off.
Step 2: Enable table-level auditing on the Project Budget table In Power Apps, find your Project Budget table, open Properties → Advanced options, and enable Audit changes to its data. Save and publish.
Step 3: Configure column-level auditing Open each column in the Project Budget table. Enable auditing on: Budget Amount, Approved By, Status, Contingency Percentage. Disable auditing on: Last Modified Notes. Save each column change.
Step 4: Generate test audit records Open a Project Budget record and make several changes: modify the Budget Amount, change the Status from Draft to Submitted, and update the Contingency Percentage. Save the record. Make two more rounds of changes with saves in between.
Step 5: Verify audit records in the admin center Go to Power Platform admin center → Settings → Audit and logs → Audit summary view. Filter by your table name and the last hour. You should see three audit records (one per save). Click one and verify you see the before/after values for your changed fields. Verify that Last Modified Notes does not appear in the change data.
Step 6: Add the audit history subgrid to the form Open the Project Budget main form in the form editor. Add a new tab, label it "Change History." Add a section with a single column layout. Insert a Subgrid component: Table = Audits, Default view = Audit History, Show related records = enabled. Set the default row count to 10. Save and publish.
Step 7: Configure security Open the "Budget Manager" security role (or create it if it doesn't exist). Navigate to the Business Management tab → Privacy Related Privileges. Grant View Audit History. Do not grant View Audit Summary. Save the role.
Verification Log in as a user with the Budget Manager role. Open a Project Budget record with changes. Navigate to the Change History tab. Verify that audit records appear with the correct before/after values. Then navigate to Settings in the model-driven app and verify that the user cannot access the org-wide Audit Summary view.
Three things to check in order:
All three must be true. The audit subgrid silently shows empty results if any of these is false — it doesn't display an error message.
Column-level auditing may be disabled on those specific columns. Check each column's audit setting individually. Also verify that the columns aren't formula columns or calculated columns — those are never audited regardless of the column setting.
You likely have a high-volume table with thousands of audit records per record. Implement lazy tab loading (don't load the tab until the user clicks it), reduce the default row count to 10, and consider creating a custom audit view filtered to the last 90 days.
When a record is deleted, its audit history is retained — audit records are not cascade-deleted with the parent record. However, the deleted record won't appear in any standard grid or form (it's gone). To retrieve audit history for deleted records, query the audit entity directly via Advanced Find or the Web API, filtering by objectid if you know the GUID, or by action eq 3 (delete) to find all deletion events.
This happens for Create events and Delete events under certain configurations. For a Create, the "old value" is nothing — some configurations return minimal changedata. For Delete events, changedata captures the field values at the time of deletion, but this behavior can vary based on platform version. If you need reliable field values at deletion time, build a pre-deletion snapshot pattern in Power Automate.
If you didn't set up an export pipeline before purging, those records are gone — there's no recycle bin for audit records. Going forward, implement a scheduled Power Automate flow or Azure Data Factory pipeline that exports audit records older than 60 days to Azure Blob Storage in JSON or Parquet format before purging them from Dataverse.
Warning
Purging audit records in Dataverse is permanent and irreversible. Implement your retention export pipeline before you need to purge, not after you've already cleared the data. This is a one-way door.
You've covered the full stack of Dataverse auditing — from the underlying storage architecture to form-level history surfacing and compliance configuration. Let's consolidate the key decisions you'll need to make when implementing this in a real environment:
Architecture decisions:
User experience decisions:
Operational decisions:
For deeper exploration of the security layer controlling who can do what with audit data, the lesson on model-driven app security roles and field permissions gives you the complete picture. If you're building forms that need to surface change history alongside other complex subgrid patterns, designing model-driven forms with sections, tabs, and subgrids covers the form layout architecture in depth. And if your compliance requirements extend beyond change history to data quality — preventing duplicate records, enforcing data entry standards — Dataverse alternate keys and duplicate detection pairs naturally with an audit strategy.
Auditing is the quiet infrastructure of trustworthy systems. When you need it, you really need it. Build it right before that uncomfortable meeting, not after.
Model-Driven Apps & Dataverse