If you’ve ever needed to answer “who changed this field, and what did it say before?” inside a Dataverse-backed app, you’ve needed auditing. It’s one of those features that looks trivial in the maker portal — a couple of checkboxes — but has enough cascading rules, gotchas, and storage implications that it’s worth understanding properly before you flip the switches in a production environment.
This guide walks through how Dataverse auditing actually works, what the default state is at each level, sets up a small sample table so the concepts have something real to attach to, and then enables auditing one layer at a time — so you can see what each layer adds rather than just reading about it.
Why Dataverse Auditing Exists
Dataverse auditing exists to answer governance and compliance questions: who created or changed a record, which columns changed, what the previous value was, who accessed the system, and who deleted something. It’s not a general-purpose history feature — it’s purpose-built for internal and external compliance, security reviews, and regulatory governance, and it stores its data in a separate log storage tier rather than regular database storage.
That last detail matters more than it sounds. Because audit data lives in log storage, it consumes its own capacity allocation (separate from database and file capacity), and entries can show up with a short delay in the Audit History tab or Audit Summary view rather than appearing instantly.
The Three Layers of Auditing — Defaults and Cascade Rules
Auditing in Dataverse is configured at three levels, and each level gates the one below it:
- Environment level — the master switch. Nothing below this matters until it’s on.
- Table level — turns on auditing for a specific table, but only takes effect if the environment switch is on.
- Column level — the most granular layer. A column’s changes are only captured if both the table and the environment are also enabled for auditing.
Default State at Each Level
Before you touch anything, here’s what Dataverse ships with out of the box:
| Level | Default state | Notes |
|---|---|---|
| Environment | Off | Auditing is disabled in all newly provisioned environments. You must explicitly turn it on. |
| Table (standard) | Varies by table | Many core Dynamics 365 tables (Account, Contact, Lead, Opportunity, etc.) ship with table-level auditing pre-enabled. Custom tables you create default to off. |
| Column | On for most columns | When you add a column to a table that has auditing enabled, the column’s own audit checkbox defaults to on — meaning the new column is tracked from creation. You have to explicitly opt out at the column level. |
That last default surprises people the most. It means: once a table is audited, newly added columns are captured automatically unless you deliberately turn them off. The column-level setting exists primarily for exclusion, not inclusion.
The Cascade Dependency
This cascading dependency is the single most common point of confusion (and a frequent exam trap): enabling auditing on a column does absolutely nothing if the table isn’t audited, and enabling a table does nothing if the environment isn’t audited. Turning off any layer silently disables everything underneath it — Dataverse won’t warn you that your carefully configured column settings are now inert.
You need the System Administrator or System Customizer security role (or an equivalent custom role) to change any of these three settings.
Here’s how the dependency chain looks for the sample table we’ll build later in this post:
What Actually Gets Logged — and What Doesn’t
A subtlety that trips people up: auditing doesn’t capture every interaction with a record — only specific operations, and only when the new value of a column differs from the old value.
The Practical Example: A “Vendor Invoice” Table
Concepts stick better with a concrete example, so let’s build a small custom table and audit it incrementally — enabling one layer at a time and checking what we can see at each stage. The incremental approach mirrors what happens in real implementations: you’re often called in to troubleshoot “the audit trail isn’t working” and the root cause is almost always a layer that wasn’t turned on.
Table: Vendor Invoice (demo_vendorinvoice)
| Display Name | Schema Name | Data Type | Purpose |
|---|---|---|---|
| Invoice Number | demo_invoicenumber | Text (Primary column) | Unique invoice identifier, e.g. INV-1001 |
| Vendor Name | demo_vendorname | Text | Supplier name |
| Invoice Amount | demo_invoiceamount | Currency | Total invoice value |
| Invoice Date | demo_invoicedate | Date Only | Date the invoice was issued |
| Payment Status | demo_paymentstatus | Choice (Pending, Approved, Paid, Rejected) | Approval workflow state |
| Approver Notes | demo_approvernotes | Multiline Text | Free-text internal comments |
| Is Confidential | demo_isconfidential | Yes/No | Flags sensitive vendor contracts |
We’ll enable auditing on Payment Status, Invoice Amount, and Is Confidential — the fields a financial controller actually cares about — and deliberately leave Approver Notes unaudited, as if it were considered low-value noise for the audit trail. But we’ll get there incrementally, starting from zero.
Phase 1: Environment-Only Auditing (Nothing Works Yet)
Step 1 — Turn On Auditing at the Environment Level
This is the master switch, and there are two equivalent ways to set it.
Option A — Power Platform admin center, Compliance page (good for turning on auditing across a standard set of common entities in one action):
- Sign in to the Power Platform admin center.
- Go to Security → Compliance, select the Auditing tile, then choose your environment.
- Select Set up auditing → Turn on auditing.
- Optionally select Common entities across Dynamics 365 apps to bulk-enable a standard set of tables (Account, Contact, Lead, and similar).
- Set the Event log retention period to match your data retention policy.
Option B — Environment Settings (the route we’ll use for our custom table):
- In the admin center, go to Manage → Environments, select your environment, then Settings.
- Expand Audit and logs → Audit settings.
- Set the retention period (e.g., 90 days for this exercise).
- Select Global Audit Settings, turn on Start Auditing, and save.
A couple of settings worth understanding here:
- Log access records every sign-in/system access event.
- Read logs sends activity-log data — record reads and exports — to the Microsoft Purview compliance portal, a separate destination from the Dataverse Audit History tab. This option only appears once minimum Microsoft 365 licensing requirements are met, and is sent for production environments only.
- Setting retention to Forever means nothing is ever auto-deleted; any other value starts a rolling deletion the moment a record crosses that age.
Environment-Level Auditing Settings at a Glance
| Setting | What It Captures | Where Data Is Stored | Visible In | Notes |
|---|---|---|---|---|
| Start Auditing | Creates, updates, and deletes on audited tables | Dataverse log storage (separate from database capacity) | Audit History tab (per record) & Audit Summary View (environment-wide) | Master switch — nothing works without this. Table and column auditing must also be enabled separately. |
| Log Access | User sign-in and system access events | Dataverse log storage | Audit Summary View only — not the per-record Audit History tab | Independent of table/column configuration. Enable if you need to answer “who accessed the environment and when?” |
| Read Logs | Who viewed or exported records | Microsoft Purview (not Dataverse) | Microsoft Purview portal — completely separate from Dataverse | Requires: (1) minimum Microsoft 365 E1 license, (2) production environment only, (3) Enable SAS Logging in Purview toggled on under Power Platform Admin Center → Settings → Product → Privacy + Security. If column auditing is later disabled, Purview will show * instead of before/after values. |
Important: Enabling Read Logs alone is not enough to see data in Purview. You must also enable Enable SAS Logging in Purview per environment under Power Platform Admin Center → Settings → Product → Privacy + Security.
What You Can See After Phase 1
With only the environment switch on, create a new Vendor Invoice record (INV-1001) and then open its Related → Audit History tab.
You’ll see: nothing. The Audit History tab is empty.
This is the expected result and an important lesson: the environment switch is necessary but not sufficient. With table-level auditing still off (the default for custom tables), no data is being captured for Vendor Invoice regardless of the environment setting. The environment switch just means “auditing could work in this environment” — it doesn’t make it happen for any specific table.
Diagnostic tip: If you’re troubleshooting a missing audit trail in a real environment, the very first thing to verify is that the environment switch is on. It’s easy to miss in a newly provisioned or restored environment where defaults reset.
Phase 2: Add Table-Level Auditing (Row Operations Appear)
Step 2 — Turn On Table-Level Auditing
- Open Power Apps maker portal, select your environment.
- Go to Tables, select Vendor Invoice, then Edit table properties.
- Expand Advanced options and check Audit changes to its data.
- Save and publish.
If you’re customizing inside a solution (recommended for any real implementation — it keeps your audit configuration exportable and tied to your publisher prefix), add the table to your solution first, then perform these steps from inside the solution before publishing.
What You Can See After Phase 2
Now go back and create a new record (or re-open INV-1001 if you already have one):
- Invoice Number:
INV-1001 - Vendor Name:
Fabrikam Supplies - Invoice Amount:
$4,500.00 - Invoice Date: today
- Payment Status:
Pending - Is Confidential:
No - Approver Notes:
Awaiting manager approval
Open Related → Audit History on the record.
You’ll now see a Create entry. It shows your user, a timestamp, and the fact that the record was created — but it does not show a list of individual field values captured at creation. That’s normal: the Create event is a row-level event.
Now change Payment Status from Pending to Approved and save. Check Audit History again.
You’ll see the Update entry — but with no column detail. The entry will show that a change happened, but because no columns have column-level auditing configured yet, Dataverse doesn’t know which specific columns to track. The row changed; which fields changed is not recorded.
This is the key behavior that Phase 2 exposes: table-level auditing alone tells you that a record was touched and by whom, but not what specifically changed. For “who deleted this?” questions, that’s often enough. For “who changed the invoice amount, and what was it before?” — you need Phase 3.
Reminder about column defaults: When you first enabled the table in Step 2, all columns technically have column-level auditing defaulting to on. However, this only takes effect going forward — records created or changed before table-level auditing was enabled won’t have retroactive history. And more practically: you won’t see old-value/new-value detail in Audit History until you’ve verified the column-level settings in the next phase.
Phase 3: Add Column-Level Auditing (Field Changes Captured)
Step 3 — Configure Column-Level Auditing
- Still inside the Vendor Invoice table, go to Columns.
- Open Payment Status, expand Advanced options, verify Enable auditing is checked (it should default to on), save.
- Repeat for Invoice Amount and Is Confidential.
- Open Approver Notes and uncheck Enable auditing — this is intentional. We’re treating this field as low-value noise for the audit trail.
- Publish all customizations.
Why uncheck Approver Notes explicitly? Remember the default: columns default to auditing on when their table is audited. If you want a column excluded, you have to opt it out deliberately. This split configuration — some columns tracked, some not — is exactly the kind of selective decision real solutions make, and it’s the easiest way to prove that column-level auditing is genuinely independent.
What You Can See After Phase 3
Now make a change to Payment Status from Pending to Approved and save.
You’ll now see full before/after detail in Audit History — the field name, the old value (Pending), the new value (Approved), who made the change, and when.
Make another change to Invoice Amount from $4,500.00 to $4,750.00 and save.
Again, a new entry with old and new values appears.
Now change Approver Notes to Confirmed with finance lead and save.
Nothing new appears in Audit History for this change. This is the proof that column-level auditing is genuinely independent: the table is being audited, the record clearly changed, but because Approver Notes has auditing disabled, no entry is generated for it. If you saw an entry here, your column-level configuration didn’t take.
Here’s roughly what the Audit History looks like after Phases 1, 2, and 3 combined:
Additional Tests at Full Configuration
With all three layers active, a few more behaviors are worth verifying.
Test: Share the Record
Share the INV-1001 record with another user (or team), granting Read access.
Expected result: An audit entry is logged reflecting the change to sharing privileges — this is tracked at the table level automatically and doesn’t require a specific column to be enabled.
Test: Delete the Record
Delete INV-1001.
Expected result: Because the record itself no longer exists, you can’t open its Audit History tab anymore. Instead, go to the environment-wide Audit Summary view (via Power Platform Environment Settings app → Advanced Settings → System → Auditing → Audit Summary view) and filter by table or user. You’ll find a Delete entry for INV-1001 there — this is the main reason the Audit Summary view exists: it’s the only place to see audit trail for records that are no longer in the system.
Test: No-op Update
Open a record, change Payment Status to Approved, then immediately change it back to Pending and save in a single operation.
Expected result: No audit entry is created, because the saved value is identical to the previous saved value. Auditing evaluates the delta at save time, not the edit history within a session.
Verifying Programmatically
For automated tests or integrations, you don’t have to click through the UI — Dataverse exposes dedicated messages for retrieving audit data via the Web API or the .NET SDK:
| Message | What it returns |
|---|---|
RetrieveRecordChangeHistory | The full change history for all audited columns of a single record |
RetrieveAttributeChangeHistory | The change history for one specific column of a record |
RetrieveAuditDetails | The complete detail of a single audit record |
A minimal Web API call to pull a record’s full change history looks like this:
GET [Organization URI]/api/data/v9.2/demo_vendorinvoices(<record-guid>)/Microsoft.Dynamics.CRM.RetrieveRecordChangeHistory
This is the approach to use if you want to write a regression test that asserts “changing Payment Status produces exactly one audit entry, changing Approver Notes produces zero” — essentially automating the Phase 3 tests above.
You can’t delete individual audit rows directly through the Audit table, though — deletion only happens through dedicated messages: DeleteRecordChangeHistory (wipes a single record’s trail), BulkDelete (asynchronous, query-based, for large volumes), and DeleteAuditData (specific to environments using customer-managed encryption keys).
Managing Retention and Storage
A few behaviors around retention and cleanup are easy to get wrong:
- Retention changes aren’t retroactive. Each audit log is stamped with whatever retention period was active when it was created. If you change the policy from 30 to 90 days, existing logs keep expiring on the old 30-day clock — only new logs get 90 days.
- The maximum custom retention is 24,855 days, and the option isn’t available at all for Dynamics 365 Customer Engagement (on-premises) or environments using customer-managed encryption.
- Deletion is irreversible — once an audit log is deleted, that period’s history is gone for good.
- There’s both a legacy deletion process (delete only the single oldest log, repeatedly) and a newer process that lets you delete by table, delete all access logs, or delete everything up to a chosen date — all as asynchronous background jobs you can monitor through the Bulk Deletion view. Expect roughly 100 million records deleted per day (about 4 million per hour).
- Large text fields are capped at 5 KB (~5,000 characters) in audit storage — a truncated value ends in three dots, e.g. “Confirmed with finance lead, pending fi…”.
- If you turn off auditing on a column that was previously audited, Purview activity logs will show before/after values as
*going forward rather than silently omitting the entry.
Dataverse Audit History vs. Microsoft Purview Activity Logging
These two systems get confused constantly because they’re both called “auditing,” but they serve different purposes and live in different places:
| Dataverse Audit History | Microsoft Purview (via Read logs) | |
|---|---|---|
| Captures | Create / Update / Delete / Share / Associate / security role changes | Record reads and exports, plus optional sign-in access |
| Where you view it | Audit History tab (per record) and Audit Summary view (per environment) | Microsoft Purview compliance portal |
| Typical use case | ”What changed on this record, and who changed it?” | Centralized, cross-Microsoft-365 compliance review of who viewed or exported data |
| Requires | Environment + table + column auditing enabled | Environment-level “Read logs” + minimum Microsoft 365 licensing |
For most in-app, day-to-day scenarios, table- and column-level auditing with the Audit History tab is all you need. Reach for Purview activity logging when your compliance team needs centralized visibility across Microsoft 365 and Power Platform together, or needs to track read access rather than just changes.
Common Pitfalls
A short list of things that tend to catch people off guard:
- Forgetting the cascade. Re-enabling a column after a table was temporarily turned off doesn’t backfill anything, and if the table or environment switch is off, the column setting is just sitting there inert.
- Misreading the column default. Columns default to auditing on, but that only has any effect after the table is audited. Many people interpret “I didn’t configure the column” as “the column isn’t audited” — it’s the opposite. If you want a column excluded, you have to explicitly disable it.
- A small set of system tables can never be audited, regardless of configuration — examples include
ActivityPointer,Annotation,Workflow,KbArticle, andResource. If a table doesn’t expose the audit checkbox, this is usually why. - Choice (picklist) values audit by current label, not by the label active at the time of the change — unless you explicitly set the
StoreLabelNameforPicklistAuditsorganization setting totrue. Without it, renaming a choice option later will rewrite how old audit entries display, which can be surprising during a compliance review. - Manage audit configuration inside a solution, not directly against the default/unmanaged layer — it keeps the setup portable across environments and tied to your publisher prefix, which matters the moment you need to promote the configuration through dev/test/prod.
- Audit Summary view filtering has known quirks — the Record column filter doesn’t work, and the Entity column’s Equals/Does not equal options don’t populate values; use Contains and type the table name instead.
- Exporting audit logs isn’t supported from the UI at all — if you need the data outside Dataverse, you’re going through the Web API/SDK messages, or linking the Audit table via Azure Synapse Link for Power BI reporting.
- No retroactive fill. Enabling auditing at any level — environment, table, or column — only starts capturing from that moment forward. Changes made before auditing was enabled are gone.
Quick Reference
Default States
| Level | Default | What it means in practice |
|---|---|---|
| Environment | Off | All newly provisioned environments start with auditing disabled |
| Table (custom) | Off | Your custom tables aren’t audited until you explicitly enable them |
| Table (standard Dynamics 365) | On for many | Core tables like Account and Contact ship with auditing pre-enabled |
| Column | On | Columns default to being audited once their table is audited — opt out explicitly if needed |
Configuration Locations
| Layer | Where to configure | Depends on | Typical role required |
|---|---|---|---|
| Environment | Power Platform admin center → Environment Settings, or Compliance page | Nothing — top of the chain | System Administrator / System Customizer |
| Table | Maker portal → Table → Edit table properties → Advanced options | Environment auditing ON | System Administrator / System Customizer |
| Column | Maker portal → Table → Columns → Edit column → Advanced options | Table and environment auditing ON | System Administrator / System Customizer |
Where to View Results
| Action | Where to view it |
|---|---|
| Single record’s history | Audit History tab on the record (needs View Audit History privilege) |
| All audit logs, environment-wide | Audit Summary view (needs View Audit Summary privilege) |
| Deleted record’s history | Audit Summary view only — the record’s own tab no longer exists |
| Programmatic retrieval | RetrieveRecordChangeHistory, RetrieveAttributeChangeHistory, RetrieveAuditDetails |
Wrapping Up
The mechanics of Dataverse auditing are simple once you’ve seen them work end to end: three cascading switches with distinct defaults, a defined set of operations that get captured, and two places to go looking for the result depending on whether the record still exists.
The incremental Vendor Invoice walkthrough above deliberately surfaces what each layer adds on its own — an empty Audit History tab with only the environment on, row-level events with no field detail once the table is enabled, and finally full before/after column values once column-level auditing is confirmed. That progression is the fastest way to internalize why all three switches matter, and it’s also your mental model for diagnosing a broken audit trail in production: check the environment, then the table, then the columns, in that order.