Table-level security isn't granular enough when you need to hide salary data from managers who can see the rest of an employee record. This lesson teaches you how Dataverse Column Security Profiles work architecturally, how to create and configure them, and how to assign them across security roles, teams, and users — including the edge cases that silently break integrations and audit trails.

Imagine you've built a polished model-driven app for your HR department. Managers can view employee records, update job titles, and track performance — everything working exactly as designed. Then you realize that every manager, every recruiter, and every department head can also see the Salary column you added six months ago. It's right there on the form, visible to anyone with the Employee table's read privilege. No one meant for that to happen; table-level security just wasn't granular enough to stop it.
This is the gap that Dataverse Column Security Profiles were built to fill. Table-level security controls whether a user can read, create, update, or delete an entire record. Column security controls whether a user can read, create, or update a specific field within that record — even if they can see the record itself. It's the difference between "you can access this filing cabinet" and "you can open this filing cabinet, but not the drawer marked Compensation & Benefits." Once you understand this distinction, you'll start seeing column security as a non-negotiable layer in any app that handles PII, financial data, health information, or any other sensitive attributes.
By the end of this lesson, you'll have a thorough, working knowledge of how Dataverse column security profiles are structured, how to create and configure them, and how to assign them to security roles, teams, and individual users with full awareness of the edge cases that trip up even experienced makers.
What you'll learn:
Before diving in, you should have a solid working knowledge of:
You should also have the System Administrator or System Customizer role in the environment where you're working, since column security configuration requires those elevated privileges.
Before touching any configuration, you need to understand the data model behind column security — because the moment you treat it as a simple checkbox, you'll create a false sense of security or accidentally lock yourself out of your own data.
Dataverse security operates in two distinct layers, and both must grant access for a user to read or write a value.
Layer 1 — Table-level security: A user's security role grants (or denies) privileges on a table as a whole. If a role grants Read access to the Employee table at the Organization level, any user with that role can retrieve Employee records. This is where most makers spend their security configuration time.
Layer 2 — Column-level security (CLS): When column security is enabled on a specific column, Dataverse adds an additional check beyond the table-level privilege. Even if a user's role grants Read on the Employee table, they can only see the value in the cr7a3_salary column if they are also assigned a Column Security Profile that grants read permission on that column.
These two layers are independent. You cannot grant column-level access if the user lacks table-level access — column security never elevates a user's table privileges. But you can absolutely restrict column access even when a user has full table access. The combination is what enables the fine-grained model.
Under the hood, a Column Security Profile is a FieldSecurityProfile record. Each profile contains a set of FieldPermission child records — one per secured column — that specify the allowed operations (Read, Create, Update) as true/false values. When a user attempts to read or write a secured column, Dataverse evaluates whether any of the FieldSecurityProfiles associated with that user (directly or through team membership) grants the required permission.
This is an any-grants model, not a least-privilege model. If a user has three profiles assigned and one of them grants Read on cr7a3_salary, the user can read salary — even if the other two profiles deny it. There is no "deny" permission; absence of a permissive profile is the denial. This design matters enormously when you start stacking profiles across teams and roles.
Key insight
Column security uses an additive, grant-only model. If any assigned profile grants a permission, the user has that permission. You cannot create a "deny" profile to revoke access granted by another profile. This means your strategy should be to grant access narrowly rather than trying to deny it selectively after granting broadly.
When you enable column security on a column and a user reads a record without having any profile that grants read access on that column, one of two things happens depending on context:
****). The user knows the field exists but cannot see its content.This distinction matters especially if you're building integrations. A Power Automate flow running under a service account that lacks the correct profile will silently receive empty values rather than an error — a failure mode that can be very hard to diagnose.
Warning
Silent omission in API responses is a significant operational risk. If a Power Automate flow processes employee records and the service account lacks a Column Security Profile granting read on cr7a3_salary, the flow will process records with that field as null/empty — no error, no warning, no indication that anything is wrong. Always test automated integrations under the actual service account identity, not your admin credentials.
Column security is not on by default. You explicitly opt individual columns into the system. Let's walk through this for a realistic scenario: you're building an employee management app, and you need to restrict access to Annual Salary, Social Security Number, and Performance Rating.
Open make.powerapps.com, select your environment, and navigate to Tables in the left navigation. Open the Employee table (or whatever your target table is). Select the Columns tab to see all columns on the table.
Find the column you want to secure — say, Annual Salary (logical name cr7a3_annualsalary). Click to open the column's property panel.
In the column editor, scroll down until you find the Enable column security toggle (sometimes labeled Field Security depending on the maker portal version you're using). Flip it on and save the column.
This single toggle is what registers the column with the column security subsystem. Once enabled:
FieldPermission record with all three permissions set to false (no read, no create, no update) for every existing Column Security Profile in the environment.Warning
Enabling column security on a column is effectively immediate for existing users. The moment you save the column with security enabled, users without a permissive profile lose visibility into that field's value — even in existing sessions. Plan your profile assignments before enabling security on high-visibility columns in production, or at minimum schedule the change during low-traffic hours.
Column security is enabled per-column. You'll repeat this process for Social Security Number and Performance Rating. You can also enable column security via the solution XML if you're automating deployments, which is the recommended approach for managed solution environments. The relevant XML attribute in the column definition is:
<attribute>
<LogicalName>cr7a3_annualsalary</LogicalName>
<IsSecured>1</IsSecured>
...
</attribute>
Setting IsSecured to 1 in your solution's customizations.xml achieves the same result as the UI toggle and travels cleanly through solution import.
A Column Security Profile is essentially a named permission set that you'll assign to users and teams. Think of it as analogous to a security role, but scoped entirely to column-level permissions rather than table-level privileges.
Good profile naming and scoping strategy is where experienced makers diverge from beginners. The temptation is to create one profile per table ("Employee Column Access") but this quickly becomes unmanageable. Better practice is to create profiles that map to job function access needs, which may cut across tables.
For our HR scenario, let's say we have three distinct access needs:
| Profile Name | Who gets it | What they can do |
|---|---|---|
| HR Compensation Reviewer | HR Business Partners, Compensation Analysts | Read salary; cannot update |
| HR Compensation Manager | HR Directors, CHRO | Read + Update salary |
| HR Full Access | HR System Admins | Read + Create + Update all secured HR columns |
| Recruiter Limited Access | Recruiters | Read performance rating; no salary access |
This design gives you four profiles to manage rather than trying to accommodate all variations in one. It also makes auditing clear: when you need to know who can read salary data, you look at who is assigned HR Compensation Reviewer or HR Compensation Manager or HR Full Access.
Column Security Profiles are managed in the Power Platform Admin Center (admin.powerplatform.microsoft.com), not in the Power Apps maker portal. This is a common source of confusion.
Navigate to your environment, then go to Settings > Users + permissions > Column security profiles. Select + New profile, enter a name and optional description, and save.
After saving, the profile editor opens. You'll see a tab called Field Permissions (sometimes it's accessed via the Related tab on older UIs). This is where you define what each secured column allows for this profile.
In the Field Permissions tab, you'll see a list of every column in the environment that has column security enabled. For each column, you can set:
For the HR Compensation Reviewer profile:
cr7a3_annualsalary → Read: Yes, Create: No, Update: Nocr7a3_socialsecuritynumber → Read: No, Create: No, Update: Nocr7a3_performancerating → Read: No, Create: No, Update: NoFor the HR Compensation Manager profile:
cr7a3_annualsalary → Read: Yes, Create: No, Update: Yescr7a3_socialsecuritynumber → Read: Yes, Create: No, Update: Nocr7a3_performancerating → Read: Yes, Create: No, Update: YesYou set these by opening each field permission record and changing the picklist values from "No" to "Yes" for the desired operations.
Tip
When you have many secured columns, editing field permissions one at a time through the UI becomes tedious. Consider using the Power Platform CLI or a simple Power Automate cloud flow against the FieldPermission entity to bulk-configure permissions programmatically. The entity's key attributes are fieldpermissionid, entityname, attributelogicalname, canread, cancreate, and canupdate.
Here's a minimal API call structure (using OData) to update a field permission record programmatically:
PATCH https://yourorg.crm.dynamics.com/api/data/v9.2/fieldpermissions(GUID)
Content-Type: application/json
{
"canread": 4,
"cancreate": 1,
"canupdate": 1
}
The integer values map to the field_security_types option set: 1 = Not Allowed, 4 = Allowed. Yes, that's a quirky choice — 4 for allowed, not 1. The gap in the numbering is historical baggage, so don't let it trip you up when scripting.
This is where the architecture gets genuinely interesting — and where most column security mistakes are made.
You can associate a Column Security Profile directly with a Security Role. When a user has a role that's associated with a profile, they inherit the profile's column permissions. This is the most scalable assignment method because it follows your existing role structure.
To add this association, open the Column Security Profile in the Power Platform Admin Center. Navigate to the Security roles tab (or Related > Security roles) and add the roles you want to associate. Every user with those roles in this environment will now have the profile's permissions applied.
In our example, the HR Compensation Reviewer profile would be associated with the HR Business Partner and Compensation Analyst security roles.
Note
The association between a profile and a security role is different from the security role itself. The security role still controls table-level access. The profile adds column-level access on top. Removing the role from the profile association does not change the role's table privileges — it only removes the column permission grant.
Teams are often the more flexible assignment mechanism, especially for column security that needs to change dynamically (e.g., an employee joins or leaves the compensation committee). When a Column Security Profile is assigned to a team, every member of that team inherits the profile.
To assign a profile to a team, open the profile in Power Platform Admin Center, go to the Teams tab, and add your teams. You can assign to both Owner Teams and Access Teams (though owner teams are more commonly used for column security, as access teams are typically created dynamically per record for sharing scenarios).
You can also assign a profile directly to a specific user. Navigate to the profile's Users tab and add users individually. This is appropriate for:
Direct user assignment is straightforward but creates governance overhead — it's harder to audit and maintain than role or team-based assignment. Use it sparingly.
Remember the additive, grant-only model mentioned earlier. When evaluating whether User A can read cr7a3_annualsalary, Dataverse checks:
If any of these results in a profile where canread = 4 (Allowed) for cr7a3_annualsalary, the user can read the field. Period.
This means if you accidentally assign the HR Full Access profile to a broad team (say, the root business unit's default team), every user in the environment gets full column access — completely defeating the purpose. This is by far the most common serious mistake with column security.
Warning
The default team for a business unit automatically contains every user in that business unit (and child business units). If you assign a permissive Column Security Profile to a default team, you've effectively granted that column access to everyone in that business unit. Always verify team membership before assigning profiles to teams, especially teams that might be default or broadly-scoped.
Understanding how column security surfaces in the UI is critical for both designing good user experiences and diagnosing problems.
When a user without read access opens a record containing a secured column, the form renders the field with **** (asterisks) as a placeholder. The form itself doesn't hide the field — it shows the field label, the control widget, and the masked value. This is intentional: hiding the field entirely could confuse users who expect to see it, and the masking makes it clear that data exists but is restricted.
If you want the field to be completely invisible to users without access, you have two options:
OnLoad event to hide the control based on a check — but this is client-side only and doesn't constitute a security boundaryThe second approach is more robust. Users with restricted column access get a form variant that simply doesn't include those fields. Column security remains the enforcement layer; the form design provides the UX cleanliness.
If a secured column is included in a view, users without read access will see the column in the view grid but the cells will be empty — not asterisked, just blank. This can be confusing because it looks like the field simply has no data. When designing views that include secured columns, consider whether the column belongs in a view at all for general audiences.
From a view configuration perspective, you should create role-specific views that include secured columns only for views surfaced to users who will have the necessary profile assigned. While this doesn't prevent a user from adding the column to their personal view, it keeps the default experience coherent.
Tip
Editable grids add an extra wrinkle — if a secured column is in an editable grid and the user lacks update permission, the cell will appear read-only in edit mode. This is handled automatically by the platform, but verify it in your specific app version, as behavior has changed across Power Apps releases.
Quick view forms — the summarized record views that appear when you hover over a lookup field — also respect column security. If a quick view form includes a secured column and the viewing user lacks read permission, they'll see asterisks in the quick view panel. This is important to know because quick view forms are often designed by people who don't think about column security; the rendering behavior can seem like a bug until you understand what's happening.
The interaction between column security and API access is where things get operationally serious, especially in complex environments with multiple integrations.
When a call to the Dataverse Web API retrieves a record containing a secured column, the response depends on the calling user's (or application user's) column security profile:
There is no HTTP 403 Forbidden response for individual fields. The record itself returns successfully; the field simply isn't there. This means any integration that processes these fields needs to handle null/missing gracefully and should be tested under the actual service principal identity.
For write operations:
PATCH or POST payload includes a secured column and the calling user lacks create/update permission, the entire request fails with a 403 error — not just the field. This asymmetry between read (silent omission) and write (hard failure) is architecturally significant.When you execute a FetchXML query that includes a secured column in its <attribute> list, the same rules apply: the column is omitted from results if the user lacks read access. If you use an <order> or <filter> on a secured column and lack read access, the behavior is environment-version-dependent but generally the filter is silently ignored or the query fails. Don't rely on filtering secured columns for security enforcement — a user without read access shouldn't be issuing those queries at all.
Power Automate flows, Azure Logic Apps, and other automated integrations run under either:
Application users (non-interactive users authenticated via Azure AD app registration) are subject to column security just like regular users. To grant an application user access to secured columns, you must assign a Column Security Profile directly to that user record in Dataverse (since app users can't hold security roles the same way interactive users can in some configurations, though they can be assigned to teams).
The reliable pattern is to create a dedicated team (e.g., "Integration Service Accounts"), assign appropriate Column Security Profiles to that team, and add your application users to the team. This gives you clear governance and a single place to manage integration permissions.
Column security interacts with platform features in non-obvious ways that you need to understand before deploying.
Business rules in Dataverse that reference secured columns are evaluated server-side, so the platform's system context (which has full access to all column values) executes the rule logic regardless of the current user's column security profile. This means a business rule can read a salary value and set a hidden field based on it — and the business rule will work correctly even if the triggering user can't see the salary column directly.
However, a business rule that displays a value by setting another field's value will surface that derived value to the user. Indirectly exposing secured data through business rule outputs is a real concern and requires careful design.
Formula columns and rollup columns that reference secured columns are more nuanced. The calculation itself runs in system context and will access the secured column's value. But whether the result of the formula is visible depends on whether the formula column itself is secured.
If you create a formula column like AnnualSalary / 12 to show monthly salary, and that formula column is not secured, users who cannot see cr7a3_annualsalary might still be able to compute the salary from the formula column's visible result. This is a data leakage pattern that's easy to miss. The correct approach: if the source column is sensitive, the derived column should either also be secured or structured so the result doesn't reveal the source value.
Plugins executing in system context (which is the default for synchronous plugins registered with a specific user account that has System Administrator) can read and write secured columns regardless of column security settings. This is by design — the server layer needs unrestricted data access to perform operations. If you have a plugin that writes audit information to a secured column, it will work fine. But be aware that plugins cannot be used to bypass column security for end-user operations — the enforcement happens at the data access layer before the plugin firing for retrieves.
Given that column security protects your most sensitive data, knowing who accessed what and when is critical. For regulated industries, it's not optional.
Dataverse auditing, when enabled for a table and column, records read and write operations. Importantly, auditing operates independently of column security — even attempts to access secured columns that result in masked values can be logged if you configure audit to track reads.
For the most complete picture, enable auditing on both the table and the individual secured columns. You can view this audit history in the model-driven app's audit log views. For a deeper walkthrough of how to configure this, see Configuring Dataverse Auditing and Field-Level Change History in Model-Driven Apps.
Key insight
Enabling column security without enabling auditing on sensitive columns is a half-measure. You've restricted access, but you have no visibility into whether the restriction is working, whether profiles are assigned correctly, or whether unusual access patterns are occurring. For GDPR, HIPAA, or SOX compliance scenarios, you need both.
If you're building in a proper application lifecycle management (ALM) pipeline — which you should be for any production system — you need to understand how column security travels through solutions.
When you export a solution, the following column security artifacts are included automatically:
IsSecured flag on column definitions (travels as part of the table component)What does not automatically travel is:
This is a deliberate design decision: user and team membership is environment-specific. You cannot (and should not) have a managed solution that prescribes which users have access to sensitive columns in a production environment.
This creates an ALM challenge: your deployment process needs a post-import step that configures profile assignments in the target environment. Common approaches include:
Tip
Document your profile assignment matrix in a separate governance document that travels with your solution documentation. During a migration to production, this matrix is the checklist your admin works through. Without it, you'll inevitably forget an assignment and end up with either over-privileged or under-privileged users in the new environment.
You can lock Column Security Profile definitions using managed properties if you're shipping a managed solution to a customer or downstream environment. This prevents administrators in the target environment from modifying the profile's field permissions. However, they can still assign or remove users, teams, and roles from the profile — managed properties cannot control those associations. For ISV-grade solutions, this is usually the right balance: you control the permission definitions, the customer controls the assignments.
This exercise walks you through a complete end-to-end column security configuration for a hypothetical financial services scenario. You'll secure a column, build profiles, assign them, and verify the behavior.
Scenario: You have a Loan Application table with a Credit Score column (cr8b1_creditscore) that should be visible only to Loan Officers and Credit Analysts, not to general Customer Service Representatives.
Credit Score column**** where the credit score value should appearLoan Officer Credit Accesscr8b1_creditscore (Loan Application > Credit Score) and open itRepeat Step 2 with profile name Credit Analyst Full Access, but configure the field permission as:
Loan Officer Credit Access profileLoan Officer security roleCredit Analyst Full Access profileCredit Analyst security roleTest as three different users:
**** in the Credit Score fieldAlso test via the API using each user's credentials:
GET https://yourorg.crm.dynamics.com/api/data/v9.2/cr8b1_loanapplications(GUID)?$select=cr8b1_creditscore
Verify that the Customer Service Rep response omits cr8b1_creditscore from the JSON payload entirely.
Add Credit Score to the All Loan Applications view. Log in as a Customer Service Rep and observe that the column appears in the grid header but all cells are blank (not asterisked — this is the view behavior).
Symptom: Everyone in the environment can suddenly read a secured column.
Cause: A column security profile was assigned to the root business unit's default team, which includes all users.
Fix: In Power Platform Admin Center, open the profile and remove the default team from the Teams tab. Add only the specific, deliberately-scoped teams you intend.
Symptom: A Power Automate flow returns null or empty values for a secured column in its "Get record" action, even though the column clearly has data.
Cause: The service account or flow connection user lacks a Column Security Profile granting read on the column.
Fix: Create or identify the appropriate profile for the integration, and assign it directly to the application user record, or add the app user to a team that has the profile assigned.
Symptom: Users who cannot see the Annual Salary column can compute it from a visible formula column.
Cause: A formula or rollup column was built from the secured column but was not itself secured.
Fix: Enable column security on the derived column and assign appropriate profiles, or redesign the formula to not expose the sensitive value.
Symptom: Profile sprawl — 40+ profiles that no one can explain, some granting overlapping permissions.
Cause: Profiles were created ad hoc without a naming or design convention.
Fix: Audit your profiles using a FetchXML query against FieldSecurityProfile and FieldPermission. Consolidate redundant profiles and document the intended assignment matrix.
<fetch>
<entity name="fieldsecurityprofile">
<attribute name="name" />
<attribute name="fieldsecurityprofileid" />
<link-entity name="fieldpermission" from="fieldsecurityprofileid" to="fieldsecurityprofileid" alias="fp">
<attribute name="entityname" />
<attribute name="attributelogicalname" />
<attribute name="canread" />
<attribute name="cancreate" />
<attribute name="canupdate" />
</link-entity>
</entity>
</fetch>
Run this in XrmToolBox's FetchXML Builder or via the API to get a complete picture of your current configuration.
Symptom: After disabling column security on a column, you still have orphaned FieldPermission records in your profiles, and the permission editing UI shows columns that no longer exist as secured.
Cause: Disabling column security on a column removes the security enforcement, but existing FieldPermission records for that column in all profiles remain in the database.
Fix: After disabling column security on a column, do a FetchXML query for all FieldPermission records where attributelogicalname matches the column you just unsecured, and delete them. This is housekeeping that the platform doesn't do automatically.
Symptom: Users in offline-enabled apps (using the mobile app offline feature) can see secured column values they shouldn't be able to access.
Cause: Column security enforcement depends on server-side evaluation. In offline mode, cached data may include secured field values that were cached when the user had a different profile, or the offline cache may not consistently apply column security.
Fix: For highly sensitive columns, evaluate whether those tables should be enabled for offline sync at all. If they must be offline-available, ensure the offline data profile is scoped appropriately and test thoroughly.
Column security profiles are one of the most powerful and most underused security capabilities in Dataverse. The core principle to carry forward is the two-layer model: table security controls record access, column security controls field-level access within records that you can already see. Neither layer substitutes for the other.
The key operational decisions you now know how to make are:
From here, your natural next areas to explore are the broader record-sharing model (how sharing allows ad-hoc access that also works alongside column security), and the audit and compliance configuration that gives you observability into your security posture. For the record-sharing side, Column-Level Security and Record Sharing in Dataverse covers the complementary sharing mechanisms in depth. For auditing configuration, Configuring Dataverse Auditing and Field-Level Change History in Model-Driven Apps walks through the full audit pipeline.
If you're working through the broader model-driven app security picture, Model-Driven App Security: Configuring Security Roles, Field Permissions, and Team-Based Access for Table Data provides the complete table-privilege layer that column security sits on top of — essential reading for anyone designing a production-grade security model.
Column security done well is invisible to users who have the access they need and unambiguous to users who don't. Getting there requires care in design, discipline in governance, and genuine understanding of the architecture. You now have all three.