FlowLibs
Browse Cloud FlowsConnectorsAI ToolsDev ToolsPricingAboutContact
Dev Tools/Dataverse

Dataverse Developer Reference

A working reference for Microsoft Dataverse — the relational, secure data platform under Power Apps, Power Automate, and Dynamics 365. Tables and column types, relationships, the OData v4 Web API, OData query options, FetchXML, the plug-in event pipeline and SDK, and the security-role model (privileges, access levels, business units, teams).

Dataverse28 min read·Updated 2026-06-16
DataverseWeb APIODataFetchXMLPluginsSDKSecurity RolesBusiness UnitsRelationshipsColumnsIOrganizationService

On this page

  • Fundamentals
  • Column data types
  • Relationships
  • Web API basics
  • Querying with OData
  • FetchXML
  • Plug-ins & the SDK
  • Security roles & access
  • Web API patterns
  • Choosing an access path
  • Tip

Fundamentals

Dataverse is a managed, relational data platform — tables, typed columns, relationships, keys, rich metadata, and a role-based security model, all served over a uniform OData v4 Web API and a .NET SDK. It is the native store for model-driven apps and Dynamics 365, and the most scalable source for canvas apps and cloud flows.

  • Tables hold rows (records) made of typed columns. Every row has a GUID primary key (e.g. accountid) and a primary name column.
  • Table types: *Standard* (normal), *Activity* (time-bound, e.g. tasks/emails), *Virtual* (data sourced from an external system at runtime), and *Elastic* (NoSQL-backed, for very high volume / variable schema).
  • Ownership is chosen at table creation and is permanent: *User/Team-owned* rows participate in the full access-level hierarchy; *Organization-owned* rows are on/off only.
  • Alternate keys let you address a row by a business value (e.g. an order number) instead of its GUID — essential for upsert and integration.
9/9
Modern termClassic termNotes
TableEntitySchema/metadata still calls it EntityMetadata
RowRecordIdentified by a GUID primary key
ColumnField / AttributeAttributeMetadata in the SDK
ChoiceOption setLocal (one column) or global (reusable)
ChoicesMulti-select option setStored as a comma-separated set of values
Yes/NoTwo options / BooleanA two-option choice under the hood
LookupLookup / EntityReferenceA foreign key to another table
RelationshipRelationship1:N, N:1, or N:N
DataverseCommon Data Service (CDS) / XRMSame platform, renamed in 2020
Modern Dataverse terms and their classic CRM equivalents — you will still see the old names in APIs and docs.

Three names for every object

Each table and column has a display name (for humans, localizable), a schema name (PascalCase, carries the publisher prefix, e.g. contoso_ApprovalStatus), and a logical name (the lowercase schema name, e.g. contoso_approvalstatus) — the Web API and FetchXML use the logical name. The publisher customization prefix is baked in forever, so pick it before you create the first table.

Column data types

18/18
Column typeWeb API JSONNotes
Single line of textstringFormats: email, URL, phone, ticker, text
Multiple lines of textstringLong text; memo
Choice (option set)number (Edm.Int32)Integer value; label via FormattedValue annotation
Choices (multi-select)string of numbersComma-separated option values
Yes/No (Boolean)booleanA two-option choice
Whole NumbernumberEdm.Int32; also duration/timezone/language formats
DecimalnumberExact; up to 10 dp — use for precise values
Floating pointnumberApproximate — avoid for money
Currency (Money)numberAdds a _base companion + exchange rate
Date and timestring (ISO 8601)Behavior: User Local / Date Only / Time-Zone Independent
LookupGUID via @odata.bindRead back as _name_value with annotations
Customerpolymorphic lookupPoints to account OR contact
Ownerpolymorphic lookupPoints to systemuser OR team
Unique Identifierstring (GUID)Read-only GUID column
AutonumberstringServer-generated sequence (e.g. INV-0001)
File / ImagebinaryStreamed via dedicated endpoints
Formula / CalculatedvariesComputed on read; not writable
Rollupnumber/moneyAggregated async by a system job
Pick the narrowest type that fits — it drives validation, storage, and how the Web API serializes the value.

DateTime behavior is a one-way decision

A date column’s behavior — *User Local* (stored UTC, shown in the user’s zone), *Date Only*, or *Time-Zone Independent* — is set at creation and mostly cannot be changed later. Use *Date Only* for birthdays and *Time-Zone Independent* for values like a contract date that must not shift across zones.

Relationships

Relationships are first-class metadata, not just foreign keys. A 1:N relationship adds a lookup column on the “many” (child) table; an N:N relationship creates a hidden intersect table. In the Web API you traverse them as navigation properties with $expand.

7/7
TypeWhat it isExample
1:N (one-to-many)A parent row has many childrenAccount → Contacts
N:1 (many-to-one)The lookup column on the childContact → Account (parentcustomerid)
N:N (many-to-many)Two-way, via an intersect tableCourse ↔ Student
Customer (polymorphic)Lookup to account OR contactCase.customerid
Owner (polymorphic)Lookup to systemuser OR teamownerid on owned tables
Regarding (polymorphic)Activity → many possible tablesTask.regardingobjectid
ConnectionsAd-hoc, untyped link between any rowsLoose “related to” associations

Cascade behavior (1:N relationship behavior)

  • Parental — actions on the parent cascade to children (delete the parent, delete the children). Use sparingly; it is the strongest coupling.
  • Referential — children survive; the lookup is simply cleared or blocked. *Referential, Restrict Delete* blocks deleting a parent that still has children.
  • Configurable cascade — set each action independently: *Assign*, *Share/Unshare*, *Reparent*, *Delete*, and *Merge* can each be Cascade All / Active / User-owned / None.

Parental cascades multiply work

A parental relationship re-shares, re-assigns, and deletes down the whole tree in one transaction. On high-volume tables that can turn a single assign into thousands of row updates — prefer *Referential* unless the child genuinely cannot exist without its parent.

Web API basics

The Web API is an OData v4 service at https://<org>.crm.dynamics.com/api/data/v9.2/. Everything is JSON over HTTPS with an OAuth 2.0 bearer token (Microsoft Entra ID). You address a table by its entity set name — usually the logical name pluralized (account → accounts, contoso_widget → contoso_widgets).

7/7
MethodOperationExample path
GETRetrieve a row / collection/accounts(<id>) · /accounts?$top=10
POSTCreate a row/accounts
PATCHUpdate — or upsert by id/alternate key/accounts(<id>)
DELETEDelete a row/accounts(<id>)
POSTCall an unbound/bound action/WinOpportunity · /accounts(<id>)/Microsoft.Dynamics.CRM.Merge
GETCall a function/WhoAmI()
POSTBatch / changeset/$batch
11/11
HeaderValue / purposeWhen
AuthorizationBearer <token>Always (Entra OAuth)
OData-MaxVersion4.0Always
OData-Version4.0Always
Acceptapplication/jsonAlways
Content-Typeapplication/jsonPOST / PATCH
Prefer: return=representationEcho the created/updated row backPOST / PATCH when you need the result
Prefer: odata.include-annotations="*"Return formatted values + lookup namesGET when you need labels
Prefer: odata.maxpagesize=NPage size for large GETsPaged reads
If-Match: <etag> / "*"Optimistic concurrency / update-onlyPATCH / DELETE
If-None-Match: "*"Create-only (block update on upsert)PATCH upsert
MSCRMCallerID: <systemuserid>Impersonate another userDelegated writes
Headers that matter on almost every Web API call.
http
POST [Organization URI]/api/data/v9.2/accounts
OData-MaxVersion: 4.0
OData-Version: 4.0
Content-Type: application/json
Prefer: return=representation

{
  "name": "Contoso Ltd",
  "creditlimit": 50000,
  "primarycontactid@odata.bind": "/contacts(00000000-0000-0000-0000-000000000001)"
}
Create an account, bind a lookup, and get the row back in one round-trip.
http
PATCH [Organization URI]/api/data/v9.2/contoso_orders(contoso_ordernumber='SO-1042')
If-None-Match: "*"
Content-Type: application/json

{ "contoso_total": 1299.00, "contoso_status": 1 }
Upsert by alternate key — PATCH to the keyed URL; If-None-Match: "*" makes it insert-only.

Bind lookups, don’t set the GUID

Set a lookup by posting "<navprop>@odata.bind": "/<entityset>(<id>)", not by writing the raw _x_value column. To clear a lookup, DELETE the single-valued navigation property: DELETE .../accounts(<id>)/primarycontactid/$ref.

Querying with OData

7/7
OptionWhat it doesExample
$selectReturn only these columns (always do this)$select=name,revenue
$filterRow criteria$filter=revenue gt 100000
$orderbySort$orderby=revenue desc,name asc
$topTake the first N rows$top=10
$countInclude a total count$count=true
$expandPull related rows / lookups$expand=primarycontactid($select=fullname)
$applyAggregate / group (analytics)$apply=aggregate(revenue with sum as total)
7/7
Operator / functionMeaningExample
eq / neEqual / not equalstatecode eq 0
gt / ge / lt / leGreater / less (or equal)revenue ge 50000
and / or / notBoolean compositionrevenue gt 1000 and statecode eq 0
( )Precedence grouping(a eq 1 or a eq 2) and b eq 3
contains()Substring matchcontains(name,'Contoso')
startswith() / endswith()Prefix / suffix matchstartswith(name,'A')
nullIs / is not setprimarycontactid eq null
Standard OData operators and string functions supported by the Web API.
6/6
FunctionWhat it matchesExample
LastXDays / OlderThanXDaysRelative date windowsLastXDays(PropertyName='createdon',PropertyValue=7)
ThisMonth / Today / ThisFiscalYearCalendar windowsToday(PropertyName='createdon')
EqualUserId / EqualUserTeamsOwned by me / my teamsEqualUserId(PropertyName='ownerid')
Above / Under / AboveOrEqualHierarchy traversalUnder(PropertyName='accountid',PropertyValue='<id>')
In / NotInValue set membershipIn(PropertyName='statuscode',PropertyValues=['1','2'])
ContainValuesMulti-select choice containsContainValues(PropertyName='contoso_tags',PropertyValues=['1'])
Dataverse-specific query functions (namespace Microsoft.Dynamics.CRM) — power the same filters as FetchXML.
http
GET [Organization URI]/api/data/v9.2/accounts?
  $select=name,revenue,statuscode&
  $filter=revenue gt 100000 and statecode eq 0&
  $orderby=revenue desc&
  $expand=primarycontactid($select=fullname,emailaddress1)&
  $top=25
Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"
Filter + sort + expand a lookup, returning labels via the annotations preference.

Page with the server’s nextLink

Set Prefer: odata.maxpagesize=N and read up to N rows; if more exist, the response carries an @odata.nextLink URL — follow it for the next page rather than building your own $skip. Use $count=true for a total, and read choice/lookup labels from the ...@OData.Community.Display.V1.FormattedValue annotations.

FetchXML

FetchXML is Dataverse’s XML query language. It backs saved views and the SDK’s RetrieveMultiple, and it does a few things OData does more awkwardly — aggregates/grouping, deep link-entity joins, and distinct. Many makers author it in the FetchXML Builder (XrmToolBox) and paste the result.

xml
<fetch top="50">
  <entity name="account">
    <attribute name="name" />
    <attribute name="revenue" />
    <filter type="and">
      <condition attribute="statecode" operator="eq" value="0" />
      <condition attribute="revenue" operator="gt" value="100000" />
    </filter>
    <link-entity name="contact" from="contactid" to="primarycontactid" alias="pc">
      <attribute name="fullname" />
    </link-entity>
    <order attribute="revenue" descending="true" />
  </entity>
</fetch>
Join (link-entity), filter, and sort — the FetchXML equivalent of a $filter + $expand query.
8/8
OperatorMatchesNotes
eq / neEqual / not equalvalue attribute holds the literal
like / not-likeSQL wildcard (%)value="%Contoso%"
in / not-inValue setNested <value> elements
null / not-nullIs / is not setNo value needed
on / on-or-after / on-or-beforeDate comparisonsFor date columns
last-x-days / next-x-daysRelative date windowvalue = number of days
eq-userid / ne-useridCurrent userNo value — uses caller
under / eq-or-under / aboveHierarchy traversalHierarchical relationships
A selection of FetchXML condition operators — many map to the Dataverse query functions above.
xml
<fetch aggregate="true">
  <entity name="opportunity">
    <attribute name="estimatedvalue" alias="total" aggregate="sum" />
    <attribute name="opportunityid" alias="cnt" aggregate="count" />
    <attribute name="ownerid" alias="owner" groupby="true" />
    <filter>
      <condition attribute="statecode" operator="eq" value="0" />
    </filter>
  </entity>
</fetch>
Aggregate: total and count of open opportunities grouped by owner.

FetchXML ⇄ OData, at a glance

<entity name> → the entity set · <attribute> → $select · <filter>/<condition> → $filter · <link-entity> → $expand (or a nested filter) · <order> → $orderby. Reach for FetchXML when you need aggregate/groupby or distinct; reach for OData for everyday CRUD and when you want a plain REST URL. You can also run FetchXML *through* the Web API via ?fetchXml=<url-encoded>.

Plug-ins & the SDK

A plug-in is a sandboxed .NET class implementing `IPlugin` that the platform runs when a message (Create, Update, Delete, a custom API…) fires against a table. You register it on a stage of the event pipeline with the Plugin Registration Tool or pac plugin. Inside, you reach the platform through the IServiceProvider.

4/4
StageValueIn transaction?Use it for
PreValidation10No (before tx, before security)Cancel early; cross-row validation
PreOperation20YesChange values on the inbound row
MainOperation30YesPlatform only (and custom APIs)
PostOperation40YesReact after the write; async work
The event execution pipeline. Register synchronous logic on 10/20/40; 30 is the platform’s own work.
9/9
MemberWhat it gives youNotes
InputParameters["Target"]The inbound Entity (or EntityReference)Entity on Create/Update; ref on Delete
PreEntityImagesSnapshot before the writeRegister the image to use it
PostEntityImagesSnapshot after the writePostOperation only
OutputParameters["id"]New row id on CreatePostOperation
MessageName / PrimaryEntityNameWhich message / table firedBranch on these
DepthRe-entrancy counterGuard against infinite loops
UserId / InitiatingUserIdEffective vs calling userUse for the service context
IOrganizationServiceFactoryCreates IOrganizationServicePass UserId or null (SYSTEM)
ITracingServiceTrace() for the plug-in trace logYour only real debugger in prod
What you read from IPluginExecutionContext / IServiceProvider.
csharp
public class AccountPreOperation : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider
            .GetService(typeof(IPluginExecutionContext));
        var tracing = (ITracingService)serviceProvider
            .GetService(typeof(ITracingService));
        var factory = (IOrganizationServiceFactory)serviceProvider
            .GetService(typeof(IOrganizationServiceFactory));
        var service = factory.CreateOrganizationService(context.UserId);

        // Guard against re-entrancy (our own Update re-triggering us).
        if (context.Depth > 1) return;

        if (context.InputParameters.TryGetValue("Target", out var t) && t is Entity account)
        {
            tracing.Trace("Validating account " + account.Id);
            var name = account.GetAttributeValue<string>("name");
            if (string.IsNullOrWhiteSpace(name))
                throw new InvalidPluginExecutionException("Account name is required.");

            // PreOperation: set values on the inbound row — no extra Update needed.
            account["contoso_normalizedname"] = name.Trim().ToLowerInvariant();
        }
    }
}
A minimal, well-behaved plug-in skeleton.
  • Sync vs async: synchronous steps run inline (10/20/40) and block the caller; asynchronous steps run after commit via the system-job queue and may only register on PostOperation (40).
  • Cancel an operation by throwing InvalidPluginExecutionException — ideally in PreValidation, before the transaction and security checks.
  • Images beat re-reading: register a Pre/Post image to get old/new values instead of calling Retrieve inside the plug-in.
  • Sandbox isolation: no file system or registry, outbound HTTP/HTTPS only, and a ~2-minute execution limit — keep plug-ins fast and side-effect-light.

Always check Depth

A plug-in that updates its own table on Update will re-trigger itself. Guard with if (context.Depth > 1) return; (or scope your step’s filtering attributes) so a routine save does not recurse into a loop and trip the depth limit.

Security roles & access

Dataverse uses role-based access control. A security role grants a privilege (an action) at an access level (how far it reaches) per table — together those are the user’s *access rights*. A user can hold several roles, and access is the least-restrictive union of them all — you cannot subtract access with another role.

8/8
PrivilegeAllowsNotes
CreateMake a new row
ReadOpen and view a rowThe gate for everything else
WriteChange a row
DeletePermanently remove a row
AppendAttach this row to anothere.g. add a note to a case
AppendToLet others attach to this rowN:N needs Append on both tables
AssignGive ownership to another user
ShareGrant access while keeping yoursAd-hoc, per-row grant
5/5
Access levelApp labelReach
NoneNoneNo access
BasicUserRows you own, are shared, or your team owns
LocalBusiness UnitAll rows in your business unit
DeepParent: Child Business UnitsYour BU and every BU beneath it
GlobalOrganizationEvery row in the environment
Access levels for user/team-owned tables. Org-owned tables are Global or None only.
  • Business units form the hierarchy that Local/Deep/Global are measured against; every user and team belongs to exactly one.
  • Teams carry roles too: *Owner* teams can own rows; *Access* teams grant per-row access at scale; Microsoft Entra group teams sync membership from a security group — the cleanest way to manage access.
  • Field-level security secures individual columns (e.g. salary, national ID) via Field Security Profiles that grant Read / Update / Create on the secured column — independent of table privileges.
  • Hierarchy security layers Manager or Position hierarchies on top of roles so managers see their reports’ rows without a custom role.

Grant up, never patch down

Because roles are purely additive, the fix for “this user sees too much” is never to add a restrictive role — it is to lower the access level on the role that over-granted. Model from least privilege up. A handy check: WhoAmI plus the user’s roles tells you the effective business unit the access levels resolve against.

Web API patterns

Recipes that combine the pieces above — each is a copy-and-adapt request for real integration work.

Atomic multi-write with a $batch changeset

http
POST [Organization URI]/api/data/v9.2/$batch
Content-Type: multipart/mixed; boundary=batch_AAA

--batch_AAA
Content-Type: multipart/mixed; boundary=changeset_BBB

--changeset_BBB
Content-Type: application/http
Content-ID: 1

POST /api/data/v9.2/accounts HTTP/1.1
Content-Type: application/json

{ "name": "Contoso Ltd" }

--changeset_BBB
Content-Type: application/http
Content-ID: 2

POST /api/data/v9.2/contacts HTTP/1.1
Content-Type: application/json

{ "lastname": "Reed", "parentcustomerid_account@odata.bind": "$1" }
--changeset_BBB--
--batch_AAA--
Everything inside a changeset commits or rolls back together. Use $batch to stay under request limits too.

Optimistic concurrency (don’t clobber a newer write)

http
PATCH [Organization URI]/api/data/v9.2/accounts(<id>)
If-Match: W/"1234567"
Content-Type: application/json

{ "creditlimit": 75000 }
Send back the ETag you read; a 412 Precondition Failed means someone edited the row first.

Associate / disassociate an N:N

http
POST [Organization URI]/api/data/v9.2/accounts(<id>)/contoso_account_contact_set/$ref
Content-Type: application/json

{ "@odata.id": "[Organization URI]/api/data/v9.2/contacts(<contactid>)" }
Associate by POSTing a $ref to the collection navigation property; DELETE the same to disassociate.

Impersonate another user for a delegated write

http
POST [Organization URI]/api/data/v9.2/tasks
MSCRMCallerID: 7b1f8a90-0000-0000-0000-000000000abc
Content-Type: application/json

{ "subject": "Follow up", "regardingobjectid_account@odata.bind": "/accounts(<id>)" }
MSCRMCallerID makes the row owned/audited as that user — the caller still needs the privilege to impersonate.

Choosing an access path

PathBest forNotes
Web API (REST/OData)Cloud flows, JS, any language, integrationNo SDK needed; OAuth bearer token
SDK for .NET (IOrganizationService)Plug-ins, Azure Functions, console appsStrongly typed; Create/Update/Retrieve(Multiple)
FetchXMLAggregates, group-by, distinct, saved viewsRun via SDK or ?fetchXml= on the Web API
Dataverse connectorPower Automate / Power Apps, no codeList rows, filter rows (OData), bound actions
Four ways to talk to Dataverse — pick by where your code runs.

Same model, many doors

Logical names, choice values, relationships, and the security model are identical across all four paths — learn the metadata once and it carries from a canvas app filter to a plug-in to a $batch integration.

Tip

Every cheat table above is searchable

Use the filter box on each table to jump to a column type, operator, privilege, or pipeline stage fast. This whole guide is generated from one structured dataset, so it stays in sync with the MCP reference.

← All Dev ToolsUse these in your AI assistant →
Spotted an error or have a suggestion? Let us know
FlowLibs

A curated library of production-grade Power Automate cloud flow patterns. Packaged as managed solutions, ready to import into your environment.

Library

  • Browse Cloud Flows
  • Approvals
  • Email & Notifications
  • Reporting
  • Security & Compliance

AI

  • AI Tools
  • MCP Server
  • Generate a Token

Resources

  • About
  • FAQ
  • Support
  • Status
  • Contact
  • Power Automate Docs
  • Connector Reference

© 2026 FlowLibs. All rights reserved.

  • Privacy
  • Terms
  • Refunds
  • Cookies
  • Acceptable Use
  • DMCA
Help