Standard lookups point to one table. But real business data is messier than that — a customer might be an Account or a Contact, an expense might belong to a Project or a Department. Learn how Dataverse polymorphic lookups, Customer columns, and Regarding columns solve this cleanly, and how to configure them end-to-end in model-driven apps.

Here's a scenario that shows up constantly in real business systems: you're building a support ticketing app, and every support case needs to be associated with a "customer." But your business has two kinds of customers — individual contacts and corporate accounts. Sometimes the person filing the ticket is a contact under an account. Sometimes it's a small business owner who is the account. And sometimes you need to treat both the same way for reporting, routing, and SLA purposes.
If you try to solve this with two separate lookup columns — one for Account and one for Contact — you end up with ambiguous data, validation headaches, and forms that feel wrong. The user has to know which field to use, and nothing prevents them from filling in both. What you actually need is a single column that can point to either table depending on the record. That's exactly what a polymorphic lookup — and the specialized Customer column that ships with Dataverse — is designed to do.
By the end of this lesson, you'll understand how polymorphic lookups work under the hood in Dataverse, when to use the built-in Customer column versus building your own multi-table relationship, how to configure both on forms and views in a model-driven app, and how to write logic that handles the type-switching behavior correctly. This is the kind of modeling knowledge that separates data architects from form builders.
What you'll learn:
You should be comfortable with standard Dataverse table relationships and lookup columns before working through this lesson. If you need a refresher on one-to-many relationships, how lookups reference parent records, and cascade behaviors, work through Configuring Dataverse Table Relationships in Model-Driven Apps: One-to-Many, Many-to-Many, and Cascade Behaviors Explained first. You should also understand Designing a Dataverse Data Model: Relationships, Lookups, and Choice Columns — specifically how lookup columns store a record reference including both the GUID and the target table's logical name.
Familiarity with model-driven app form design is helpful. If you've built at least one form with tabs, sections, and subgrids, you're in good shape. See Designing Model-Driven Forms: Sections, Tabs, Subgrids, and Quick View Forms if you need that foundation.
A standard lookup column in Dataverse stores a reference to exactly one target table. When you create a lookup from Case to Account, Dataverse adds a foreign key column (accountid) and a corresponding string column (accountid_name) to the Case table. The type is fixed at design time. Every row in that lookup column will always point to an Account record.
A polymorphic lookup relaxes that constraint. Instead of pointing to one fixed table, it can point to records across multiple tables. Internally, Dataverse still stores a GUID and a name, but it also stores a third piece of information: the object type code of the target entity. That type code tells Dataverse which table the GUID belongs to.
This is how Dataverse can have a single regardingobjectid column on the Activity table that might point to an Account in one row, an Opportunity in the next row, and a custom Quote table in the row after that. The schema is flexible by design.
Key insight
Polymorphic lookups don't use a single foreign key column the way standard lookups do. Behind the scenes, Dataverse stores the GUID in a column like regardingobjectid, the entity type in regardingobjecttypecode, and the display name in regardingobjectidname. When you work with these in Power Automate or the Web API, you'll encounter all three fields.
There are three flavors of polymorphic lookup behavior you'll work with in practice:
Understanding which type fits your scenario is the first design decision.
The Customer column is one of those features that's easy to overlook because it doesn't show up in the column type picker the way a standard lookup does. It was originally designed for the Sales module, where a Case or Opportunity often belongs to either an Account or a Contact. You'll find it pre-configured on the Case table (the customerid column) and on the Invoice, Order, and Quote tables.
When you add a Customer column to a custom table, Dataverse actually creates two separate one-to-many relationships in the background:
It then exposes both through a single UI control on forms. The user sees one lookup field, but they can search across both Accounts and Contacts, and the stored value resolves to whichever they choose.
Let's say you're building a Service Request table for a field services company. A service request can be submitted by either an Account (a business client) or a Contact (an individual homeowner). This is a textbook Customer column scenario.
Navigate to your solution in the Power Apps maker portal (make.powerapps.com), open your Service Request table, and go to the Columns panel.
Select Add column. In the column type dropdown, scroll down to find Customer. Give it a display name like "Requested By" and the logical name will be something like cr4f2_requestedby. Save the column.
When you save, Dataverse quietly creates:
cr4f2_account_cr4f2_servicerequest — the relationship to Accountcr4f2_contact_cr4f2_servicerequest — the relationship to ContactYou can verify this by going to the Relationships tab of your Service Request table. Both will appear as separate one-to-many relationships, each with its own cascade configuration.
Tip
Because the Customer column creates two independent relationships, you can configure cascade behaviors separately for each. For example, you might set Account's cascade delete behavior to Restrict (don't delete the service request if the account is deleted) while setting Contact to RemoveLink (clear the field if the contact is deleted). Navigate to each relationship individually to configure this.
Once the column exists, adding it to a form is straightforward — but the form behavior has some nuances worth knowing.
Open your Service Request main form in the form designer. Drag the Customer column (Requested By) onto the form canvas. When users interact with this field at runtime, the lookup control shows a type selector — they can choose to search for an Account or a Contact before they start typing. The type selector appears as a small icon or toggle within the lookup field.
The search behavior respects the lookup views of each target table. When a user selects "Account" and starts typing, the lookup control runs a search against the Account Quick Find view. Same for Contact. This means your Quick Find view configuration on each table matters — if you haven't customized the Account lookup view to include relevant columns, users may find the search results unhelpful. See Configuring Model-Driven App Views as Default Views, Quick Find Views, and Lookup Views: Controlling How Records Are Surfaced Across the App for how to tune this.
Warning
The Customer column's type-switcher in the lookup control is only available in the full Unified Interface form renderer. Older classic forms and some third-party integrations may not render it correctly, or may show separate Account and Contact fields instead. If you're building new apps, you're on Unified Interface, but be aware of this if you ever inherit legacy customizations.
A common UX pattern with Customer columns is to show information about the selected customer — their phone number, city, or account tier — directly on the Service Request form without the user having to navigate away.
You do this with a Quick View Form. Create a Quick View form on both the Account table and the Contact table that surfaces the fields you want to show. Then on your Service Request main form, add a Quick View control. When you configure it, you'll be asked which relationship to use. You'll need to add two Quick View controls — one targeting the Account relationship and one targeting the Contact relationship. Each will only display when the corresponding type is selected in the Customer column.
This is genuinely useful, but it does create a minor layout challenge: you'll have two Quick View panels that are mutually exclusive at runtime, but the form designer doesn't natively hide/show them based on the Customer column type. You can hide the inactive one using a business rule that shows/hides the appropriate Quick View section based on which type is currently selected.
The Regarding column (regardingobjectid) is the most powerful and the most constrained polymorphic lookup in Dataverse. It's built into the Activity table hierarchy and allows any activity record to be linked to a wide variety of tables.
You don't configure the Regarding column from scratch — it's part of the activity infrastructure. What you do control is which of your custom tables appear in the Regarding lookup's type selector. This is called enabling activities on a table.
To allow your Service Request table to appear in the Regarding lookup of activities (so you can log a phone call or task "regarding" a service request), open your Service Request table settings and turn on the Activities option under the Communications & Collaboration section.
Once enabled, users can navigate to the Activity subgrid on a Service Request form, create a new Task or Phone Call, and the Regarding field will auto-populate with the current Service Request. The relationship is managed by Dataverse's activity pointer infrastructure.
Note
Enabling activities on a table is a one-way operation — you cannot disable it after the fact without deleting the table. Make this decision deliberately as part of your data model design, not as a quick experiment.
When you enable activities, Dataverse adds the table to the list of valid targets for regardingobjectid. Behind the scenes, this registers the table's object type code in the activity pointer's allowed type list. This is also why you'll see your custom table appear as an option in the Regarding field when creating activities from the global activity creation dialog.
With activities enabled, add an Activity subgrid to your Service Request main form. In the subgrid configuration, set the table to Activities and filter by the regarding relationship. This gives users an embedded timeline of all related activities directly on the record.
The Activities Timeline control (distinct from a raw subgrid) is even more powerful — it shows a chronological feed of emails, tasks, appointments, and notes. Add it via the Timeline control from the form component library in the modern form designer.
Beyond Customer and Regarding, Dataverse supports fully custom polymorphic lookups through the Lookup (multi-table) column type. This feature allows you to specify two or more target tables when defining a lookup, creating a genuinely flexible relationship.
A realistic scenario: you're building an Expense Report table. An expense line item needs to be associated with either a Project or a Department — the accounting team routes expenses differently depending on which it is. You can't use Customer (wrong targets), and Regarding is overkill here. A custom multi-table lookup is the right tool.
In the Power Apps maker portal, navigate to your Expense Line Item table. Add a new column, and select Lookup as the type. You'll see a "Related table" field — start by selecting your first target table, say Project. Save and close the column creation dialog.
Now here's the key step that most people miss: you configure the multi-table nature of the lookup through additional relationships, not from the column creation dialog itself. After creating the initial lookup column targeting Project, you need to add a second relationship that targets Department and uses the same lookup column.
Go to the Relationships tab of your Expense Line Item table. Select Add relationship, choose Many-to-One, and set the Related table to Department. In the Lookup column field, instead of letting Dataverse create a new column, select your existing lookup column (the one you created for Project). This is the mechanism that makes it polymorphic — two relationships, one column.
Warning
This approach is only available in certain regions and tenants that have enabled the multi-table lookup feature, which is still rolling out as of this writing. If you don't see the option to select an existing lookup column when creating a second relationship, your environment may not have this capability yet. Check the Power Apps release notes for your region's availability.
Once configured, the column on your form will show a type switcher (Project or Department) just like the Customer column does, with searching scoped to the selected type.
When you retrieve a record with a polymorphic lookup via the Dataverse Web API, the response includes the type information alongside the GUID. For example, fetching an Expense Line Item might return:
{
"cr4f2_expenselineitemiid": "a1b2c3d4-...",
"cr4f2_costcenterid": "f9e8d7c6-...",
"_cr4f2_costcenterid_value": "f9e8d7c6-...",
"_cr4f2_costcenterid_value@Microsoft.Dynamics.CRM.lookuplogicalname": "cr4f2_project",
"_cr4f2_costcenterid_value@OData.Community.Display.V1.FormattedValue": "Q4 Infrastructure Upgrade"
}
The @Microsoft.Dynamics.CRM.lookuplogicalname annotation tells you which table the GUID resolves to. This is the critical piece for Power Automate flows, plugins, or any integration that needs to branch logic based on the lookup type. In Power Automate, you'd read this annotation and use a Condition or Switch action to route accordingly.
In Formula columns, you can reference the display value of a polymorphic lookup, but you can't directly branch on the type code within the formula language today. If you need type-conditional calculation, a real-time workflow or plugin is more appropriate.
Business rules in Dataverse work well with polymorphic lookups, but you need to understand their limitations with these column types.
You can set a business rule that makes the Customer column required, shows a notification when it's empty, or locks the field based on other conditions. These work exactly as they do with standard lookups — the business rule engine treats the Customer column as a single field regardless of which type is selected.
Here's a practical example: on your Service Request form, if the Status Reason is set to "Escalated," you want to ensure the Customer column is populated. Create a business rule with:
This works cleanly. What doesn't work is trying to write a rule that behaves differently based on which type is selected in the Customer column — the business rule engine can't inspect the object type code of a polymorphic lookup.
Key insight
If you need logic that differs based on whether the customer is an Account versus a Contact — for example, showing an "Account Manager" field only when Account is selected — you need JavaScript (a form event handler), not a business rule. Business rules treat the polymorphic lookup as a black box. See how column-level visibility and locking work in Configuring Dataverse Column-Level Business Rules and Multi-Condition Logic for context on what business rules can do.
If you do need type-aware behavior, a JavaScript form library handles it. On the form's OnLoad and on the Customer column's OnChange event, you register a handler that reads the lookup type and adjusts the form accordingly. Here's a skeleton:
function onRequestedByChange(executionContext) {
const formContext = executionContext.getFormContext();
const requestedByAttr = formContext.getAttribute("cr4f2_requestedby");
if (!requestedByAttr || !requestedByAttr.getValue()) {
// No value selected yet — hide both type-specific sections
formContext.ui.tabs.get("tab_details")
.sections.get("section_account_info").setVisible(false);
formContext.ui.tabs.get("tab_details")
.sections.get("section_contact_info").setVisible(false);
return;
}
const lookupValue = requestedByAttr.getValue()[0];
const entityType = lookupValue.entityType; // "account" or "contact"
formContext.ui.tabs.get("tab_details")
.sections.get("section_account_info")
.setVisible(entityType === "account");
formContext.ui.tabs.get("tab_details")
.sections.get("section_contact_info")
.setVisible(entityType === "contact");
}
Register this function on both the form's OnLoad event and the Customer column's OnChange event in the form properties dialog (Events tab).
The key line is lookupValue.entityType — the Xrm Client API surfaces the entity type as a property of the lookup value object. This gives you the string "account" or "contact" (always lowercase logical name), which you can use to drive any conditional UI behavior you need.
Views are where polymorphic lookups get genuinely tricky. When you add a standard lookup to a view, you can also add columns from the related table — clicking through the relationship to pull in, say, Account Name and Account Phone. This is called a joined column in a view.
With polymorphic lookups, that joined column expansion doesn't work the same way, because the "related table" isn't fixed. A single row might resolve to Account and the next might resolve to Contact — so Dataverse can't join a single consistent set of columns at query time.
You can add the polymorphic lookup column itself to a view. This will display the display name of the related record (e.g., "Contoso Ltd" or "Jane Smith") with no indication of which type it is. That's often enough for a main grid view.
What you cannot do natively is add a column from the related table — you can't say "show me the Phone column from whichever Account or Contact this resolves to." The Dataverse view query engine (which uses FetchXML under the hood) requires a fixed join target.
Tip
One practical workaround is to create a Formula column on your table that copies the phone number or other key field at write time — using a Power Automate flow or real-time workflow triggered on create/update. Store it in a plain text column on the Service Request. You lose live-sync but gain the ability to show it in any view. For simple display purposes, this tradeoff is often worthwhile.
If you need rich cross-type filtering and sorting in views, consider whether the polymorphic model is the right fit. Sometimes two separate optional lookup columns with clear labels ("Account Customer" and "Contact Customer") serve the view-browsing use case better, even if they're less elegant on the form. This is a genuine architectural tradeoff, not a cop-out.
When a user opens the polymorphic lookup field on a form and selects a type (say, Account), the search dialog uses the Lookup View of the Account table. Make sure your Account and Contact lookup views include the columns users need to identify the right record — typically Name, City or State, and for Contact, their parent Account name.
Configuring these is covered in depth in Configuring Model-Driven App Views as Default Views, Quick Find Views, and Lookup Views. Spend time here — a well-configured lookup view reduces data entry errors significantly.
Let's build a realistic end-to-end configuration. You'll create a Service Request table for a field services company, configure a Customer column, build a main form with a Quick View panel, and set up a useful view.
In your development solution, create a new table called Service Request with the following columns:
| Display Name | Type | Details |
|---|---|---|
| Service Request Title | Text (Primary) | Required |
| Requested By | Customer | Required |
| Request Type | Choice | Options: Installation, Repair, Maintenance, Inspection |
| Priority | Choice | Options: Low, Medium, High, Critical |
| Scheduled Date | Date Only | |
| Notes | Multiline Text | |
| Status | Status (system) | Active / Inactive |
Add the Requested By column last — use the Customer type as described earlier.
On the Account table, create a new Quick View form called "Customer Summary – Account" with these fields:
On the Contact table, create "Customer Summary – Contact" with:
Open the Service Request main form in the form designer. Lay it out with two tabs:
Tab 1: Request Details
Tab 2: Scheduling & History
Create a business rule on the Service Request table scoped to the main form only:
Since business rules can't inspect the lookup type directly, you'll implement this as a JavaScript handler instead (using the skeleton from the earlier section). Register it on the form's OnLoad and on the Requested By column's OnChange event.
Edit the Active Service Requests view to include:
Sort by Priority (descending) as the default, then Scheduled Date (ascending) as the secondary sort. This gives dispatchers a clear working list.
Enable the view in your model-driven app's site map. If you haven't configured your site map yet, Building Your First Model-Driven App: Site Map, Tables, Forms, and Views walks through this.
You create a Customer column and then try to add "Account > Account Rating" to your view. The column picker shows Account and Contact as expandable relationships, and you happily add the Account Rating column. It works — but only for rows where the Customer is an Account. Rows where the Customer is a Contact show blank for Account Rating. This surprises people.
This isn't a bug. It's the expected behavior of a left outer join on a specific relationship. The view query joins to Account for Account rows, but Contact rows have no Account join, so Account-sourced columns are null. If this matters for your view, use the formula column workaround described earlier.
Because a Customer column creates two relationships, you might configure cascade behaviors on one and forget the other. Users then notice that deleting an Account cascades correctly, but deleting a Contact leaves orphaned Service Request records pointing at a deleted Contact (with a broken lookup display).
Fix: After adding a Customer column, immediately go to the Relationships tab and explicitly configure the cascade behavior for both the Account relationship and the Contact relationship. Don't assume they inherit each other's settings — they're independent.
You want a view called "Account Customers Only" that shows only Service Requests where the Customer is an Account. Naturally, you try to add a filter on the Requested By column. But the standard view filter UI doesn't expose the object type code — you can't filter on "Requested By type equals Account."
Workaround: Add a separate Choice column called "Customer Type" (Account / Contact) and populate it via a real-time workflow or Power Automate flow whenever the Requested By column changes. Now you can filter views on this column. Yes, it's denormalization, but it's practical and it's a common pattern in Dataverse production apps.
Users complain that when they click into the Customer lookup and search for a contact by company, they can't find them. The Quick Find view for Contact doesn't include the parent Account name in its searchable columns.
Fix: Edit the Contact Quick Find view to include parentcustomerid (Account Name) in the Quick Find columns list. This is a two-minute fix that dramatically improves the data entry experience.
A Customer column means two relationships, which means users need read access to both Account and Contact tables to successfully use the lookup picker. If your security role grants read access to Account but not Contact, users can't find Contacts in the lookup — they just see no results when they switch to the Contact type selector.
Fix: Audit your security roles. When you add a Customer column, check that every role that can create or edit the parent table has at minimum Organization-level Read on both Account and Contact. See Model-Driven App Security: Configuring Security Roles, Field Permissions, and Team-Based Access for Table Data for how to review and configure table permissions.
If you import records via Excel or Dataflows and the Customer column shows blank even though you provided values, the problem is almost always the import format. Polymorphic lookups require both the GUID and the object type code in the import payload. Excel import doesn't handle this well for Customer columns — it expects a flat lookup GUID.
Fix: For importing data with Customer column values, use the Dataverse Web API or Dataflows with explicit type binding. In a Dataflow, map the customerid column and the customeridtype column separately (values: 1 for Account, 2 for Contact). This is documented in the Dataverse entity metadata, but the import UI doesn't surface it clearly.
Polymorphic lookups are one of Dataverse's most powerful — and most underused — modeling features. The Customer column gives you a clean, system-managed solution for Account/Contact duality that shows up constantly in sales, service, and operational apps. The Regarding column integrates your custom tables into Dataverse's activity infrastructure with minimal configuration. And custom multi-table lookups give you the same flexibility for any pair of tables you define.
The trade-offs are real: view joins don't work the way standard lookups do, business rules can't inspect the type, and security configuration needs to cover both target tables. But when the alternative is two separate optional lookup columns with no enforcement of mutual exclusivity, polymorphic lookups are almost always the better model.
Here's what to do next:
The goal isn't to use polymorphic lookups everywhere — it's to recognize the exact scenarios where they eliminate complexity, and then implement them with confidence.