Most Dataverse makers know standard lookups cold — but polymorphic lookups, the kind that can point to Account *or* Contact *or* a custom table depending on the record, are where data models get genuinely interesting. This lesson teaches you to configure Customer columns, Regarding relationships, and custom multi-table lookups from scratch, and query them correctly in OData, FetchXML, and Power Fx.

Picture this: you're building a case management system where support tickets can be filed by either a Contact or an Account. Or maybe you're designing an activity tracking system where a note can be attached to an Opportunity, a Lead, a Case, or a custom Project table — and you won't know which one until runtime. A standard lookup column won't cut it here. A standard lookup points at exactly one table. What you need is a polymorphic lookup: a single column that can reference records from multiple different tables.
Dataverse has supported polymorphic lookups for years in its own system tables — the "Regarding" column on Activities is the most famous example, and the "Customer" column on Cases and Invoices is another. But many makers treat these as black-box magic and never learn to configure them deliberately for their own custom tables. That's a missed opportunity, because polymorphic lookups are one of the most powerful modeling tools in the Dataverse toolkit, and they unlock relationship patterns that simply can't be expressed any other way.
By the end of this lesson, you'll understand exactly how polymorphic lookups work under the hood, how to configure Customer-type and Regarding-type columns on custom tables, how to surface them correctly on model-driven forms and views, and how to query them in Power Fx and the Dataverse API. You'll also build a realistic hands-on exercise that ties everything together.
What you'll learn:
You should be comfortable with standard Dataverse relationships and lookup columns before diving in. If you need a refresher, Designing a Dataverse Data Model: Relationships, Lookups, and Choice Columns covers that ground thoroughly. You should also have hands-on experience building model-driven app forms — Designing Model-Driven Forms: Sections, Tabs, Subgrids, and Quick View Forms is the right companion article there.
You'll need a Power Apps environment with Dataverse, a System Customizer or System Administrator security role, and access to the Power Apps maker portal at make.powerapps.com.
Before you configure anything, you need to understand the data model. This is one of those areas where understanding the internals will save you hours of confusion later.
A standard lookup column stores a single GUID — the ID of the related record — plus a hidden companion column that stores the related record's table name (the logical name). You might see this as _new_accountid_value in the OData API, and the companion field _new_accountid_value@Microsoft.Dynamics.CRM.lookuplogicalname tells you it points to the account table.
A polymorphic lookup works the same way mechanically, but the table name stored in that companion field can vary. For a given row, the lookup might point to an account record. For another row in the same table, it might point to a contact record. The GUID column and the table-name companion column are identical in structure — the difference is that Dataverse permits multiple target tables to be registered against a single logical lookup.
Dataverse implements this through what it calls connections between the lookup column and each eligible target table. Each connection creates a separate N:1 relationship definition. So a polymorphic lookup with two target tables actually has two relationship records backing it, both pointing at the same physical column.
Key insight
When you look at a polymorphic lookup in the metadata API, you'll see multiple relationship definitions with the same ReferencingAttribute (the column name) but different ReferencedEntity values. The column itself is singular; the routing logic is metadata.
This has a practical implication: you can add new target tables to an existing polymorphic lookup column after the fact, by creating an additional N:1 relationship that reuses the same attribute name. You don't need to recreate the column. This is how Microsoft has extended the Regarding column over time to support more and more entity types.
Dataverse gives you three distinct scenarios, and they're not interchangeable. Understanding which one applies to your use case is the most important decision you'll make.
The Customer column type is a built-in polymorphic lookup that targets exactly two tables: Account and Contact. It appears on the Case table out of the box (customerid), and you can add it to your own custom tables.
This is the right choice when your data model has a "this record belongs to either an individual or an organization" pattern — which shows up constantly in CRM scenarios. Think of it as Dataverse's first-class support for the Account/Contact duality.
Activities in Dataverse (Phone Calls, Tasks, Emails, Appointments, and custom activities) have a regardingobjectid column that can point to almost any table in the system. When you mark a table as an activity, Dataverse wires up this polymorphic regarding relationship automatically.
You can also add a "Regarding" relationship manually between any table and multiple target tables, giving non-activity tables that same "this is regarding..." semantic. This is useful for Notes, Documents, or any entity that serves as metadata attached to other records.
Starting with the modern Power Apps solution editor (and fully supported via the API and XrmToolBox), you can create a standard lookup column and then add additional target tables to make it polymorphic. This is the most flexible option and the one most makers don't know exists.
Note
Custom polymorphic lookups are a relatively newer capability in the maker portal. If you're using an older Power Apps interface and can't find the option, make sure you're working in the modern solution editor at make.powerapps.com, not the classic interface at make.preview.powerapps.com or the legacy customizations area.
Let's start with the Customer column because it's the most common real-world scenario.
Imagine you're building a Complaint table for a customer service application. A complaint can come from a Contact (an individual person) or an Account (a company filing on behalf of their organization). The Customer column is purpose-built for this.
Navigate to make.powerapps.com, open your solution, and open your Complaint table. Go to the Columns area and click New column.
Set the following properties:
new_customerid or similar)The Customer data type is distinct from a standard Lookup. It appears in the data type dropdown specifically as "Customer" — not "Lookup to Account" or "Lookup to Contact." When you select it, Dataverse knows to create the polymorphic relationship targeting both Account and Contact simultaneously.
Save the column. Behind the scenes, Dataverse creates two relationship definitions:
new_complaint_account_new_customerid (Account → Complaint)new_complaint_contact_new_customerid (Contact → Complaint)Both use the same physical column new_customerid.
Open your Complaint form in the form editor. Drag the Customer column from the columns panel onto the form canvas.
When you publish and test the form, the Customer field renders as a lookup with an extra element: a dropdown selector that lets the user choose whether they're looking up an Account or a Contact before they type the search value. This is the signature UX of a polymorphic lookup — the user selects the entity type first, then searches within that type.
Tip
The type selector on a Customer column defaults to "Account" for new records. If your users more commonly link complaints to Contacts, you can set a default view filter or guide users with a business rule that pre-populates the field based on context. Unfortunately, you can't change the default type selection itself through configuration alone — but a Canvas custom page or JavaScript can handle it if this is a real usability pain point.
The tricky part with polymorphic lookups in views is that you can't simply add a column like "Customer.Full Name" to a view the way you can with a standard lookup. The related table is ambiguous.
Instead, Dataverse surfaces the polymorphic lookup's value using the primary name column of whichever target record is stored. You can add the Customer column directly to a view — it will display the referenced record's name regardless of whether it's an Account or a Contact.
Navigate to the Views area, open your Active Complaints view, and add the Customer column. It will display the name of the linked Account or Contact correctly. What you can't do in a native view is also display Account-specific fields (like Industry) or Contact-specific fields (like Job Title) in the same column — for that, you'd need a Formula column that branches based on the lookup type, which we'll cover shortly.
Now for the more advanced scenario: building a polymorphic lookup from scratch that targets tables other than Account and Contact.
Consider a Document table that can be attached to any of the following: an Opportunity, a Project (custom table), or a Contract. You need a single "Relates To" lookup column that can point to any of these three.
In your Document table, create a new column:
Save this as new_relatestoid. This creates a standard N:1 relationship between Document and Opportunity.
This is the step most makers miss. You're going to create additional N:1 relationships from Document to Project and Contract, but you'll configure them to reuse the same column (new_relatestoid) rather than creating new columns.
This can be done through the Power Apps solution editor by navigating to the Relationships area of your Document table, clicking New relationship → Many-to-one, and in the relationship editor, expanding the Advanced options section. Look for the Lookup field option — here you can select an existing lookup column instead of creating a new one.
Set:
new_relatestoidRepeat for Contract.
Warning
Not all versions of the Power Apps maker portal expose this "use existing lookup field" option clearly. If you don't see it, you may need to use the XrmToolBox Polymorphic Lookup Creator plugin, or define the relationships via the Dataverse API directly. The underlying platform fully supports it; the maker portal UI is catching up.
After adding all three target relationships, navigate back to your Document table's Relationships area. You should see three N:1 relationships all showing new_relatestoid as the referencing attribute but different referenced tables: opportunity, new_project, and contract.
On any Document record's form, the Relates To field will now show the entity type selector offering "Opportunity," "Project," and "Contract" as choices.
You can restrict which target tables appear in the lookup dropdown on a specific form by configuring the lookup column's Related record filter in the form editor. Select the Relates To field on the form, go to its properties, and look for the lookup filtering options. You can hard-filter to only show specific entity types, which is useful when a form is contextually always going to relate to one specific table (like a Document form accessed from within a Project).
Views become interesting when you need to display type-specific information alongside the polymorphic lookup. This is where Formula columns and Rollup Columns in Dataverse become genuinely powerful.
Dataverse formula columns support a function called IsType() and AsType() that let you branch based on the actual type stored in a polymorphic lookup.
Here's a realistic example. You want a formula column on your Complaint table called Customer Type that returns "Account" or "Contact" depending on what's stored in the Customer field:
If(
IsType(ThisRecord.Customer, Accounts),
"Account",
If(
IsType(ThisRecord.Customer, Contacts),
"Contact",
"Unknown"
)
)
And here's a more useful one — a formula column called Customer Detail that returns the Account's Primary Phone if the customer is an Account, or the Contact's email if it's a Contact:
If(
IsType(ThisRecord.Customer, Accounts),
AsType(ThisRecord.Customer, Accounts).'Main Phone',
If(
IsType(ThisRecord.Customer, Contacts),
AsType(ThisRecord.Customer, Contacts).'Email',
""
)
)
These formula columns can then be added to views, giving you type-specific data in a single column even though the source is polymorphic. This is one of the more elegant patterns in the Dataverse data model.
Tip
AsType() will throw an error if the record isn't actually of the specified type, which is why you always wrap it in IsType() first. Think of IsType() as your type guard — always check before you cast.
The form experience for polymorphic lookups requires some deliberate configuration. Let's walk through the key considerations.
A common pattern with standard lookups is to add a Quick View Form that displays fields from the related record inline on the parent form. With a polymorphic lookup, this gets complicated because the related record could be one of several types, each with its own form.
The solution is to create separate quick view forms for each possible target table, and then use visibility rules (via JavaScript or business rules) to show the appropriate one based on the current lookup type.
Alternatively, if you're using a Customer column specifically, you can add two quick view sections:
In the form editor, you can configure each section's visibility via a Business Rule that checks the Customer column type. Business rules can read the polymorphic lookup's value, and while they can't natively check the type of the lookup (Account vs. Contact) without JavaScript, they can check whether the field is null and respond accordingly.
Note
For type-based visibility on quick view sections, you'll typically need a lightweight JavaScript form event handler on the OnChange and OnLoad events of the polymorphic lookup column. This is one area where the low-code/no-code toolbox falls just short, and a few lines of JavaScript bridge the gap cleanly.
When a user opens a polymorphic lookup to search for a record, they see a lookup dialog. You can configure which view appears in that dialog for each target table. Navigate to the lookup column's properties in the form editor and look for Views for each related table. You can specify a custom view (like "Active Accounts with Open Contracts") that filters the lookup results for better usability.
This is especially important in large environments where unfiltered lookup dialogs would surface thousands of records. Well-configured lookup views are one of the most impactful usability improvements you can make to a model-driven app — and you can read more about the mechanics in Configuring Model-Driven App Views as Default Views, Quick Find Views, and Lookup Views.
Once your data is configured correctly, you need to know how to query it — especially for Power Automate flows, custom API calls, and advanced reporting.
When you retrieve a record with a polymorphic lookup via the Web API, the response includes both the GUID and the type:
GET /api/data/v9.2/new_complaints?$select=new_complaintid,_new_customerid_value
Response:
{
"new_complaintid": "a1b2c3d4-...",
"_new_customerid_value": "f5e6d7c8-...",
"_new_customerid_value@Microsoft.Dynamics.CRM.lookuplogicalname": "contact",
"_new_customerid_value@OData.Community.Display.V1.FormattedValue": "Jane Smith"
}
The @Microsoft.Dynamics.CRM.lookuplogicalname annotation tells you which table the GUID belongs to. Your consuming code — whether it's a Power Automate flow parsing a response or a custom API client — should always read this annotation before deciding how to use the GUID.
To expand a polymorphic lookup in a single OData query, you need to know which table you're expanding, because OData $expand requires a specific navigation property name. You can't expand a polymorphic lookup generically:
# Expand as Account
GET /api/data/v9.2/new_complaints?$expand=new_customerid_account($select=name,telephone1)
# Expand as Contact
GET /api/data/v9.2/new_complaints?$expand=new_customerid_contact($select=fullname,emailaddress1)
You'd typically include both expansions and check which one returned data.
FetchXML handles polymorphic lookups through its link-entity syntax. The key is that you need separate link-entity elements for each possible target:
<fetch>
<entity name="new_complaint">
<attribute name="new_complaintid" />
<attribute name="new_customerid" />
<link-entity name="account"
from="accountid"
to="new_customerid"
link-type="outer"
alias="cust_account">
<attribute name="name" alias="account_name" />
<attribute name="telephone1" alias="account_phone" />
</link-entity>
<link-entity name="contact"
from="contactid"
to="new_customerid"
link-type="outer"
alias="cust_contact">
<attribute name="fullname" alias="contact_name" />
<attribute name="emailaddress1" alias="contact_email" />
</link-entity>
</entity>
</fetch>
Using link-type="outer" is critical here. If you used inner joins, a record linked to an Account would fail the Contact join and be excluded from results entirely. Outer joins let both nullable paths coexist.
Warning
FetchXML outer joins on polymorphic lookups can produce rows where one set of aliased columns is null and the other is populated. Your consuming code (a flow, a report, a plugin) needs to handle both cases gracefully. Always null-check before reading type-specific fields.
To retrieve only Complaints linked to Contacts (not Accounts), you filter on the lookup's companion attribute using a condition:
<fetch>
<entity name="new_complaint">
<attribute name="new_complaintid" />
<filter>
<condition attribute="new_customeridtype"
operator="eq"
value="2" />
</filter>
</entity>
</fetch>
The new_customeridtype column (the physical column name follows the pattern {lookupcolumn}type) stores an integer code representing the table: 1 for Account, 2 for Contact. These codes correspond to the Dataverse object type codes for each table. Custom tables get their own unique codes assigned at creation time.
You can look up object type codes in the metadata: query EntityDefinitions?$select=LogicalName,ObjectTypeCode from the Web API, or check the table's properties in the solution editor.
The Regarding column (regardingobjectid on the Activity family of tables) is the original polymorphic lookup in Dataverse, and it deserves its own discussion because it behaves somewhat differently from custom polymorphic lookups.
Any table can be made a valid target of the Regarding column. To do this, navigate to your custom table's settings and enable the Activities option under "Create a new activity." This doesn't make your table an activity — it makes your table a valid target for activities and notes.
Once enabled, when a user creates a Phone Call, Task, Email, or other activity and fills in the Regarding field, your custom table will appear as an option in the type selector.
Key insight
Enabling activities on a table is a one-way door. You cannot disable it later. Think carefully before enabling this on every custom table — in large environments with many custom tables, an overly broad Regarding type selector becomes noisy and confusing for users. Enable it only on tables where the activity association is genuinely meaningful.
Once activities are enabled on a table, you can add an Activities subgrid to the main form that shows all associated activities regardless of type. This is the typical "timeline-style" activity display. The subgrid uses the Regarding relationship automatically — you don't need to configure a separate relationship.
In the form editor, add a subgrid component and configure it to show "Activities" as the related table, filtered by the Regarding column. This gives you the familiar timeline view of calls, emails, and tasks associated with your record.
This exercise walks you through building a real-world document management scenario using a custom polymorphic lookup.
Scenario: You're building a Document Library for an internal operations team. Documents can be linked to any of three things: a Project (custom table), an Vendor (Account with category = Vendor), or an Internal Process (another custom table). You need a single "Linked To" field on the Document table.
First, create two custom tables if they don't exist:
These are simple tables — just primary name columns are fine for this exercise.
Create a Document table (new_document) with:
Navigate to the Relationships section of the Document table. Create a new Many-to-One relationship:
new_linkedtoidCreate another:
new_linkedtoidYou now have three relationships sharing the new_linkedtoid column.
On the Document table, add a new Formula column:
If(
IsType(ThisRecord.'Linked To', Projects),
"Project",
If(
IsType(ThisRecord.'Linked To', Accounts),
"Vendor",
If(
IsType(ThisRecord.'Linked To', 'Internal Processes'),
"Internal Process",
"Not Linked"
)
)
)
Open the Active Documents view and add:
Sort by Upload Date descending.
Add the Document table to your model-driven app's site map. Test by creating several documents: one linked to a Project, one to an Account, one to an Internal Process. Verify the view correctly shows the record name and the computed type label.
Tip
When testing polymorphic lookups, create at least one record of each possible linked type, plus one record with no linked record at all. This covers all the branches in your formula column and exposes any null-handling gaps early.
Customer columns, Regarding columns, and custom polymorphic lookups look similar in the UI but have different metadata structures and different query patterns. A query designed for the Customer column's customeridtype companion attribute won't work for a custom polymorphic column without checking the actual attribute name (new_linkedtoid → new_linkedtoidtype).
Fix: Always verify the companion type attribute name by querying EntityDefinitions(LogicalName='new_document')/Attributes and looking for the attribute of type EntityNameAttributeMetadata that corresponds to your lookup.
When filtering on the type companion column in FetchXML, you need the integer object type code, not the table logical name. Using "account" instead of 1 in a condition will return zero results with no error.
Fix: Query /api/data/v9.2/EntityDefinitions?$select=LogicalName,ObjectTypeCode to get the code for each table. Keep a reference list handy in your data dictionary.
Every table with activities enabled appears in the Regarding field's type selector. In a system with 30+ custom tables, this selector becomes a wall of options.
Fix: Only enable activities on tables where users will genuinely create activity records. For tables where you just want to log structured data, model that data directly on the table rather than relying on activities.
If you write a Power Automate flow that always expands new_linkedtoid_account, you'll get null for records linked to Projects or Internal Processes, and you won't know why.
Fix: In your flow, first read the _new_linkedtoidtype (or equivalent) value from the response body, then branch with a Switch action to handle each type appropriately before expanding or navigating to the related record.
When adding a subgrid on a form to show records related via a polymorphic lookup, you might accidentally configure the subgrid against only one of the N:1 relationships, missing records of other types.
Fix: For a polymorphic lookup subgrid going in the reverse direction (showing Documents from a Project form, for example), configure the subgrid's relationship filter explicitly. Navigate to the subgrid's properties and confirm you're using the correct relationship name — the one that corresponds to the Project target.
If your polymorphic lookup renders as a plain lookup (no type selector dropdown) in the form, it usually means only one target relationship is registered. The type selector only appears when there are two or more valid target tables.
Fix: Go to the Relationships area of your table and verify that multiple relationships share the same referencing attribute. If only one exists, create the additional relationships as described in the configuration steps above.
Polymorphic lookups interact with Dataverse security roles in ways that can surprise you. A user needs read access to the target table to have the lookup resolve and display correctly. If a user has read access to Complaints but not to Accounts, any Complaint whose Customer field points to an Account will show an empty or unresolvable lookup.
This creates a situation where the same form looks different to different users — some see the Customer name, others see a blank field — not because the data is wrong, but because security is filtering what's readable.
Warning
Polymorphic lookup fields can inadvertently reveal which table a related record belongs to even when the user can't read the related record. The type selector in the lookup UI exposes that "this is linked to an Account" even if the Account name is hidden. Design your security model with this in mind.
For environments with strict data partitioning, consider whether a polymorphic lookup is the right choice, or whether separate explicitly-typed lookup columns with field-level security give you more control. That tradeoff is discussed in depth in the column-level security and record sharing article.
Polymorphic lookups are slightly more expensive to query than standard lookups because the query planner can't assume a single index path. When you join across a polymorphic lookup in FetchXML using outer joins to both possible targets, you're generating a query that spans multiple tables.
For tables with high row counts (100K+ rows), consider:
Index the type companion column. The new_linkedtoidtype column is queryable, and filtering on it early in a FetchXML query lets the query planner reduce the result set before performing the outer joins.
Use linked-entity joins only when necessary. If you only need the name of the related record (not type-specific attributes), the polymorphic lookup column itself renders the name correctly without needing a join.
Materialize type labels in a formula column. Rather than computing IsType() in every view query, a stored formula column means the type label is pre-computed and indexed. This is much more performant for views with large result sets.
Polymorphic lookups unlock relationship patterns that standard lookups can't express: a single column that can reference records from multiple tables, resolved at runtime based on what's actually stored. You've now seen how all three variants work — Customer columns, Regarding relationships, and custom multi-table lookups — and how to configure, query, and surface them in model-driven apps.
The key principles to carry forward:
*type) is your routing signal. It's an integer object type code, not a string, and you'll need it in FetchXML filters and OData queries.IsType() / AsType() let you surface type-specific data in views without custom code.From here, strong next steps include deepening your understanding of how views control what users see in lookup dialogs — Configuring Model-Driven App Views as Default Views, Quick Find Views, and Lookup Views covers that in detail. And if you're building the kind of complex data model where polymorphic lookups live, it's worth reviewing how your solution is packaged and versioned — Solutions for Model-Driven Apps: Publishers, Managed vs Unmanaged, and Solution Layering will keep your deployment story clean as the complexity grows.
Model-Driven Apps & Dataverse