8.1Dataflows: standard and analytical
the destination decides the kind
A dataflow is a Power Query definition held in your environment and refreshed on a schedule. It reads from a connector, transforms in Power Query, and writes to a destination. The destination is not a property of the transformation: it is chosen when the dataflow is created, and it decides what the dataflow is called and what it can do.
Selecting a storage destination determines the dataflow's type. A dataflow that loads into Dataverse tables is a standard dataflow; one that loads into analytical tables is an analytical dataflow. A standard dataflow can be created in Power Apps and nowhere else. Dataflows created in Power BI are analytical, and dataflows created in Power Apps are either kind, depending on the choice made at creation.citedsource: Power Query docs — Dataflow types › what decides standard or analytical · artifacts-ch08.md
The destination decides the feature set, and the documentation gives six differences between the two kinds.citedsource: Power Query docs — Dataflow types › the capability differences · artifacts-ch08.md
| Operation | Standard | Analytical |
|---|---|---|
| Where it writes | Dataverse | Azure Data Lake Storage, provided by Power BI, by Dataverse, or by the customer |
| Power Query transformations | Yes | Yes |
| AI functions | No | Yes |
| Computed table | No | Yes |
| Load behaviour by default | Incremental load | Full load |
| Rows per query or table | Unbounded, and governed by Dataverse service protection at the moment of ingestion | Not stated on this page |
A computed table — a table in a dataflow whose input is another table in the same dataflow — belongs to the analytical kind alone. When a standard dataflow meets Dataverse service protection, the documented behaviour is that a record is retried up to three times.citedsource: Power Query docs — Dataflow types › the capability differences · artifacts-ch08.md
A dataflow may be refreshed on a schedule up to 48 times a day, and one table's refresh may run for up to 2 hours. Incremental refresh is bounded at 50 partitions for a single query and 150 for the whole dataflow, checked at publish time rather than at refresh time. A backup and restore of an environment does not copy its dataflows.citedsource: Power Query docs — Dataflows in Power Platform › refresh limits and known limitations · artifacts-ch08.md
Evidence — the two probes that stand in for a dataflow the lab has no way to author
Usage: pac [admin] [application] [auth] [canvas] [catalog] [code] [connection] [connector] [copilot] [env] [help] [managed-identity] [model] [modelbuilder] [package] [pages] [pcf] [pipeline] [plugin] [power-fx] [solution] [telemetry] [test] [tool](neither command printed a line)
pac surface, so this section shows the absence rather than a command. The bar's second segment filters two help outputs for the word; the line beneath the verb list is what they printed.The CLI at 2.10.1 publishes 24 top-level command groups, and a dataflow group is not among them. Filtering the help of the solution group and of the env group for the word returns nothing from either. A dataflow is authored in a browser and there is no command to put in a listing beside it.verifiedartifact: ch08-pac-no-dataflow · artifacts-ch08.md
The tables a dataflow is stored in are present in a developer-plan environment and empty. In skylib-test, msdyn_dataflows and msdyn_dataflowrefreshhistories both answered a query with zero rows rather than with a missing-table error. The storage belongs to the platform; what the plan withholds is the authoring surface and the capacity to run one.verifiedartifact: ch08-dataflow-tables · artifacts-ch08.md
Caution
This chapter's lab is three developer-plan environments. They cannot author a dataflow, they have no Azure Data Lake Storage account attached and no Fabric workspace linked, and no claim below rests on watching a dataflow run. The dataflow half of this chapter is cited with retrieval; the movement half is run against skylib-test. Two probes stand in for the part that could not be exercised, and they establish an absence rather than a behaviour.
Guidance
Read the type of a dataflow off its destination before reading its queries. A transformation that has to produce a computed table has already chosen its destination, and a transformation whose output has to be read by a model-driven app has chosen the other one.
8.2Dataflows inside a solution
what travels, and what has to be done again at the other end
A dataflow can be added to a solution and moved between environments with it. What arrives at the other end is the definition, and the parts of a dataflow that are not the definition are the parts a deployment has to deal with.
A dataflow added to a solution is a solution-aware dataflow, and a solution may hold several. Only a dataflow created in a Power Platform environment can be solution-aware. The data a dataflow loaded into its destination is not portable as part of the solution — only the definition is — so the data is recreated at the other end by refreshing the dataflow there. Credentials for the connections a dataflow uses are not persisted by a solution: after deployment the connections have to be edited before the dataflow can be scheduled, and that holds for an update import and an upgrade import alike, whether or not the dataflow itself changed.citedsource: Power Query docs — Dataflows in solutions › what travels and what does not · artifacts-ch08.md
The documented limitations name four that decide whether a pipeline can carry a dataflow at all. A dataflow cannot use a connection reference for any connector, and cannot use an environment variable — the two mechanisms a solution has for deferring a value to its target (§9.3). A dataflow does not add its required components, so a custom table it loads into is added to the solution by hand. An application user cannot deploy a dataflow, which rules out a service principal (§9.5). An incremental refresh configuration is not carried by the solution and is reapplied at the target after deployment, as is a link to another dataflow.citedsource: Power Query docs — Dataflows in solutions › the known limitations list · artifacts-ch08.md
Teams that ship on a schedule tend to stop putting dataflows in the deployed solution. The dataflow becomes a separately managed artifact, configured once per environment and left there, and the solution carries the tables it writes into. You trade the ingestion definition's place in source control for a release that finishes without a person.experientialbasis: deployments where the dataflow in a solution was the one component that made an otherwise unattended release need a person, and teams that responded by moving ingestion out of the solution rather than by automating around it
Caution
The two constraints compound. A deployment running as a service principal cannot deploy the dataflow, and one running as a person leaves the dataflow unable to refresh until someone signs into its connections. So a pipeline carrying a solution with a dataflow in it ends in a manual step, by design, on every environment it reaches.
Guidance
Decide where your dataflow lives before the first pipeline runs. Adding one to the deployed solution later turns an unattended release into an attended one, and that cost lands on whoever is on call.
8.3Paths out of Dataverse
the axis is direction, latency and who operates it
Six mechanisms move data between Dataverse and something else, and they are not generally alternatives — each answers a different question. What separates them is which direction data travels, how fresh it is at the far end, and which team runs the thing.
Sorted on that axis, the six fall out as follows. The table is a judgment about which question each path answers; what each path does is cited below it.experientialbasis: the sorting a team actually performs when choosing an integration path, drawn from projects where the wrong one was chosen first; each path's own behaviour is cited separately in this section rather than in the table
| Path | Direction | Freshness at the far end | Operated by |
|---|---|---|---|
| Dataflow | In | Scheduled refresh | The maker who authored it |
| Cloud flow | Either | Per event | The maker who authored it |
| Web API | Either | Per call | The system on the other side |
| Azure Synapse Link | Out | Near real time, and an hourly snapshot beside it | The team that owns the storage account |
| Link to Microsoft Fabric | Out | As changes occur | The team that owns the Fabric workspace |
| TDS endpoint | Out, read only | Live | Whoever wrote the query |
Three of the six are in the table for the comparison and settled elsewhere: the dataflow in §8.1 and §8.2, the Web API in §8.4 and §8.5, and the cloud flow in the chapter before this one. This section takes up the remaining three.
Azure Synapse Link for Dataverse, formerly Export to data lake, writes into an Azure Data Lake Storage account the customer owns. A table is eligible only if its Track changes property is on, the person creating the link holds the Dataverse system administrator role, and an environment is bounded at 10 Synapse Link profiles. It produces two copies of each table: near real-time data, synchronised by detecting what changed since the last synchronisation, and snapshot data, a read-only copy of the near real-time data refreshed every hour. Under high transaction volumes the documentation states that near real-time availability cannot be guaranteed.citedsource: Power Apps docs — Azure Synapse Link for Dataverse › what it produces · artifacts-ch08.md
Link to Microsoft Fabric needs no storage account and no Synapse workspace. Dataverse writes an optimised replica of the data in delta parquet format into Dataverse's own storage and shortcuts it into OneLake, so the data stays in Dataverse and consumes Dataverse storage rather than the customer's. Every non-system table whose Track changes property is on is added when the link is made. An environment links to one Fabric workspace.citedsource: Power Apps docs — Link to Microsoft Fabric › how it differs from Synapse Link · artifacts-ch08.md
The Tabular Data Stream endpoint emulates a SQL connection over the Dataverse business layer and is read-only: an INSERT or an UPDATE does not work through it. The Enable TDS endpoint setting is on by default. Only Microsoft Entra ID authentication is accepted, and a client needs port 1433 or port 5558 open. A query is bounded by a fixed five-minute timeout, which the platform reduces to two minutes for a complex query. A query through this endpoint does not fire plug-ins registered on RetrieveMultiple or Retrieve, it executes under the service protection limits, and it cannot read an elastic table.citedsource: Power Apps docs — Use SQL to query data › the TDS endpoint and its bounds · artifacts-ch08.md
Evidence — what the lab environment says about its own Fabric flags
<OrgSettings> <IsCommandingModifiedOnEnabled>true</IsCommandingModifiedOnEnabled> <IsLinkToFabricEnabled>true</IsLinkToFabricEnabled> <IsFabricVirtualTableEnabled>true</IsFabricVirtualTableEnabled> <CanCreateApplicationStubUser>false</CanCreateApplicationStubUser> <EnableActivitiesTimeLinePerfImprovement>1</EnableActivitiesTimeLinePerfImprovement> <EnableActivitiesFeatures>1</EnableActivitiesFeatures> <IsRetentionEnabled>true</IsRetentionEnabled> <IsArchivalEnabled>true</IsArchivalEnabled> <AllowRoleAssignmentOnDisabledUsers>false</AllowRoleAssignmentOnDisabledUsers> </OrgSettings>
orgdborgsettings is shown, so the two Fabric flags are read in the company of the seven settings beside them rather than on their own.The lab's environment reports IsLinkToFabricEnabled and IsFabricVirtualTableEnabled as true in orgdborgsettings. That is what the environment says about itself. The lab has no Fabric workspace to link to, so nothing here warrants a claim about how such a link behaves once it is made.verifiedartifact: ch08-org-settings · artifacts-ch08.md
Guidance
Choose your read path by who gets woken up when it stops. The TDS endpoint puts the query in the caller's hands and the failure in the caller's logs. A Synapse Link puts a storage account and a sync between the two, and a stalled sync surfaces as data that is silently a day old.
8.4Alternate keys and upsert
addressing a row by the identifier the other system has
An integration writes rows it did not create. The system on the far side holds its own identifier for each of them and has no Dataverse row id. An alternate key is what lets you address a Dataverse row by that foreign identifier, in the URL, without a lookup first.
An alternate key is one or more columns whose combination is unique. A table may have up to 10 of them. The columns that may take part are decimal, whole number, single line of text, date and time, lookup and choice, and the key as a whole must fit the underlying index constraints of 900 bytes and 16 columns. Where a key column holds a null, uniqueness is not enforced. Where a key column's data contains any of <, >, *, %, &, :, /, \ or #, an update or upsert through PATCH does not work.citedsource: Power Apps docs — Define alternate keys › limits and the index system job · artifacts-ch08.md
Creating an alternate key starts a system job that builds the indexes enforcing its uniqueness, and the key is not in effect until they exist. The key's state follows that job through Pending, In Progress, Active and Failed, and only Active means the key can be used.citedsource: Power Apps docs — Define alternate keys › limits and the index system job · artifacts-ch08.md
An upsert is a PATCH to the URI of a record: where the record does not exist it is created, and where it does it is updated. The documented use is synchronising with an external system that has no Dataverse row id, addressing the row by an alternate key instead. The response is the same either way, and the OData-EntityId header carries the key values rather than the row id — so a caller that needs to know which happened sends Prefer: return=representation and reads 201 Created for a create and 200 OK for an update, at the cost of an added retrieve. Alternate key values are not put in the request body. If-Match: * is the header that stops a PATCH creating a record by accident.citedsource: Power Apps docs — Update and delete rows with the Web API › upsert · artifacts-ch08.md
Evidence — the key becoming usable, and one URL answering four ways
"LogicalName": "ch89_releasecode",
"SchemaName": "ch89_ReleaseCode",
"KeyAttributes": [
"ch89_code"
],
"EntityKeyIndexStatus": "Pending"
… the same request later in the same session …
"EntityKeyIndexStatus": "Active"
Creating the key over a single text column answered HTTP 204 at once, and a read issued immediately afterwards reported EntityKeyIndexStatus as Pending. The same read later in the same session reported Active. The 204 reports that the request was accepted; the state reports whether the key can be used.verifiedartifact: ch08-alternate-key · artifacts-ch08.md
==== upsert 1 — PATCH by alternate key, record does not exist
HTTP 201
Preference-Applied: return=representation
x-ms-ratelimit-burst-remaining-xrm-requests: 7998
x-ms-ratelimit-time-remaining-xrm-requests: 1,199.86
{
"@odata.context": "https://org5014704c.crm.dynamics.com/api/data/v9.2/$metadata#ch89_releases(ch89_releaseid)/$entity",
"@odata.etag": "W/\"2123651\"",
"ch89_releaseid": "052c1e2d-9190-f111-8076-70a8a59af8d5"
}
==== upsert 2 — the same URL again, record now exists
HTTP 200
Preference-Applied: return=representation
x-ms-ratelimit-burst-remaining-xrm-requests: 7997
x-ms-ratelimit-time-remaining-xrm-requests: 1,199.86
{
"@odata.context": "https://org5014704c.crm.dynamics.com/api/data/v9.2/$metadata#ch89_releases(ch89_releaseid)/$entity",
"@odata.etag": "W/\"2124255\"",
"ch89_releaseid": "052c1e2d-9190-f111-8076-70a8a59af8d5"
}
==== upsert 3 — If-Match: * refuses to create
HTTP 404
x-ms-ratelimit-burst-remaining-xrm-requests: 7996
x-ms-ratelimit-time-remaining-xrm-requests: 1,199.86
{
"error": {
"code": "0x80060891",
"message": "A record with the specified key values does not exist in ch89_release entity"
}
}
==== upsert 4 — If-None-Match: * refuses to update
HTTP 412
x-ms-ratelimit-burst-remaining-xrm-requests: 7995
x-ms-ratelimit-time-remaining-xrm-requests: 1,199.86
{
"error": {
"code": "0x80040237",
"message": "A record with matching key values already exists."
}
}
One URL, run against skylib-test, produced four outcomes. The first PATCH created a row and answered 201; the second, to the same URL, answered 200 and returned the same ch89_releaseid. With If-Match: *, a key value matching nothing answered 404 with code 0x80060891 and created nothing. With If-None-Match: *, a key value matching a row answered 412 with code 0x80040237 and updated nothing.verifiedartifact: ch08-upsert · artifacts-ch08.md
Caution
Create an alternate key and load data through it in the same run and you have a race. Your load is written against a key whose index may not have finished building, and the failure reads as a data problem rather than a timing one.
Guidance
Send the conditional header that names your intent. An integration that only ever inserts should say If-None-Match: *, and one that only ever updates should say If-Match: *. A bare upsert is right for a synchronisation. Anywhere else it states no intent, and a wrong key value writes you a new row instead of failing.
8.5Batching and the service protection counters
what one request costs, and what a batch is one of
A row-at-a-time integration spends most of its time waiting for the network. The Web API gives you $batch, which carries many operations in one HTTP request, and the change set, which makes a group of them atomic.
A batch request carries up to 1,000 individual requests and cannot contain another batch. Operations grouped in a change set are atomic: where one of them fails, the batch rolls back the ones that had completed. A GET is not allowed inside a change set. The payload's line endings must be CRLF, and other line endings produce deserialisation errors. By default the batch stops at the first error and returns it; Prefer: odata.continue-on-error makes the server carry on and report each failure in the body.citedsource: Power Apps docs — Execute batch operations with the Web API › batches and change sets · artifacts-ch08.md
Evidence — a change set that committed, one that rolled back, and the table afterwards
HTTP 200 --batchresponse_9ed5058b-f992-424c-8d7b-3eacbfa917c5 Content-Type: multipart/mixed; boundary=changesetresponse_9264b309-bee9-405f-a5c7-bcaf14a0e7d2 --changesetresponse_9264b309-bee9-405f-a5c7-bcaf14a0e7d2 Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 1 HTTP/1.1 204 No Content OData-Version: 4.0 Location: https://org5014704c.crm.dynamics.com/api/data/v9.2/ch89_releases(ch89_code='REL-0003') OData-EntityId: https://org5014704c.crm.dynamics.com/api/data/v9.2/ch89_releases(ch89_code='REL-0003') X-Content-Type-Options: nosniff… the parts for Content-ID 2 and Content-ID 3, in the same shape …--changesetresponse_9264b309-bee9-405f-a5c7-bcaf14a0e7d2-- --batchresponse_9ed5058b-f992-424c-8d7b-3eacbfa917c5--
OData-EntityId names the row by its alternate key. A caller that addressed the row by a foreign identifier is answered in those same terms.HTTP 400
--batchresponse_d9374753-b113-4f07-a479-552b368e08de
Content-Type: multipart/mixed; boundary=changesetresponse_86a9d9c8-fea3-4753-8e92-429d5d6fc8fb
--changesetresponse_86a9d9c8-fea3-4753-8e92-429d5d6fc8fb
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 3
HTTP/1.1 400 Bad Request
REQ_ID: c6652958-c214-476c-a8b7-9c3582919265
X-Content-Type-Options: nosniff
Content-Type: application/json; odata.metadata=minimal
OData-Version: 4.0
{"error":{"code":"0x80044331","message":"A validation error occurred. The length of the 'ch89_name' attribute of the 'ch89_release' entity exceeded the maximum allowed length of '100'."}}
--changesetresponse_86a9d9c8-fea3-4753-8e92-429d5d6fc8fb--
--batchresponse_d9374753-b113-4f07-a479-552b368e08de--
HTTP 200
x-ms-ratelimit-burst-remaining-xrm-requests: 7992
x-ms-ratelimit-time-remaining-xrm-requests: 1,198.52
{
"@odata.context": "https://org5014704c.crm.dynamics.com/api/data/v9.2/$metadata#ch89_releases(ch89_code,ch89_name,ch89_releaseid)",
"value": [
{
"@odata.etag": "W/\"2124255\"",
"ch89_releaseid": "052c1e2d-9190-f111-8076-70a8a59af8d5",
"ch89_name": "Release 0001, renamed",
"ch89_code": "REL-0001"
},
{
"@odata.etag": "W/\"2124721\"",
"ch89_releaseid": "192e1e2d-9190-f111-8076-70a8a59af8d5",
"ch89_name": "Release 0003",
"ch89_code": "REL-0003"
},
{
"@odata.etag": "W/\"2124722\"",
"ch89_releaseid": "1a2e1e2d-9190-f111-8076-70a8a59af8d5",
"ch89_name": "Release 0004",
"ch89_code": "REL-0004"
},
{
"@odata.etag": "W/\"2124723\"",
"ch89_releaseid": "1b2e1e2d-9190-f111-8076-70a8a59af8d5",
"ch89_name": "Release 0005",
"ch89_code": "REL-0005"
}
]
}
A FetchXML read taken immediately after the rolled-back batch returned four rows, and the four settle every write in this chapter at once. REL-0001 carries the name the second upsert gave it. REL-0002 is absent, so the If-Match: * request created nothing. REL-0003 to REL-0005 are present, so the first change set committed. REL-0006 to REL-0008 are absent, so the second change set rolled back the two operations that had passed validation.verifiedartifact: ch08-read-back · artifacts-ch08.md
8.5.1The three counters, and what the lab's headers report
Service protection is enforced on three facets: the number of requests a user sends, the combined execution time of those requests, and the number of concurrent requests. The documented defaults, stated per web server, are 6,000 requests within a five-minute sliding window, 20 minutes of combined execution time within the same window, and 52 concurrent requests or higher. Each web server an environment makes available enforces them independently, and most environments have more than one; a trial environment has a single one. Exceeding a facet returns 429 Too Many Requests with a Retry-After header in seconds. The documentation says the figures can change and can vary between environments.citedsource: Power Apps docs — Service protection API limits › the three facets and their figures · artifacts-ch08.md
Two response headers report what is left: x-ms-ratelimit-burst-remaining-xrm-requests is the remaining number of requests for the connection, and x-ms-ratelimit-time-remaining-xrm-requests the remaining combined duration for all connections on the same user account. The documentation says not to steer by them — they are for debugging. On batching it is equally direct: a batch avoids the number-of-requests facet and pays for it on the execution-time facet, most scenarios are fastest sending single requests with a high degree of parallelism, and larger batches make the execution-time facet the one that fires. Batching is not a way around entitlement, which is a separately evaluated meter.citedsource: Power Apps docs — Service protection API limits › the response headers and batching advice · artifacts-ch08.md
Beside $batch there are messages that take many rows of one table in one request: CreateMultiple, UpdateMultiple, UpsertMultiple, and DeleteMultiple for elastic tables. For a standard table the documented starting point is 100 to 1,000 records per request; for an elastic table it is 100 operations, sent in parallel. On a standard table any error rolls the whole operation back. Custom standard tables and many common standard tables carry these messages, and not all standard tables do. Each such message counts as one request against the 6,000 facet, while every row inside it accrues against entitlement. One caveat matters for this section: an alternate key cannot be used with UpdateMultiple through the Web API.citedsource: Power Apps docs — Bulk operation messages › what they are and how limits apply · artifacts-ch08.md
Entitlement counts every create, read, update and delete, whichever client or endpoint issued it. Work that runs without a person — an application user, a non-interactive user, an administrative user, the SYSTEM user — draws on a pool defined at the tenant level rather than on a per-user allowance, and that pool is shared across all of those identities. For a tenant holding Power Apps licences alone the documented base is 25,000 requests per 24 hours. A third-party integration tool draws on the same meter as a flow.citedsource: Power Platform admin docs — Requests limits and allocations › entitlement is a separate meter · artifacts-ch08.md
Evidence — what the throttling headers reported, and what a batch cost against them
Where a response in this chapter's captures carries the two throttling headers, x-ms-ratelimit-time-remaining-xrm-requests is at or just below 1,200.00, which is the documented 20 minutes expressed in seconds. The burst header on those same responses reaches 7,998 at its highest — above the documented default of 6,000 for this environment on this day. Several captures in this chapter carry neither header, and nothing here is a claim about those.verifiedartifact: ch08-batch-timing · artifacts-ch08.md
before: burst-remaining=7994 time-remaining=1,199.55 20 rows, one request each: 8.32 s (416 ms per row) 20 rows, one $batch: 1.04 s (52 ms per row), HTTP 200, 20 success parts after: burst-remaining=7973 time-remaining=1,199.19
The same 20 upserts took 8.32 s sent one request at a time and 1.04 s sent as one batch, run against skylib-test minutes apart. Across both runs the burst counter fell from 7,994 to 7,973 — by 21, which is the 20 individual requests plus the one batch. The batch did the work of 20 and cost 1 against that counter.verifiedartifact: ch08-batch-timing · artifacts-ch08.md
Caution
Those timings are one machine, one network and one developer-plan environment on one afternoon. They are recorded for their direction and their order of magnitude. Latency to the service dominates the single-request run, and a caller closer to the region would narrow the gap without changing which side of it is faster.
Guidance
Size your bulk load against entitlement and shape it against service protection. The two meters answer different questions: entitlement decides whether your tenant may do the work in a day, and service protection decides how fast you may attempt it.