Perform ExecuteMultiple in batches and changesets from TypeScript using dataverse-ify version 2
My free tutorial course on writing Dataverse web resources has been available for over a year now, and it has had over 1000 people enrol! The course uses version 1 of dataverse-ify, and of course over that time I've been working on version 2 which is currently available in beta.
What is Dataverse-ify?
Dataverse-ify aims to simplify calling the Dataverse WebAPI from TypeScript inside model-driven apps, Single Page Applications (SPAs) and integration tests running inside VSCode/Node. It uses a small amount of metadata that is generated using dataverse-gen and a set of early bound types to make it easy to interact with dataverse tables and columns using a similar API that you might be used to using if you've used the IOrganizationService inside C#.
You can use the new version by adding @2 on the end of the node modules:
For example:
npx dataverse-auth@2 npx dataverse-gen@2 npm install dataverse-ify@2
Soon, I will be removing the beta tag and publishing it so that it will install by default. There are a few breaking changes detailed in the Upgrading readme, but I will be publishing more samples including a Single Page Application that uses dataverse-ify even where the Xrm.WebApi is not available.
I wanted to give you a peek at one of the features that I am really excited about in version 2 - support for ExecuteMultiple with batch and change set support. A batch allows you to send multiple requests in a single request, and change sets allow you to send multiple requests that will be executed as a transaction. This can give your client-side code a performance boost and make it easier to perform a single changeset where if one request fails, they all will fail. Custom API requests can even be wrapped up in executeMultiple!
Imagine that you have a Command Bar button that calls a JavaScript function from a grid that needs to make an update to a column on all of the selected records, and then wait for a triggered flow to run, as indicated by the updated column being reset. The updates can be wrapped up in an ExecuteMultiple batch rather than being made by lots of Update requests.
Create the top-level function
When a command bar calls a JavaScript function it can return a Promise if there is asynchronous work being performed. In our case, we don't want the model-driven app to wait until our flows are run, so we can use Promise.resolve on an internal function to 'fire and forget' a long-running task:
static async CreateProjectReportTrigger(entityIds: string[]): Promise<void> {
// Fire and forget the internal command so it does not cause a ribbon action timeout
Promise.resolve(ProjectRibbon.CreateProjectReportTriggerInternal(entityIds));
}
Create the Internal function and initialize the metadata cache
Inside the internal function, we need to first set our metadata that was created using dataverse-gen - this provides dataverse-ify with some of the information, it needs to work out the data types of columns that are not present in the WebApi responses. We also create a random value to update the column that will trigger flow:
setMetadataCache(metadataCache);jj const requestCount = entityIds.length; const trigger = "trigger" + Math.random().toString();
Make the update using executeMultiple (this is not C# remember, it's TypeScript!)
This is where the magic happens - we can create an array of UpdateRequest objects using the entitiyIds provided to function from the Command Bar:
// Trigger the flow for each selected project (using a batch)
const updates = entityIds.map((id) => {
return {
logicalName: "Update",
target: {
logicalName: dev1_projectMetadata.logicalName,
dev1_projectid: id,
dev1_reportgenerationstatus: trigger,
} as dev1_Project,
} as UpdateRequest;
});
const serviceClient = new XrmContextDataverseClient(Xrm.WebApi);
await serviceClient.executeMultiple(updates);
You can see that the updates array is simply passed into executeMultiple which then will bundle them up inside a $batch request. If you wanted to, you can run the updates inside a transaction by simply wrapping the batch inside an array:
await serviceClient.executeMultiple([updates]);
This array could actually contain multiple change sets which each would run independently inside a transaction.
So the resulting function would be:
static async CreateProjectReportTriggerInternal(entityIds: string[]): Promise<void> {
// Update a column on the selected records, to trigger a flow
try {
setMetadataCache(metadataCache);
const requestCount = entityIds.length;
const trigger = "trigger" + Math.random().toString();
// Trigger the flow for each selected project (using a batch)
const updates = entityIds.map((id) => {
return {
logicalName: "Update",
target: {
logicalName: dev1_projectMetadata.logicalName,
dev1_projectid: id,
dev1_reportgenerationstatus: trigger,
} as dev1_Project,
} as UpdateRequest;
});
const serviceClient = new XrmContextDataverseClient(Xrm.WebApi);
await serviceClient.executeMultiple(updates);
// Monitor the result
const query = `<fetch aggregate="true">
<entity name="dev1_project">
<attribute name="dev1_projectid" alias="count_items" aggregate="countcolumn" />
<filter>
<condition attribute="dev1_reportgenerationstatus" operator="eq" value="${trigger}" />
</filter>
</entity>
</fetch>`;
let complete = false;
do {
const inProgressQuery = await serviceClient.retrieveMultiple(query, { returnRawEntities: true });
complete = inProgressQuery.entities.length === 0;
if (!complete) {
const inProgressCount = inProgressQuery.entities[0]["count_items"] as number;
complete = inProgressCount === 0;
// Report status
Xrm.Utility.showProgressIndicator(`Generating Reports ${requestCount - inProgressCount}/${requestCount}`);
await ProjectRibbon.sleepTimeout(2000);
}
} while (!complete);
Xrm.Utility.closeProgressIndicator();
} catch (e) {
Xrm.Utility.closeProgressIndicator();
Xrm.Navigation.openErrorDialog({ message: "Could not generate reports", details: JSON.stringify(e) });
}
}
static sleepTimeout(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
This code adds in the polling for the number of records that have yet to have the flow run and reset the dev1_reportgenerationstatus attribute, indicating that it is completed or reports an error.
The batch request would look similar to:
--batch_1665710705198
Content-Type: application/http
Content-Transfer-Encoding: binary
PATCH /api/data/v9.0/dev1_projects(2361e495-1419-ed11-b83e-000d3a2ae2ee) HTTP/1.1
Content-Type: application/json
{"dev1_reportgenerationstatus":"trigger0.39324146578062336","@odata.type":"Microsoft.Dynamics.CRM.dev1_project"}
--batch_1665710705198
Content-Type: application/http
PATCH /api/data/v9.0/dev1_projects(e8184b63-1823-ed11-b83d-000d3a39d9b6) HTTP/1.1
Content-Type: application/json
{"dev1_reportgenerationstatus":"trigger0.39324146578062336","@odata.type":"Microsoft.Dynamics.CRM.dev1_project"}
--batch_1665710705198--
The code obviously can be improved by adding a timeout and better reporting of errors - but this shows the general idea of using executeMultiple using dataverse-ify version 2.
There are lots of other improvements in version 2 - so if you've used version 1 please do give version 2 a go whilst it's in beta and report any issues inside GitHub.
In my next post on version 2, I'll show you how to call a Custom API using a batch and changeset. If you want to peek before then - take a look at the tests for version 2 - they give lots of examples of its use.
@ScottDurow
Published on:
Learn moreRelated posts
Power Platform – Block sending customer data from Dataverse audit events to Purview Activity logging
We are announcing the ability to block customer data from being sent in Dataverse audit events to Purview Activity logging in Power Platform. ...
Microsoft 365 & Power Platform Community Call – April 16th, 2026 – Screenshot Summary
Call Highlights SharePoint Quicklinks: Primary PnP Website: https://aka.ms/m365pnp Documentation & Guidance SharePoint Dev Videos Issues...
Block Unmanaged Customizations in Power Platform Environments
This post is all about how to block unmanaged customizations within production environments. I'm going to assume that you have followed the ad...
Power Platform – Copilot hub – new model-driven app settings for Data entry, Data exploration, and Summary
We are announcing new Copilot hub settings that allow administrators to disable the AI features of Data entry, Data exploration, and Summary, ...
Microsoft Power Platform 2026 Release Wave 1: What Copilot and Agents Mean for Dynamics 365 Customer Engagement
In conversations with organizations over the past several months, a consistent question has started to surface: how do we actually use these A...
Power Platform – April 2026 – Screenshot Summary
Community Call Highlights Quicklinks: Power Platform Community: Power Apps Power Automate Power BI Power Virtual Agents Power Pages M365 Pla...
Deployment and Rollback through Power Platform Solution Patches
Recently I needed to deploy parts of a solution to a test environment. I also had to be able to rollback the solution. Solution Patches within...
Microsoft 365 & Power Platform Call (Microsoft Speakers) – April 14th, 2026 – Screenshot Summary
Call Highlights SharePoint Quicklinks: Primary PnP Website: https://aka.ms/m365pnp Documentation & Guidance SharePoint Dev Videos Issues...