Creating Word or PDF Documents in Power Platform with Power Automate Word Online connector

Today's post shows a way to create a document with data from arbitrary tables in Dataverse. The data is queried and transformed in a web resource and the document - based on a word template - is created with Power Automate's Connector for Word Online to render the final document as PDF. This allows the creation of sophisticated documents without the limitations (single table+referencing tables, poor formatting) of OOB document templates.

Generating a document in Power Platform with OOB features usually involves Document Templates or SSRS reports based on FetchXML queries. While the first one has several limitations regarding data retrieval - the latter is frankly just cumbersome to work with although it has received some updates to the report authoring extensions lately.

With Power Automate's connector for Word Online we can easily author the template in Word and fill placeholders including repeating content controls like tables or bulleted lists to make sophisticated documents without additional costs for a specific document generation engine.

The main concern one has with the OOB Word Templates in Power Apps is the fact that it is always scoped to a single entity along with its related entities. This makes it not feasible if the document has to span arbitrary tables.

Therefore we are going to fetch all data upfront in the client and pass a tailored dataset for easy consumption to Power Automate. This gives us the flexibility to do the data shaping in javascript and leave it to the Word connector to create the final document.

In our sample, we will print out teams with their roles and members as well as users with there roles and memberships. Word and PDF Document

Set Up the Word Document

As the process of template creation is thoroughly described by FlowJoe, I would advise going there for further information on how to get going with the document placeholders. The template I have set up can be downloaded here. The template uses two repeating section content controls for the users and teams tables. Word Template

The flow

When we create the flow we use a HTTP trigger that receives all data already in its desired shape. To set up the HTTP trigger use the sample JSON below

{ "users": [ { "fullname": "Test User1", "caltype": "Enterprise", "title": "Software Architect", "teams": "• Team1", "roles": "• System Administrator\n• Basic User\n• Sales, Enterprise app access" } ], "teams": [ { "name": "Team1", "description": "Default team for the parent business unit. The name and membership for default team are inherited from their parent business unit.", "admin": "Test User1", "isdefault": "Yes", "teamtype": "Owner", "members": "• Test User2\n• Test User1", "roles": "" } ] }
  • The flow uses an HTTP trigger (1) so that we can call it from a web resource.
  • Use the sample JSON above to generate the schema for the requests body (2).
  • Place the template in OneDrive and wire the documents placeholders with the array from the request body (3)/(4).
  • Create a temporary file in OneDrive (5) and convert it to PDF (6).
  • Finally, return the file PDF file (7).

Flow with Word Online Connector

Get the data

As explained before we will retrieve the data and pass it in good shape to the flow that is triggered via HTTP. This approach makes it easy to get the data via WebAPI and make the necessary transformations on the data. For example it would be easy to format dates, numbers and currencies to a desired appearance.

If you create a command bar button with the handler PowerAutomateDocCreation.CreateDocument, the users und teams are queried via WebAPI and the document is fetched for the Power Automate HTTP endpoint. Finally the file is download and offered to be downloaded.

var PowerAutomateDocCreation; (function (PowerAutomateDocCreation) { PowerAutomateDocCreation.CreateDocument = async function () { const userDto = await getUsers(); const teamDto = await getTeams(); const blob = await fetchDocument(userDto, teamDto); offerDownload(blob); }; async function getUsers() { const users = await Xrm.WebApi.online.retrieveMultipleRecords( "systemuser", "?$select=fullname,domainname,caltype,userlicensetype,isemailaddressapprovedbyo365admin,islicensed,title,employeeid,_businessunitid_value&$filter=accessmode eq 0&$expand=teammembership_association($select=name,teamid,systemmanaged,isdefault,teamtype),systemuserroles_association($select=name,ismanaged,roleid)" ); const userDto = users.entities.map((x) => { return { fullname: x.fullname, caltype: x["caltype@OData.Community.Display.V1.FormattedValue"], title: x.title ?? "", isdefault: x["isdefault@OData.Community.Display.V1.FormattedValue"], teamtype: x["teamtype@OData.Community.Display.V1.FormattedValue"], teams: x.teammembership_association .map((y) => "• " + y.name) .join("\n"), roles: x.systemuserroles_association .map((y) => "• " + y.name) .join("\n"), }; }); return userDto; } async function getTeams() { const teams = await Xrm.WebApi.online.retrieveMultipleRecords( "team", "?$select=name,teamid,description,_businessunitid_value,_administratorid_value,systemmanaged,isdefault,teamtype,teamid&$expand=teammembership_association($select=fullname,domainname;$filter=accessmode eq 0),teamroles_association($select=name,ismanaged,roleid)" ); const teamDto = teams.entities.map((x) => { return { name: x.name, description: x.description ?? "", admin: x["_administratorid_value@OData.Community.Display.V1.FormattedValue"], isdefault: x["isdefault@OData.Community.Display.V1.FormattedValue"], teamtype: x["teamtype@OData.Community.Display.V1.FormattedValue"], members: x.teammembership_association .map((y) => "• " + y.fullname) .join("\n"), roles: x.teamroles_association.map((y) => "• " + y.name).join("\n"), }; }); return teamDto; } async function fetchDocument(userDto, teamDto) { const payload = { users: userDto, teams: teamDto, }; response = await fetch( "https://<URL TO YOUR HTTP TRIGGER>", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), } ); const blob = await response.blob(); return blob; } function offerDownload(blob) { const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "UserAndTeamsReport.pdf"; document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox a.click(); a.remove(); } })(PowerAutomateDocCreation || (PowerAutomateDocCreation = {}));

Blog post written by

Marius Pothmann

Marius Pothmann is a Microsoft certified solution architect focusing on the Power Platform and cloud-native application development on Azure. Working on the Microsoft stack for over a decade gave him the opportunity to deep dive into many building blocks of successful enterprise application architectures like Dynamics 365, SQL Server, ADFS, ASP.NET (Core), SPA-based Web Apps, WPF/WinForms. If you think he can assist in solving your challenges drop us an e-mail and we will get back to you right away.