[{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/dataverse/","section":"Tags","summary":"","title":"Dataverse"},{"content":"Microsoft has just released a new preview feature called Filtered record ownership. This post gives a short overview of it, based on my first tests in a non-production environment.\nWhat\u0026rsquo;s new? # If this is the first time you hear about it, I recommend reading the MS Learn article first and ideally playing around with the feature a bit in your own environment. I won\u0026rsquo;t go into much detail on things that are already covered in the official docs.\nEntity record filters # TL;DR: it is a new, declarative way to manage row-level security (RLS) in Dataverse, next to the existing concepts built around record ownership, business units, hierarchies, etc. Instead of ownership, you configure record filters as FetchXML expressions. Their conditions are evaluated for a combination of table and operation (the usual Create, Write, Append, etc.). You assign the record filters in security roles the same way you set the privilege depth (User, Business Unit, Organization, etc.) in the classic model.\nRecord ownership type # When you create a new table, there is a new option Filtered in Record ownership type. Unfortunately, so far it is only available for Standard tables, not for other table types. As expected, such a table has no ownerid or owningbusinessunitid column, so the only way to do RLS on it is with the new filters.\nTables for the new filters # New tables, plus a new lookup on roleprivileges, were added to store the configuration.\nApplying filters to existing tables # You can also apply filtered record security to any existing (or new) standard table owned by user/team or organization. I believe this will be the most common use case for most of us, as our solutions are usually built around existing system or custom tables.\nFiltering based on link-entities # As you can imagine, you will often need filter conditions on linked records. For example, you want users to see only contacts whose parent account is a supplier (customertypecode eq 11). You can create such a filter, add it to a security role and assign the role to a user or team.\n\u0026lt;fetch\u0026gt; \u0026lt;entity name=\u0026#34;contact\u0026#34;\u0026gt; \u0026lt;link-entity name=\u0026#34;account\u0026#34; from=\u0026#34;accountid\u0026#34; to=\u0026#34;parentcustomerid\u0026#34; alias=\u0026#34;O\u0026#34;\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;condition attribute=\u0026#34;customertypecode\u0026#34; operator=\u0026#34;eq\u0026#34; value=\u0026#34;11\u0026#34; /\u0026gt; \u0026lt;!--supplier--\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;/link-entity\u0026gt; \u0026lt;/entity\u0026gt; \u0026lt;/fetch\u0026gt; This brings us to an important question. Does the user need read permission on the link-entity (the account records in the example) for the condition to be evaluated correctly?\nBy default, yes. You can change this, but it is not very obvious how. The recordfilter table has a boolean column filterlinkedrecords. It is not on the default main form of recordfilter, so you have to set it through the API or some other tool.\nWith the default value true, every link-entity condition is evaluated with the user\u0026rsquo;s permissions. If you switch it to false, the link-entity conditions are evaluated regardless of the user\u0026rsquo;s read permissions on the linked records. In a way, it escalates the privileges. I guess it is similar to using the SYSTEM org service in the Dataverse .NET SDK, but only to read and evaluate the expression. I can imagine it also performs better with false, as the back-end doesn\u0026rsquo;t have to check the user\u0026rsquo;s permissions on the linked records. Pretty neat.\nUser-context operators # FetchXML has a few operators that use the current user\u0026rsquo;s context, such as eq-userid, eq-useroruserteams and eq-businessid. They work in record filters too.\nThis lets you control access through lookups to users or teams on the record itself, outside of record ownership. From a schema evolution point of view, I\u0026rsquo;m not a big fan of arbitrary lookups that assign a person to a role in some context. But at least record filters now give those lookups a real meaning in authorization.\nFor example, you want users to access only contacts whose parent account has them set as the Preferred User.\n\u0026lt;fetch\u0026gt; \u0026lt;entity name=\u0026#34;contact\u0026#34;\u0026gt; \u0026lt;link-entity name=\u0026#34;account\u0026#34; from=\u0026#34;accountid\u0026#34; to=\u0026#34;parentcustomerid\u0026#34; alias=\u0026#34;O\u0026#34;\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;condition attribute=\u0026#34;preferredsystemuserid\u0026#34; operator=\u0026#34;eq-userid\u0026#34; /\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;/link-entity\u0026gt; \u0026lt;/entity\u0026gt; \u0026lt;/fetch\u0026gt; The great thing, in my opinion, is that these operators still use the actual user, even inside a link-entity with filterlinkedrecords set to false. So the user doesn\u0026rsquo;t need read permission on the linked record that the condition is evaluated against. In the example, the user doesn\u0026rsquo;t have to be able to read the account.\nAdditive, not restrictive # There are two more scenarios that I think need a bit of explanation. A user or team can have multiple security roles, and these roles can give privileges that seem to contradict each other.\nFirst, take a table with user or team ownership. One security role gives a privilege with a record filter, and another role gives the same privilege with a classic scope (for example, Organization). In this case, the permission is always granted.\nSecond, the same user or team gets two different record filters for the same table and operation through their security roles. One filter rules out the record, the other one doesn\u0026rsquo;t. Again, the permission is granted.\nI guess this pretty much follows how the classic permission scopes work. Roles add up, they never take access away.\nSo if you want a record filter to actually limit access, make sure that no other role of the user gives broader access to the same table and operation. This includes roles the user gets through teams.\nALM # The new tables recordfilter and entityrecordfilter are solution-aware, so their records work like other Solution Component Framework (SCF) components. You can add them to a solution, and when you unpack it, each record is a separate XML file.\n\u0026lt;recordfilter uniquename=\u0026#34;my_contact_record_filter\u0026#34;\u0026gt; \u0026lt;displayname default=\u0026#34;Contact Record Filter\u0026#34;\u0026gt; \u0026lt;label description=\u0026#34;Contact Record Filter\u0026#34; languagecode=\u0026#34;1033\u0026#34; /\u0026gt; \u0026lt;/displayname\u0026gt; \u0026lt;fetchxml\u0026gt;\u0026amp;lt;fetch\u0026amp;gt; \u0026amp;lt;entity name=\u0026#34;contact\u0026#34;\u0026amp;gt; \u0026amp;lt;link-entity name=\u0026#34;account\u0026#34; from=\u0026#34;accountid\u0026#34; to=\u0026#34;parentcustomerid\u0026#34; alias=\u0026#34;O\u0026#34;\u0026amp;gt; \u0026amp;lt;filter\u0026amp;gt; \u0026amp;lt;condition attribute=\u0026#34;preferredsystemuserid\u0026#34; operator=\u0026#34;eq-userid\u0026#34; /\u0026amp;gt; \u0026amp;lt;/filter\u0026amp;gt; \u0026amp;lt;/link-entity\u0026amp;gt; \u0026amp;lt;/entity\u0026amp;gt; \u0026amp;lt;/fetch\u0026amp;gt;\u0026lt;/fetchxml\u0026gt; \u0026lt;filterlinkedrecords\u0026gt;0\u0026lt;/filterlinkedrecords\u0026gt; \u0026lt;iscustomizable\u0026gt;1\u0026lt;/iscustomizable\u0026gt; \u0026lt;isrolebusinessunitquery\u0026gt;0\u0026lt;/isrolebusinessunitquery\u0026gt; \u0026lt;statecode\u0026gt;0\u0026lt;/statecode\u0026gt; \u0026lt;statuscode\u0026gt;1\u0026lt;/statuscode\u0026gt; \u0026lt;/recordfilter\u0026gt; \u0026lt;entityrecordfilter objecttypecode=\u0026#34;contact\u0026#34; recordfilterid.uniquename=\u0026#34;my_contact_record_filter\u0026#34;\u0026gt; \u0026lt;iscustomizable\u0026gt;1\u0026lt;/iscustomizable\u0026gt; \u0026lt;name\u0026gt;Contact Entity Record Filter\u0026lt;/name\u0026gt; \u0026lt;statecode\u0026gt;0\u0026lt;/statecode\u0026gt; \u0026lt;statuscode\u0026gt;1\u0026lt;/statuscode\u0026gt; \u0026lt;/entityrecordfilter\u0026gt; In the security role XML, each role privilege points to its recordfilter by unique name.\n\u0026lt;Role id=\u0026#34;{8a5f3a57-b624-430c-8b4e-9a28bd65046b}\u0026#34; name=\u0026#34;Record Filter role\u0026#34;\u0026gt; \u0026lt;IsCustomizable\u0026gt;1\u0026lt;/IsCustomizable\u0026gt; \u0026lt;IsAutoAssigned\u0026gt;0\u0026lt;/IsAutoAssigned\u0026gt; \u0026lt;RolePrivileges\u0026gt; \u0026lt;RolePrivilege name=\u0026#34;prvReadContact\u0026#34; level=\u0026#34;RecordFilter\u0026#34; recordfilter=\u0026#34;my_contact_record_filter\u0026#34; /\u0026gt; \u0026lt;/RolePrivileges\u0026gt; \u0026lt;/Role\u0026gt; First impressions # Overall, I think this is a huge addition to authorization in Dataverse.\nSometimes the classic ownership model feels too rigid. Organizational changes, or even small changes in context, are expensive to reflect in the model, for example when you keep creating business units and moving them around in the hierarchy. Filtered record security could help in those cases, when the org hierarchy changes all the time or when you need more granular permissions.\nOne thing to keep in mind is performance. The filter queries are most likely evaluated on every request where a filter affects the user\u0026rsquo;s privilege. I haven\u0026rsquo;t done much testing of this so far.\nThere is still a lot to test in different scenarios. For example, the recordfilter table has a read-only boolean column isrolebusinessunitquery. I tested it briefly, but I wasn\u0026rsquo;t able to figure out what switches it to true. Based on its name, my guess is that it marks whether the filter is evaluated in the context of the security role\u0026rsquo;s business unit instead of the user\u0026rsquo;s. That would matter with Modernized business units, where a user can get roles from other business units. This could open up some very interesting scenarios.\nSo let\u0026rsquo;s get more familiar with the feature before it goes GA and give Microsoft some constructive feedback.\nAnyway, happy filtering!\n","date":"28 September 2026","permalink":"https://www.tompivny.com/posts/filtered-record-security-in-dataverse/","section":"Posts","summary":"\u003cp\u003eMicrosoft has just released a new preview feature called \u003cstrong\u003eFiltered record ownership\u003c/strong\u003e. This post gives a short overview of it, based on my first tests in a non-production environment.\u003c/p\u003e\n\u003ch2 id=\"whats-new\" class=\"relative group\"\u003eWhat\u0026rsquo;s new? \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#whats-new\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003eIf this is the first time you hear about it, I recommend reading the \u003ca href=\"https://learn.microsoft.com/en-us/power-apps/maker/data-platform/filtered-view-record-ownership\" target=\"_blank\" rel=\"noreferrer\"\u003eMS Learn article\u003c/a\u003e first and ideally playing around with the feature a bit in your own environment. I won\u0026rsquo;t go into much detail on things that are already covered in the official docs.\u003c/p\u003e","title":"Filtered record security in Dataverse"},{"content":"","date":null,"permalink":"https://www.tompivny.com/posts/","section":"Posts","summary":"","title":"Posts"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/security/","section":"Tags","summary":"","title":"Security"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/","section":"Tags","summary":"","title":"Tags"},{"content":"","date":null,"permalink":"https://www.tompivny.com/","section":"Tomas Pivny","summary":"","title":"Tomas Pivny"},{"content":"What is transactional outbox pattern? # Let\u0026rsquo;s start with the simplest example, which we all use, and that is an e-mail client. When you write an e-mail message and hit \u0026ldquo;send\u0026rdquo;, the message is first saved in your outbox folder, from which your client (or server-side logic, but let\u0026rsquo;s not get into any details here) pushes the message to a server that handles the rest. If there were no outbox, or other caching mechanism, then in case of some network issues or other kinds of problems, your message could be lost.\nWe can apply the same principle in case of information systems when we want to notify someone about some important event that happened in our system. If we add the atomicity principle (all operations go through successfully or not at all), we get the transaction outbox pattern.\nThis pattern solves a problem that we call in the industry, the dual-write problem - the lack of transaction coordination when pushing data to multiple systems.\nTransactions in Dataverse # To successfully build a reliable transaction outbox based on Dataverse, we really have to understand some basics about transactions of our lovely back-end. The essentials of transactions in Dataverse are provided by the Event framework. In a nutshell:\nEvery request we make against Dataverse is represented by an event / message. Each event can have its own logic executed during different stages of what we call execution pipeline. The database transaction starts at PreOperation and ends after the last synchronous PostOperation (if we consider the scenario, where the only target service is the Dataverse instance where the Event was triggered without any immediate external calls). PreValidation runs before that, outside the transaction, which is why it is the right stage to cancel an operation. This can get a bit more complex (and it often does) when in the synchronous stages, we trigger other event which has its execution pipeline logic and the cascading continues (the metric we measure this is called Plugin Depth). One thing to keep in mind here: PreValidation never opens a transaction, but at a higher depth it joins the one that is already running. Check IExecutionContext.IsInTransaction if you need to know. Do we really need a custom solution? # Ask this question (in general when building anything on Power Platform, as you should imo utilize as much what the managed platform offers). Dataverse has a built-in Change Data Capture (CDC) that we call change tracking. In my opinion, it is a very underutilized feature. It basically allows a consuming client/service to retrieve the changes for a selected time-span on the entity level. Both Microsoft\u0026rsquo;s Azure Synapse Link for Dataverse and Link to Microsoft Fabric utilize change tracking to get the latest data.\nSo is Dataverse change tracking a better option to use instead of custom-built transactional outbox? It depends as both have their use cases and both can co-exist in your systems. My mental model about this usually goes like this.\nUse change tracking if\nYour main use case is to replicate data 1:1 Consuming raw data changes based on your Dataverse DB / Entity schema is not an issue Your priority is to maintain as little custom code as possible Use transaction outbox if\nYour main use case is to share meaningful business / domain events that are hard to interpret from raw data changes Your Dataverse DB / Entity schema changes often Your responsibility is to push these events to the outside of the boundaries of your system Building the solution # During the research I noticed that Chris Piasecki submitted his Transactional Outbox reference architecture to be part of the architecture center (gh issue). I am glad to see that this topic is getting more traction and more best practices are being shared in the community. Hopefully Chris\u0026rsquo; design will be published soon so definitely check it out.\nSo if you decided you still want to go with a transactional outbox and you understand the Dataverse Event framework mentioned earlier, then building the solution is quite simple.\nWrite a plug-in and register a SdkMessageProcessingStep so you \u0026ldquo;hook\u0026rdquo; into the synchronous PostOperation execution pipeline of the main DB event (in most cases a Create or Update of a record).\nIn your code, identify if the DB event characterizes the business event you care about.\nIf it does, create a corresponding row in your outbox table.\nTo get to your end goal, you will need some kind of component that pushes the event, represented by the new outbox record, to either your central message broker or whatever the intended destination is. Usually we call this component a Message relay. Won\u0026rsquo;t get into more details here, as this part would be very specific based on what the edge destination of your message is. Regardless, ensure you follow the best practices like idempotency, respecting the order of the events etc.\nOptional but useful # A few things I want to mention that you could consider.\nExpress the business events as components # The first is to have a layer that represents the actual business events. I have touched this topic in my earlier articles about Business Events Catalogs. Basically, every event, you want to push outside of your system, should be described in the form of a custom message, where its name starts with the On* prefix and has no main operation registered.\nThis doesn\u0026rsquo;t add much to the functionality of the solution. The main thing is to force yourself or your colleagues to think about these events more in the business layer, and how the domain, that your system is responsible for, plays a role in your organization. As a side effect, this kind of abstraction makes the codebase more maintainable as it follows the separation of concerns principle.\nElastic table as the outbox # I think the elastic tables are great for this use case with a small caveat.\nFirst, the contextual data you will pass along the event will probably be in a semi-structured format and elastic tables offer the JSON column with some special server-side query abilities.\nSecond is the automatic retention. Every elastic table has a TTL column, where you can specify the number of seconds after which a given row should be deleted.\nThe caveat of elastic tables is how they behave differently in transactions. Most importantly, in a given synchronous execution pipeline, where you attempt to create a record in an elastic table, any further error thrown after the create was called won\u0026rsquo;t roll back the whole transaction, as it does for standard tables. Therefore you should really include the operation against the elastic table as the last one in the whole pipeline.\nMaintain a map of subscribers + events # In an ideal world, your organization\u0026rsquo;s centralized message broker distributes the events across the whole org and often maintains the list of subscribers per event / topic. In practice, besides the main broker, you will have to deal with some async direct point-to-point integrations or even multiple brokers. In such scenarios, I recommend maintaining a list of unique combinations of subscribers + business events. A simple standard Dataverse table is more than fine (you can relate the outbox table rows to these records). You can leverage it if you need to build specific event payloads based on the subscriber needs.\n","date":"20 September 2026","permalink":"https://www.tompivny.com/posts/transactional-outbox-in-dataverse/","section":"Posts","summary":"\u003ch2 id=\"what-is-transactional-outbox-pattern\" class=\"relative group\"\u003eWhat is transactional outbox pattern? \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#what-is-transactional-outbox-pattern\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h2\u003e\u003cp\u003eLet\u0026rsquo;s start with the simplest example, which we all use, and that is an e-mail client. When you write an e-mail message and hit \u0026ldquo;send\u0026rdquo;, the message is first saved in your outbox folder, from which your client (or server-side logic, but let\u0026rsquo;s not get into any details here) pushes the message to a server that handles the rest. If there were no outbox, or other caching mechanism, then in case of some network issues or other kinds of problems, your message could be lost.\u003c/p\u003e","title":"Transactional outbox in Dataverse"},{"content":"Modern theming is one of the customization features that gives your Model-Driven Power Apps a bit more refreshed look aligned with Fluent 2. There are scenarios where you operate multiple apps in one environment, where each deserves its own custom theme and/or branding specific to a single app. Until recently, I didn\u0026rsquo;t know it was possible to scope themes to a single app and to be honest, at least for me, the docs and community articles do not make it super clear how to apply it.\nThere are plenty of articles, blog posts and even a couple of community tools (e.g. _n.ModernThemeBuilder in XrmToolBox) that cover how to create your own theme, so I won\u0026rsquo;t go into detail here. If you never worked with custom theming, I recommend starting with the official docs and then some community posts to cover the practical side.\nComponents # In terms of solution components in this context, we usually deal with:\nXML web resource with our custom theme (optionally with only app header colors) Optionally a web resource with the logo (can be svg, jpg, png) At least one app-module (model-driven app) to apply the theme to Settings definitions (provided by Microsoft) with your own values (app or environment values). In particular these two definitions: Custom theme definition and Override app header color Solution explorer method # Let\u0026rsquo;s now assume that you want to apply a theme scoped to your single app that doesn\u0026rsquo;t follow the default (environment) theming settings.\nYou normally follow the usual steps - create your theme definition that you upload as an XML web resource, optionally along with the logo to display in the app header.\nThen in one solution, put your model-driven app along with a particular Setting definition (Add existing -\u0026gt; More -\u0026gt; Setting). \u0026ldquo;Override app header color\u0026rdquo; does exactly what the name suggests, only modifies the app header coloring. If we want additional customization, such as a custom logo in the app header or default Fluent colors, then instead select \u0026ldquo;Custom theme definition\u0026rdquo;. If you use values for both, the \u0026ldquo;Custom theme definition\u0026rdquo; supersedes the \u0026ldquo;Override app header color\u0026rdquo; setting.\nThe wizard appears after you confirm the selection and asks you if you want to include the setting definition and setting environment value. You can uncheck both, as the setting definition already exists in every environment we would be deploying to, and we don\u0026rsquo;t care about the environment value since we are applying this per-app.\nIf you have both the model-driven app and the setting in one solution, when you edit the setting, you should see at the bottom an option to set the value specifically for your app. This option is only visible if you have both the setting and the app in the same solution. Put there the unique name of the XML web resource with your theme.\nPublishing all the changes should propagate the theme to your app, so other apps are unaffected, and you can happily export your solution and deploy it downstream if you wish.\nUnpacked solution method # I prefer to work directly with the unpacked components in my IDE. In this particular case, it is much simpler than going through the solution explorer, in my opinion. You simply add the app setting customization in the AppModule.xml (or its .yaml equivalent if you are working with the native git integration).\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;AppModule\u0026gt; \u0026lt;!--unrelated app module metadata \u0026lt;UniqueName\u0026gt;beer_ProjectManagement\u0026lt;/UniqueName\u0026gt; \u0026lt;statecode\u0026gt;0\u0026lt;/statecode\u0026gt; \u0026lt;statuscode\u0026gt;1\u0026lt;/statuscode\u0026gt; ... --\u0026gt; \u0026lt;appsettings\u0026gt; \u0026lt;appsetting settingdefinitionid.uniquename=\u0026#34;CustomThemeDefinition\u0026#34;\u0026gt; \u0026lt;!-- setting definition name --\u0026gt; \u0026lt;iscustomizable\u0026gt;1\u0026lt;/iscustomizable\u0026gt; \u0026lt;value\u0026gt;beer_modern_theme.xml\u0026lt;/value\u0026gt; \u0026lt;!-- XML web resource with your custom theme --\u0026gt; \u0026lt;/appsetting\u0026gt; \u0026lt;/appsettings\u0026gt; \u0026lt;/AppModule\u0026gt; Both methods achieve the same result. The solution explorer method is more approachable if you prefer working in the browser, while the unpacked solution method is quicker if you already have your solution files checked out locally. Either way, once the solution is deployed, each app carries its own theme independently.\n","date":"17 May 2026","permalink":"https://www.tompivny.com/posts/applying-per-app-modern-theming/","section":"Posts","summary":"\u003cp\u003eModern theming is one of the customization features that gives your \u003cstrong\u003eModel-Driven Power Apps\u003c/strong\u003e a bit more refreshed look aligned with \u003cstrong\u003eFluent 2\u003c/strong\u003e. There are scenarios where you operate multiple apps in one environment, where each deserves its own custom theme and/or branding \u003cstrong\u003especific to a single app\u003c/strong\u003e. Until recently, I didn\u0026rsquo;t know it was possible to scope themes to a single app and to be honest, at least for me, the docs and community articles do not make it super clear how to apply it.\u003c/p\u003e","title":"Applying per-app Modern Theming"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/power-apps/","section":"Tags","summary":"","title":"Power Apps"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/apim/","section":"Tags","summary":"","title":"APIM"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/asp.net-core/","section":"Tags","summary":"","title":"ASP.NET Core"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/azure/","section":"Tags","summary":"","title":"Azure"},{"content":"When integrating with information systems, establishing a contract before any actual development starts is the common approach, especially in enterprise environments. The reasons for this revolve around established organizational processes and the complexity of implementations. In larger organizations, API contract design does not have to be handled directly by engineers — it is often delegated to roles like enterprise architects, solution analysts or other non-coding roles.\nFor REST APIs, the industry standard for contracts is the OpenAPI specification and the Swagger toolset. Applying this approach, especially when dealing with inbound integrations as a Power Platform engineer, poses some challenges that I\u0026rsquo;d like to address in this post.\nNative Platform solutions # The platform offers some out-of-the-box approaches to expose a REST API interface externally. Let\u0026rsquo;s briefly explore the available options from a design-first perspective.\nBuilt-in HTTP trigger # The When an HTTP request is received trigger is quite flexible in terms of interface design. It allows you to define the HTTP method and path, including path variables, and it accepts query parameters too. The request body is validated against a JSON schema you provide. Also, with the recent URL changes, you can predict the base URL at design time, as long as your environments are already provisioned and you are not using the \u0026ldquo;Anyone\u0026rdquo; authN method (the workflow ID can be manually set in the flow metadata XML source file).\nHowever, taking a pre-agreed OpenAPI/Swagger contract and implementing it with this trigger is fundamentally incompatible. Things like API versioning or certain schema validation scenarios cannot be achieved without significant hassle. Personally, I am also not a big fan of using Power Automate flows for synchronous logic where higher throughput is expected.\nCustom connectors # By design, custom connectors are on paper the closest thing to the design-first approach. You can create a custom connector in your DEV environment simply by importing an OpenAPI 2.0 definition (support for 3.0 was recently announced). For outbound scenarios, this is probably the most elegant solution the platform offers. Custom connectors even support VNET delegation for enterprise scenarios.\nThe main downside for me is being limited to two trigger types — webhook and polling. This means we cannot build true inbound synchronous operations, or asynchronous logic without subscription registration, using custom connectors.\nDataverse Web API # Dataverse automatically exposes a REST API using the OData 4.0 protocol when you create an entity. In essence, the entity schema model defines the API contract to some degree. This works great as long as your physical data model is properly normalized and all parties agree on a canonical object schema for common objects. In practice, unless there is a strong organizational emphasis on this discipline — or you have the will to push this design philosophy — it rarely gets properly embraced.\nBeyond entities, we can create custom messages in Dataverse, nowadays usually done by creating a Custom API. It allows you to define various input and output parameters, expose the message either as an action (POST method) or as a function (GET method), and set a custom plugin as the message\u0026rsquo;s main operation. This, in essence, can solve the problem of having a contract that is agnostic to the underlying data model. However, Custom APIs alone are nowhere near compatible with OpenAPI specs.\nAzure to fill the gaps # At this point, it is clear that if we want to apply the design-first approach, we cannot rely on Power Platform components alone. Usually the obvious place to look for solutions in such cases is nothing other than Azure.\nAPI gateway # In enterprise scenarios, an API gateway is usually the source of truth for API contracts and handles request validation. In Azure, Azure APIM (Azure API Management) is the clear solution to fill some of the gaps mentioned above — it supports pre-agreed OpenAPI definitions, contract validation, and even API versioning.\nAPIM, however, does not cover business logic execution — it is a gateway, not a runtime. As a team responsible for operating Power Platform-based solutions, running your own APIM introduces operational overhead you might want to avoid. The subscription cost can also get quite high, especially for higher tiers that include features like private VNET integration.\nCustom reverse proxy # If we want to avoid running our own APIM (or other API gateway equivalent) and still apply the design-first approach for inbound scenarios, we can build our own solution. When designing Power Platform solutions, I still try to follow the general rule of utilizing as much as the platform natively offers, mainly to reduce operational overhead and simplify deployments. Therefore, the role of such a solution should be minimized.\nThe custom proxy fills these gaps:\nPublish any REST endpoint with a defined method and path Translate inbound requests to Dataverse Custom API calls (more on that in the next section) Forward caller identity — use the caller\u0026rsquo;s bearer token to authenticate to Dataverse Expose endpoints only to a private VNET (common requirement in enterprise scenarios) The proxy should not require redeployment every time we need to add or modify an endpoint. To stay Power Platform / Dataverse centric, the endpoint configuration can be stored in a simple custom entity in Dataverse, allowing the proxy to periodically or on demand refresh its configuration from those records.\nThere are several options for language, runtime, and hosting. I went with a lightweight ASP.NET Core app hosted in Azure Container Apps. Other hosting options like Azure App Service should work just as well.\nHybrid architecture # No single component covers all the needs of a design-first approach end-to-end. By combining the components mentioned above, we get a complementary setup that works quite well together. Here is the solution that worked well in my scenario.\nComponents and roles # API gateway\nAs mentioned, ideally as a Power Platform engineer, I want to avoid running my own API gateway and instead use an org-wide gateway that my team does not have to operate. If such an opportunity occurs, then utilize the gateway mainly as:\nThe source of truth for agreed OpenAPI contracts The layer that validates requests against those contracts, so only valid requests reach your business application Custom reverse proxy with translation\nThe proxy serves as the entry point into my environment. Ideally, I want to keep it as lean as possible and leave the heavy lifting to the Custom API with Plugins (described in the Dataverse plugin common logic section below).\nSo what exactly do we want to translate to? The goal is to pass all parameters from the request as Custom API input parameters, where they can be properly processed by the business logic. To keep the middleware as generic as possible, the Custom API contract parameters should be consistent across all endpoints.\nParameter Type Description requestPayload String Encoded JSON object that consists of the following: body, path parameters and query strings. Those three things can be optionally split into their own input parameters. source String Optional: unique consistent identifier of the external caller (system). Usually received as part of the header in the request. correlationId String Optional: id for end-to-end tracing of operations Dataverse plugin common logic\nI won\u0026rsquo;t go into too much detail here, as I want to keep this article at a certain level of abstraction. You can get quite creative by building common core logic and, for example, wrapping it in a custom PluginBase class. Generally, processing sync and async requests involves some shared logic that should follow consistent rules.\nRequest parsing — As mentioned above, the proxy bundles the body, path parameters, and query strings into a single JSON envelope before invoking the Custom API. Unpacking and deserializing it into strongly typed DTOs should be owned by shared dispatcher infrastructure — so each plugin receives an already-parsed typed object and stays focused purely on business logic.\nError handling — Sync and async modes differ here. For sync operations, a shared dispatcher should catch all exceptions and translate them into structured HTTP-style responses (4xx for business rule violations, 5xx for unexpected failures), with the audit record always persisting regardless of outcome. For async, the middleware returns a 202 Accepted immediately with a correlation ID, and the plugin processes the request in the background. Unhandled exceptions should surface to trigger the platform\u0026rsquo;s retry mechanism — but permanent failures need explicit handling to break the retry loop. How the caller learns about the final outcome (webhook callback vs. polling a status endpoint) depends on what the API contract defines.\nDTO generation — Developers can benefit from the design-first approach by using the OpenAPI/Swagger file to generate request/response objects and take advantage of strong typing. Tools like NSwag can really help here.\nIdempotency — Utilize patterns like upserts on a natural key or correlation ID guard checks (skip if already processed) to make re-execution safe.\nLogging — We have plugin trace logs, but I believe that integration scenarios deserve their own dedicated logging storage. For logging inside Dataverse, I strongly recommend using Elastic tables due to their many benefits (automatic retention, JSON columns with Cosmos queries, partitioning for query performance, etc.). Azure Log Analytics is also a viable alternative.\nAuthN and AuthZ # Although it is not typically part of the API design, we shouldn\u0026rsquo;t leave authentication and authorization as an afterthought.\nTypically, you come across two approaches when it comes to caller identification, assuming you are using OAuth2. Either the identity of the request is the original caller (actual end-user or service principal), or authentication is handled by the API gateway (with a token belonging to the gateway\u0026rsquo;s service principal). In either scenario, you should know beforehand what identities to expect that will be calling your APIs.\nAnother thing to keep in mind is whether the caller (external system or API gateway) declares directly that it wants to be authorized to access the Dataverse URI - typically by using scope such as https://yourorg.crm4.dynamics.com/.default. In such a scenario, the caller needs to be aware of the actual Dataverse URI behind the proxy. Alternatively, it might be worth considering using the on-behalf-of flow to do an access token exchange, if you want to simplify the process for external callers.\nLastly, make sure you have your Dataverse row-level security properly configured, especially if the original caller identities should be the principals that execute the business transactions in Dataverse (in order to keep the audit trail).\nWrapping up # This setup is not lightweight. You are adding a hosted service, a configuration entity, and a translation layer to what might otherwise be a simple flow. Before going down this path, it is worth asking honestly whether the volume and governance requirements of your integrations actually justify it. For low traffic, low-stakes inbound calls where contract rigidity is not a hard requirement, the native platform options covered at the beginning are probably the better trade.\nWhen the complexity is justified, the key design principle I would take away from this is: keep the proxy thin and generic, and let the Custom API with plugins own the business logic. The proxy\u0026rsquo;s only job is to translate HTTP into a Dataverse message — it should not know anything about what the endpoint actually does. That separation is what keeps the middleware stable and lets the business logic evolve independently, without redeployments.\nThere is a lot this article deliberately skips — the actual proxy implementation, how to wire up the container deployment into your ALM pipeline, and more advanced plugin patterns like async callback handling. I may cover those in follow-up posts sometime in the future.\n","date":"10 May 2026","permalink":"https://www.tompivny.com/posts/design-first-rest-apis-in-power-platform/","section":"Posts","summary":"\u003cp\u003eWhen integrating with information systems, establishing a contract before any actual development starts is the common approach, especially in enterprise environments. The reasons for this revolve around established organizational processes and the complexity of implementations. In larger organizations, API contract design does not have to be handled directly by engineers — it is often delegated to roles like enterprise architects, solution analysts or other non-coding roles.\u003c/p\u003e\n\u003cp\u003eFor REST APIs, the industry standard for contracts is the \u003ca href=\"https://spec.openapis.org/\" target=\"_blank\" rel=\"noreferrer\"\u003eOpenAPI\u003c/a\u003e specification and the \u003ca href=\"https://swagger.io/\" target=\"_blank\" rel=\"noreferrer\"\u003eSwagger\u003c/a\u003e toolset. Applying this approach, especially when dealing with \u003cstrong\u003einbound integrations\u003c/strong\u003e as a Power Platform engineer, poses some challenges that I\u0026rsquo;d like to address in this post.\u003c/p\u003e","title":"Design-First REST APIs in Power Platform"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/plugins/","section":"Tags","summary":"","title":"Plugins"},{"content":"When you are creating a Power Automate flow with the Dataverse trigger \u0026ldquo;When an action is performed\u0026rdquo;, you are being asked to select a Catalog and a Category. Personally, I had no idea what they meant, so I just selected \u0026ldquo;ALL\u0026rdquo; which then allowed me to select my Custom API message. Finally I decided to dig deeper to find out more about what those Catalogs and Categories actually are.\nMicrosoft covers this topic to a solid degree in these two KB articles so I recommend going through them before continuing:\nMicrosoft Dataverse business events - Power Apps | Microsoft Learn Catalog and Catalog Assignment tables (Microsoft Dataverse) - Power Apps | Microsoft Learn Don\u0026rsquo;t get confused by this article - Catalog in Power Platform. That is a different kind of Catalog, which relates to distribution of packages in your organization directly from Power Platform.\nWhat are the Catalogs? # So what are the Business Events Catalogs actually?\nFirst, they don\u0026rsquo;t bring any new functionality to the Dataverse event-based framework. Their sole purpose is to use a hierarchical structure to organize, name, and describe important business events in the form of custom messages (custom APIs), tables, custom actions and improve their visibility for external consumers or internal implementation team.\nSecond, even though this doesn\u0026rsquo;t bring any direct technical innovation, in my opinion, this area is very important and should not be underestimated in any mid-to-large scale implementation. Having documented sets of important business events directly inside the system has multiple benefits, for example:\nSingle place of truth for business-oriented events documentation Promotes discoverability and knowledge-sharing Events are abstracted from actual database transactions if utilizing custom messages I am not saying that the Dataverse catalogs should be the only place to document important business events, but it is a good starting point if you don\u0026rsquo;t have anything.\nCatalog hierarchy # How to create catalog components is well described in the official docs so I won\u0026rsquo;t go into much detail here.\nThe important thing to note is that the catalog hierarchy is enforced to be two-levels. First, you need a root-level Catalog. You can have multiple root-level Catalogs in your environment. Then you need a second-level Catalog (category) where you specify the root-level Catalog as parent. This is the maximum depth you can go with catalogs, as we are unable to go deeper in the hierarchy.\nWhen you have a root + second-level catalogs ready, you can create catalog assignments. Each catalog assignment points to one of three object types: a table (entity), a custom API, or a custom process action.\nYou assign it to a second-level catalog (category), and from that point it becomes visible and selectable in the \u0026ldquo;When an action is performed\u0026rdquo; trigger in Power Automate under your custom categorization.\nNaming the events # Based on the docs, Microsoft\u0026rsquo;s own recommendation is to utilize the On prefix for custom business events.\nIf your custom action is intended only for subscribers to respond to, we recommend that the name of your custom action begin with On\nThis is very important to distinguish pure business event messages vs custom messages tied to a specific operation, usually synchronous, with logic (it does something directly when called).\nOrganizing the events # Microsoft gives some great tips in their docs for designing events. In general, it is recommended to drive the categorization as business-oriented and avoid technical-oriented categorization if possible. The main point of catalogs is about discoverability for subscribers, and consumers usually (should) think in terms of business processes, not concrete database tables.\nHere are some of my thoughts on how the events could be organized based on your circumstances:\n1. Group by business domain # Categories map to how the business thinks about the solution, not to the underlying data model. This would be my recommended default approach as it forces the developers/makers to think more in the business context.\nProject Management ├── Project Lifecycle │ OnProjectApproved, OnProjectKickedOff, OnProjectCompleted, OnProjectCancelled ├── Resource Management │ OnResourceAssigned, OnResourceReleased, OnCapacityExceeded └── Budget \u0026amp; Finance OnBudgetApproved, OnExpenseSubmitted, OnBudgetThresholdReached 2. Group by audience # When you have distinct consumer groups you can organize categories around who is expected to subscribe.\nProject Management ├── External Integration Events │ OnTimesheetApproved, OnInvoiceGenerated, OnMilestoneCompleted └── Internal Automation OnTaskOverdue, OnApprovalPending, OnRiskEscalated 3. Group by event origin # When your solution handles events from both internal Dataverse processes and external systems, you can organize categories around where the events originate. This aligns with Microsoft\u0026rsquo;s external events pattern where custom APIs for external events have specific settings (no main operation plugin, async-only processing steps, no response properties).\nProject Management ├── Internal Events │ OnProjectApproved, OnBudgetThresholdReached, OnTaskOverdue └── External Events OnTimesheetSyncCompleted, OnVendorPaymentPosted, OnInvoiceGenerated Tables and bound custom messages # Including tables in your catalog is entirely optional in my opinion. Your main goal when cataloging should be identifying important business events, not just data stores.\nIt is also worth noting that custom APIs that are bound to a specific table can be added as catalog assignments directly — not through the default maker portal forms, but via the Web API, if that is something you wish to do.\nThat said, my preference is to keep business events decoupled from database transactions entirely. I would always make custom API messages intended as business events global (not bound to any entity). A global event like OnBudgetThresholdReached clearly communicates that something meaningful happened in the business process. It does not tie the subscriber to a specific table\u0026rsquo;s lifecycle and gives you the freedom to change the underlying data model without breaking the event contract. The event becomes a stable integration point regardless of how the data is stored or structured behind it.\nThe missing pieces # I already mentioned that the catalogs themselves don\u0026rsquo;t bring any new functionality (besides exposing the events in a categorical hierarchy for Power Automate). However, the custom business events, represented by custom API messages, are the most important building blocks of the catalogs and there are a couple of things that I would like Microsoft to add and improve in this area.\nFirst, securing the custom message/event can be done by specifying a Dataverse privilege required to call the message. This is fine but Microsoft to this day still doesn\u0026rsquo;t allow generating custom privileges besides creating custom tables. I wish I didn\u0026rsquo;t have to create a dummy table every time I want to control authorization for my custom message.\nSecond, I would like to have some first-party \u0026ldquo;lightweight\u0026rdquo; component that allows me to declaratively trigger these events. Yes we have Power Automate flows and plugins that handle this, but I would like to have something much simpler (there is never enough abstraction :) ) while keeping separation of concerns (by not putting the action to call the message in multiple flows, or having to write a dedicated .NET plugin or flow for each database transaction + message call). A simple declarative component where you would specify trigger (with filter expression), message to call and some sort of expression to map data into input parameters\nThird, there is no first-party visibility into who publishes and who subscribes to a given business event. Once you have a catalog with dozens of events, the natural questions become: \u0026ldquo;Who is calling this event?\u0026rdquo; and \u0026ldquo;What automations are listening to it?\u0026rdquo; For internally managed automations, this can and should be tracked at the code repository level — your solution source should make it clear which plugins or flows react to which events. But for external parties that either emit events into your environment or subscribe to your events, there is currently no way to document or discover these relationships directly within the platform. A simple overview showing publishers and subscribers per cataloged event would go a long way toward making business event catalogs a complete governance tool, not just an organizational one.\nA note on enterprise messaging # As a side note, relying purely on Dataverse custom messages for integration purposes might not be sufficient in more complex scenarios. Depending on the context, enterprise-grade distributed messaging systems such as Azure Service Bus or Apache Kafka should be considered to handle event routing, delivery guarantees, and scalability requirements. Both approaches can co-exist and benefit from each other — Dataverse business events can serve as the source of truth for what events exist and what they mean, while the actual event delivery to external subscribers is handled by a dedicated messaging infrastructure. This might be a topic for another post.\n","date":"6 April 2026","permalink":"https://www.tompivny.com/posts/working-with-dataverse-business-events-catalogs/","section":"Posts","summary":"\u003cp\u003eWhen you are creating a Power Automate flow with the Dataverse trigger \u0026ldquo;When an action is performed\u0026rdquo;, you are being asked to select a Catalog and a Category. Personally, I had no idea what they meant, so I just selected \u0026ldquo;ALL\u0026rdquo; which then allowed me to select my Custom API message. Finally I decided to dig deeper to find out more about what those Catalogs and Categories actually are.\u003c/p\u003e\n\u003cp\u003eMicrosoft covers this topic to a solid degree in these two KB articles so I recommend going through them before continuing:\u003c/p\u003e","title":"Working with Dataverse business event catalogs"},{"content":"When dealing with advanced security requirements, Modernized Business Units are the go-to feature to use to utilize implicit access across Dataverse records. Essentially, by turning on this environment switch, you are enabling two things:\nDataverse tables with User or Team ownership type also have an owningbusinessunit reference. Without using Modernized Business Units, this reference is always set to the record owner\u0026rsquo;s (user or team) business unit that he belongs to and it cannot be modified. Modernized Business Units allow changing this reference independently of the current record owner. Every business unit in Dataverse will get its own set of Dataverse security roles. In practice, when you enable this feature in an environment, for every existing business unit, Dataverse will generate its own security role to be used in the context of the business unit. Creating a new security role will also create the role for each existing business unit. Creating a new business unit will trigger generation of a new set of security roles. Find out more about Modernized Business Units on MS Learn\nModernized business units security - Power Platform | Microsoft Learn Security concepts in Microsoft Dataverse With that being said, what was not obvious to me from the documentation and community resources is how the Parent: Child Business Unit access level works when used in the context of a business unit other than the user\u0026rsquo;s main business unit.\nThe documentation says: \u0026ldquo;Users can access records in their business unit and all business units subordinate to it. Users with this access automatically have business unit and user access.\u0026rdquo;\nTo me, \u0026ldquo;their business unit\u0026rdquo; implies that this access level should work only in the context of the business unit the user belongs to and not when granted a security role in another business unit somewhere else in the hierarchy. Let\u0026rsquo;s put it to the test.\nLet\u0026rsquo;s imagine this simple scenario.\nA user (Bob) belongs to business unit BU1, which is a direct child of the root BU.\nIn the hierarchy, another set of business units exists, where a specific contact \u0026ldquo;Contact 1\u0026rdquo; is owned by BU3, which is a direct child of BU2.\nBy utilizing Modernized Business Units, the user is assigned to a custom security role Contact reader to a BU2. This security role definition contains a privilege that allows reading contact records with the access level Parent: Child Business Unit\nLet\u0026rsquo;s impersonate as Bob and try to access the Contact 1.\nAnd indeed this scenario works as you can see. The Parent: Child Business Unit access level can be utilized when giving someone access in the BU hierarchy, which is outside the scope of the main user\u0026rsquo;s BU.\nThis shows to me how the inherent Dataverse security model is truly robust. I can imagine that in a lot of scenarios where implementations utilize explicit access methods (sharing, access teams), this would be a better alternative in my opinion, as the scaling options are much better, since we are not polluting the principalobjectaccess which causes performance issues when overused.\n","date":"21 March 2026","permalink":"https://www.tompivny.com/posts/modernized-business-units-how-does-cross-hierarchy-parent-child-business-unit-access-work/","section":"Posts","summary":"\u003cp\u003eWhen dealing with advanced security requirements, \u003ca href=\"https://learn.microsoft.com/en-us/power-platform/admin/modernized-business-units-security\" target=\"_blank\" rel=\"noreferrer\"\u003eModernized Business Units\u003c/a\u003e are the go-to feature to use to utilize implicit access across Dataverse records. Essentially, by turning on this \u003ca href=\"https://learn.microsoft.com/en-us/power-platform/admin/wp-security-cds#enable-the-matrix-data-access-structure\" target=\"_blank\" rel=\"noreferrer\"\u003eenvironment switch\u003c/a\u003e, you are enabling two things:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDataverse tables with User or Team ownership type also have an \u003ccode\u003eowningbusinessunit\u003c/code\u003e reference. Without using Modernized Business Units, this reference is always set to the record owner\u0026rsquo;s (user or team) business unit that he belongs to and it cannot be modified. Modernized Business Units allow changing this reference independently of the current record owner.\u003c/li\u003e\n\u003cli\u003eEvery business unit in Dataverse will get its own set of Dataverse \u003ca href=\"https://learn.microsoft.com/en-us/power-platform/admin/security-roles-privileges#security-roles-for-users\" target=\"_blank\" rel=\"noreferrer\"\u003esecurity roles\u003c/a\u003e. In practice, when you enable this feature in an environment, for every existing business unit, Dataverse will generate its own security role to be used in the context of the business unit. Creating a new security role will also create the role for each existing business unit. Creating a new business unit will trigger generation of a new set of security roles.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFind out more about Modernized Business Units on MS Learn\u003c/p\u003e","title":"Modernized Business Units: How does cross-hierarchy \"Parent: Child Business Unit\" access work?"},{"content":"Custom APIs in Dataverse have been available for a while now, serving as a great pro-code alternative to custom actions. However, they are synchronous by design; calling the custom message exposed by the API executes the plugin logic as a main operation in the execution pipeline. There are scenarios where you may not want to wait for the plugin to complete. I have found two viable solutions to achieve this.\nBackground Operations # Microsoft introduced Background operations back in 2023. To this day, they remain in (public) preview. They offer several nice features out of the box that:\nProactive status monitoring: You can query the background operation table) Callback: It supports a webhook to receive the operation result and an OnBackgroundOperationComplete message that we can subscribe to if we need to react to the operation\u0026rsquo;s completion. Cancellation: Support for on-demand cancellation of an operation. Starting an operation is easily triggered from Power Automate, as the Dataverse connector has a built-in action to create a new background operation.\nRegarding .NET, the SDK doesn\u0026rsquo;t have a request class included so we have to use the OrganizationRequest class.\n//Custom API reuqest (class generated via pac cli modelbuilder or similar tool) var customApiRequest = new beer_AsyncPluginAPIRequest { MyTextParameter = \u0026#34;my text value\u0026#34; }; var backgroundOperationsRequest = new OrganizationRequest(\u0026#34;ExecuteBackgroundOperation\u0026#34;) { Parameters = { {\u0026#34;Request\u0026#34;, customApiRequest} } }; service.Execute(backgroundOperationsRequest); For Web API (as well as .NET SDK), Microsoft provides some valuable examples.\nAre there any downsides to using Background Operations?\nBased on my experience, they are simply unreliable regarding completion timing. I tested them in a Sandbox environment, and occasionally, an operation did not start for tens of minutes or even a couple of hours. While the point of this feature is Fire-and-Forget some guaranteed reliability would be preferable.\nFurthermore, as all my development occurs in Developer environments, background operations there often failed to start entirely, remaining stuck in the Waiting For Resources state indefinitely. Working with them during development is a huge pain at least for me.\nFinally, they have been in preview for a considerable time, so I am not yet convinced they are ready for production use.\nAsynchronous Plugin Steps # I first came across this pattern while debugging a Custom API plugin via the Plugin Profiler (see Andrew Butenko\u0026rsquo;s article). Besides the Custom API, you register a post-operation asynchronous plugin step on the same plugin and message exposed by the Custom API. To do this, ensure you allow adding async custom processing steps to the Custom API message.\nThen you can register an asynchronous plugin step on the same message. When calling the Custom API message, the plugin executes twice: first as a main operation (Stage 30) and then as a post-operation (Stage 40). Of course, we want to avoid executing the logic twice. We must check the current stage from the plugin execution context and skip the logic if we are in the main operation stage of the execution pipeline.\nprotected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext) { var context = localPluginContext.PluginExecutionContext; // Exit if in the main operation stage if (context.Stage == 30) { return; } // Rest of your async plugin logic var myText = context.InputParameters[\u0026#34;MyTextParameter\u0026#34;] as string; localPluginContext.TracingService.Trace($\u0026#34;Async Plugin executed with parameter: {myText}\u0026#34;); } This approach works for Fire-and-Forget scenarios. Because we force the Main Operation (Stage 30) to return immediately, the Dataverse response is sent back to the caller before your asynchronous logic (Stage 40) even runs. This means you cannot return data to the caller using the Custom API\u0026rsquo;s Output Parameters\nInput parameters are passed through the entire execution pipeline so that they can be utilized during the post-operation execution.\nVerdict # Both approaches have pros and cons; choose the one that best fits your needs.\nBackground operations offer useful out-of-the-box functionality regarding status management and are easy to implement. The built-in callback webhook and the OnBackgroundOperationComplete message are, in my opinion, the best features. However, they have been in preview for over two years, are unreliable regarding start times, and simply do not work in Developer environments (at least in my experience).\nAsynchronous plugin steps don\u0026rsquo;t have the out of the box bells and whistles around status management. However, because they utilize the traditional Asynchronous service, they are reliable and start almost immediately, provided your Dataverse instance is not under heavy load.\nKeep in mind that neither case bypasses the 2 minute timeout for plugin execution. For long-running operations, your best options within the Platform are Power Automate flows or Custom Actions. Outside the Platform, Azure Functions are a viable option.\n","date":"31 December 2025","permalink":"https://www.tompivny.com/posts/async-dataverse-custom-apis-execution/","section":"Posts","summary":"\u003cp\u003eCustom APIs in Dataverse have been available for a while now, serving as a great pro-code alternative to \u003ca href=\"https://learn.microsoft.com/en-us/power-apps/developer/data-platform/workflow-custom-actions\" target=\"_blank\" rel=\"noreferrer\"\u003ecustom actions\u003c/a\u003e. However, they are synchronous by design; calling the custom message exposed by the API executes the plugin logic as a main operation in the \u003ca href=\"https://carldesouza.com/dynamics-365-plugin-execution-pipeline/\" target=\"_blank\" rel=\"noreferrer\"\u003eexecution pipeline\u003c/a\u003e. There are scenarios where you may not want to wait for the plugin to complete. I have found two viable solutions to achieve this.\u003c/p\u003e\n\u003ch1 id=\"background-operations\" class=\"relative group\"\u003eBackground Operations \u003cspan class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100\"\u003e\u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\" style=\"text-decoration-line: none !important;\" href=\"#background-operations\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\u003c/span\u003e\u003c/h1\u003e\u003cp\u003eMicrosoft introduced \u003ca href=\"https://learn.microsoft.com/en-us/power-apps/developer/data-platform/background-operations?tabs=sdk\" target=\"_blank\" rel=\"noreferrer\"\u003eBackground operations\u003c/a\u003e back in 2023. To this day, they remain in (public) preview. They offer several nice features out of the box that:\u003c/p\u003e","title":"Async Dataverse Custom APIs execution"},{"content":"In the world of Power Platform ALM, we have largely solved the problem of moving logic and structure. We pack tables, flows, and other components into Solutions, commit them to git, and deploy them via pipelines (at least in my scenario). But there is a glaring gap in this automated chain: Security Assignments.\nWhile the Security Roles themselves travel inside Solutions as root components, the assignments of those roles to Dataverse Teams do not, as they are just data. The same applies to Column Security Profiles.\nFor many, this results in a fragile \u0026ldquo;Source of Truth.\u0026rdquo; Assignments are often managed manually in each environment or are handled via one-off scripts. As the complexity of a project grows—especially in enterprise environments with strict governance—relying on manual intervention or unmanaged data loads becomes a significant risk.\nIn this article, I’m going to tackle the problem of establishing a configuration-driven source of truth for Dataverse security assignments. We will look at why standard tools like the Configuration Migration Tool (CMT) fall short for this specific use case, and I will walk you through a custom solution that solves the problem.\nWhere CMT data packages fall short # The CMT is the de facto standard for importing data into Dataverse as it \u0026ldquo;Upserts\u0026rdquo; data into a target environment. It can also handle creation of many-to-many associations between entities. This sounds like a perfect fit for our scenario as the two assignments (teamroles - security roles to teams and teamprofiles - column security profiles to teams) that I am going to tackle are basically both many-to-many relationships in Dataverse.\n\u0026lt;!-- data.xml generated by CMT--\u0026gt; \u0026lt;entities xmlns:xsd=\u0026#34;http://www.w3.org/2001/XMLSchema\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; timestamp=\u0026#34;2025-12-07T15:24:44.9797180Z\u0026#34;\u0026gt; \u0026lt;entity name=\u0026#34;team\u0026#34; displayname=\u0026#34;Team\u0026#34;\u0026gt; \u0026lt;records\u0026gt; \u0026lt;record id=\u0026#34;cf42b04e-80d3-f011-8543-7c1e527829f3\u0026#34;\u0026gt; \u0026lt;field name=\u0026#34;name\u0026#34; value=\u0026#34;Course Managers\u0026#34; /\u0026gt; \u0026lt;field name=\u0026#34;teamtype\u0026#34; value=\u0026#34;0\u0026#34; /\u0026gt; \u0026lt;field name=\u0026#34;teamid\u0026#34; value=\u0026#34;cf42b04e-80d3-f011-8543-7c1e527829f3\u0026#34; /\u0026gt; \u0026lt;field name=\u0026#34;membershiptype\u0026#34; value=\u0026#34;0\u0026#34; /\u0026gt; \u0026lt;/record\u0026gt; \u0026lt;/records\u0026gt; \u0026lt;m2mrelationships\u0026gt; \u0026lt;!-- teamroles --\u0026gt; \u0026lt;m2mrelationship sourceid=\u0026#34;cf42b04e-80d3-f011-8543-7c1e527829f3\u0026#34; targetentityname=\u0026#34;role\u0026#34; targetentitynameidfield=\u0026#34;roleid\u0026#34; m2mrelationshipname=\u0026#34;teamroles\u0026#34;\u0026gt; \u0026lt;targetids\u0026gt; \u0026lt;targetid\u0026gt;17f74987-c8e1-473f-8369-eb77976502dd\u0026lt;/targetid\u0026gt; \u0026lt;/targetids\u0026gt; \u0026lt;/m2mrelationship\u0026gt; \u0026lt;!-- teamprofiles --\u0026gt; \u0026lt;m2mrelationship sourceid=\u0026#34;cf42b04e-80d3-f011-8543-7c1e527829f3\u0026#34; targetentityname=\u0026#34;fieldsecurityprofile\u0026#34; targetentitynameidfield=\u0026#34;fieldsecurityprofileid\u0026#34; m2mrelationshipname=\u0026#34;teamprofiles\u0026#34;\u0026gt; \u0026lt;targetids\u0026gt; \u0026lt;targetid\u0026gt;fe46bc91-81d3-f011-8543-7c1e527829f3\u0026lt;/targetid\u0026gt; \u0026lt;/targetids\u0026gt; \u0026lt;/m2mrelationship\u0026gt; \u0026lt;/m2mrelationships\u0026gt; \u0026lt;/entity\u0026gt; \u0026lt;/entities\u0026gt; However, if we try to import the data package, we get the following message from CMT during the import:\nProcessing Entity M2M Relations team (threaded), Imported 0 of 1 Failed to Link N:N relationship associate between team (ID:cf42b04e-80d3-f011-8543-7c1e527829f3) and role (ID:00000000-0000-0000-0000-000000000000), Target Record missing. The security role I am trying to associate with the team exists in the target environment, but the CMT import refuses to create the association.\nWhen looking closer at the SOAP messages exchanged between CMT and Dataverse (via Fiddler), I did not find any attempt to resolve the target record as it does for other associations; therefore, it just uses the empty guid when creating the association.\nWhile the Team Column Security Profiles associations are imported fine, the Team Roles are not. Also, even if it worked, the CMT cannot be used to disassociate many-to-many relationships, which in my case is kind of important as I want to have a declarative approach to the security assignments.\nPackage Deployer to the rescue # As I am deploying solutions and data packages via the Package Deployer, I decided to utilize its ability to implement custom logic during the deployment process. At the same time, I wanted to have this logic reusable across different projects and to be as easy to use as possible. Therefore, I decided to develop a small .NET library published as a NuGet package.\nThe main class in the library is PackageExtension, which implements the ImportExtension class (the default base class for all Package Deployer extensions) and adds custom logic/tasks to handle the security assignments.\nIn a Package Deployer project, you would simply inherit the PackageExtension class from the custom library and invoke the custom methods in the PostImport stage of the deployment process.\nusing BeerLike.PackageDeployer; namespace YourPackageNamespace; public class PackageExtension : PackageExtension // inherit from the BeerLike.PackageDeployer library { public override bool AfterPrimaryImport() { SyncTeamRoles(GetImportPackageDataFolderName + \u0026#34;/TeamRoles.json\u0026#34;); SyncTeamCsps(GetImportPackageDataFolderName + \u0026#34;/TeamCsps.json\u0026#34;); return true; } } The declarative configurations are stored in the Package Assets folder (default is PkgAssets) of the package as JSON files:\n// TeamRoles.json [ { \u0026#34;team\u0026#34;:\u0026#34;Course Managers\u0026#34;, // team name or id \u0026#34;removeUnassigned\u0026#34;: true, \u0026#34;securityRoles\u0026#34;: [ \u0026#34;17f74987-c8e1-473f-8369-eb77976502dd\u0026#34; ] } ] On each deployment, the logic to check and sync the security assignments is executed according to the configuration in the JSON files. This ensures that the security assignments (teamroles and teamprofiles) are managed in a declarative way and are versioned in a git repository.\nThere is a bit more to it, but the details on how to set this up are all in the README of the repository.\nOverall approach # My approach is to have everything versioned in a git repository as the source of truth and then deploy the parts:\nThe Security Roles and Column Security Profiles are root components and, therefore, are normally deployed as part of the Solution (Solutions are deployed via the Package Deployer). The Dataverse Teams are records and therefore are part of a CMT data package (Data packages are deployed via the Package Deployer). The associations between the Security Roles and Teams and between the Column Security Profiles and Teams are handled via a custom solution that extends the Package Deployer as explained above. ","date":"7 December 2025","permalink":"https://www.tompivny.com/posts/the-source-of-truth-problem-of-dataverse-security-assignments/","section":"Posts","summary":"\u003cp\u003eIn the world of Power Platform ALM, we have largely solved the problem of moving logic and structure. We pack tables, flows, and other components into Solutions, commit them to git, and deploy them via pipelines (at least in my scenario). But there is a glaring gap in this automated chain: Security Assignments.\u003c/p\u003e\n\u003cp\u003eWhile the Security Roles themselves travel inside Solutions as root components, the assignments of those roles to Dataverse Teams do not, as they are just data. The same applies to Column Security Profiles.\u003c/p\u003e","title":"The \"Source of Truth\" Problem of Dataverse Security Assignments"},{"content":"Recently, I\u0026rsquo;ve been finally diving into PCF (Power Apps Component Framework) development. One of the first controls I\u0026rsquo;ve built was a React checkbox control with tooltips for a multi select option set as I wanted to have a replacement for the ugly combo box that Microsoft provides us as the only default option for this type of attribute.\nTo my surprise, pairing the custom control with client scripts triggered by events on a form was not as straightforward as I expected. Let me share with you my findings.\nMethods for choice(s) controls # The Client API offers quite a few methods to interact with controls on a form. Multi select option sets are no exception (see the docs here).\nMethods such as setVisible or setDisabled are common for almost all controls. These work as expected, as long as your component reacts to the context properties (for example, your control should react to the isControlDisabled property from the context, and enable/disable the control accordingly).\nIssue with choice(s) specific methods # Then we have a group of methods for choice controls that can dynamically change the options offered to the user such as: addOption, removeOption and clearOptions. Unexpectedly, these methods do not work for the custom control as they normally do for the default combo box control from Microsoft (I tried both the formContext and deprecated Xrm.Page objects inside a client form script as well).\nDefault combo box control: Custom control: It seems to me that Microsoft simply forgot to add these methods to the multi select custom control objects. Out of a curiosity, I\u0026rsquo;ve tried to run the same methods on a single select option set PCF and they worked as expected.\nThis behavior appears to be a limitation in Microsoft\u0026rsquo;s implementation.\nWorkaround # We cannot use the choices specific methods directly with the custom multi select PCF controls for now. However, as the methods work fine with the default combo box control, we can take advantage of this to implement a workaround.\nIf we modify the options of the default combo box control using client script methods as shown above, the available options inside the context of the custom control are also updated as long as both controls are bound to the same attribute.\nWe can simply hide the default combo box control on the form and refer to it in the client scripts. This way, we can dynamically modify the options of the custom control.\n// Client script used in the example above. var Sdk = window.Sdk || {}; (function() { /** * Trigger: On form load * @param {Object} executionContext - The execution context of the form. */ this.onLoad = function(executionContext) { this.filterLevel(executionContext); }; /** * Trigger: On form load or on change of the \u0026#34;pivny_firstyearcourse\u0026#34; attribute * Description: Filters the options of the level control based on the value of the first year course attribute. * @param {Object} executionContext - The execution context of the form. */ this.filterLevel = function(executionContext) { const formContext = executionContext.getFormContext(); const firstYearLevels = [ { value: 1000, text: \u0026#34;Undergraduate\u0026#34; } ] const nonFirstYearLevels = [ { value: 1000, text: \u0026#34;Undergraduate\u0026#34; }, { value: 1100, text: \u0026#34;Graduate\u0026#34; }, { value: 1300, text: \u0026#34;Postgraduate\u0026#34; } ] const isFirstYear = formContext.getAttribute(\u0026#34;pivny_firstyearcourse\u0026#34;).getValue(); const levelControl = formContext.getControl(\u0026#34;pivny_level1\u0026#34;); //Default combo box control levelControl.clearOptions(); switch (isFirstYear) { case true: firstYearLevels.forEach(level =\u0026gt; { levelControl.addOption(level); }); break; case false: nonFirstYearLevels.forEach(level =\u0026gt; { levelControl.addOption(level); }); break; default: console.warn(`Unexpected value for isFirstYear: ${isFirstYear}`); break; } }; /** * Trigger: On change of the \u0026#34;pivny_level\u0026#34; attribute * Description: Clears the current values of the \u0026#34;pivny_level\u0026#34; attribute. * @param {Object} executionContext - The execution context of the form. */ this.clearLevelValues = function(executionContext) { const formContext = executionContext.getFormContext(); formContext.getAttribute(\u0026#34;pivny_level\u0026#34;).setValue(); } }).call(Sdk); Conclusion # PCFs are an excellent way to enhance the user experience in Power Apps (particularly Model Driven Apps), in scenarios where Microsoft\u0026rsquo;s out-of-the-box offerings fall short. However, when implementing custom controls for multi select option sets, be cautious as some Client API methods triggered by form events don\u0026rsquo;t function as expected - it appears these capabilities were overlooked in the implementation. This post demonstrates a straightforward workaround to address this limitation, by interacting with the default combo box control from Microsoft. The other workaround that comes to my mind is to implement own event handlers that would be bound to the window.parent object, but that requires a bit more work to be done.\nFor anyone interested in the checkbox control displayed in this article, I\u0026rsquo;ve shared the repo on GitHub.\n","date":"2 February 2025","permalink":"https://www.tompivny.com/posts/multi-select-option-set-pcfs-and-client-scripts/","section":"Posts","summary":"\u003cp\u003eRecently, I\u0026rsquo;ve been finally diving into \u003ca href=\"https://learn.microsoft.com/en-us/power-apps/developer/component-framework/overview\" target=\"_blank\" rel=\"noreferrer\"\u003ePCF (Power Apps Component Framework)\u003c/a\u003e development. One of the first controls I\u0026rsquo;ve built was a \u003ca href=\"https://learn.microsoft.com/en-us/power-apps/developer/component-framework/react-controls-platform-libraries\" target=\"_blank\" rel=\"noreferrer\"\u003eReact\u003c/a\u003e checkbox control with tooltips for a multi select option set as I wanted to have a replacement for the ugly combo box that Microsoft provides us as the only default option for this type of attribute.\u003c/p\u003e\n\u003cp\u003eTo my surprise, pairing the custom control with client scripts triggered by events on a form was not as straightforward as I expected. Let me share with you my findings.\u003c/p\u003e","title":"Multi Select Option Set PCFs and Client Scripts"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/pcf/","section":"Tags","summary":"","title":"PCF"},{"content":"For SME customers, Power BI licensing costs can be a significant barrier when distributing reports to stakeholders who don\u0026rsquo;t have licenses or the dedicated Premium / Fabric capacity. I previously worked with a customer who needed a highly cost-effective solution for sharing Power BI reports. We developed an automated system that efficiently distributes Power BI reports as static PDFs while minimizing licensing expenses.\nIn this post, I want to share the solution as a pattern, as it might be useful for others to solve a similar problem.\nNOTE: This is not an end-to-end how-to guide, but more of a pattern that can be implemented in different scenarios (although I included some examples of my implementation to give you an idea).\nBefore we get into the details, let\u0026rsquo;s take a look at the available options, as there is no one-size-fits-all solution, and there might be existing solutions that fit your needs and requirements.\nOptions for distributing Power BI reports # While there are several options to distribute Power BI reports, they all come with either licensing costs or other drawbacks, depending on the scenario:\nStandard sharing - Probably the most straightforward out-of-the-box solution. You have great flexibility in terms of sharing options when it comes to utilizing role-based security (Entra ID security groups, roles, etc.). Publish to web - Works great just with Pro or Premium per user license assigned to the publisher if you want to make the report publicly available (not recommended for any internal or sensitive data). There are also quite a few limitations. Publish to a third-party app - A bit of a niche scenario, but definitely worth mentioning. It\u0026rsquo;s feasible to embed reports using Power BI Embedded capacity in almost any web application that we build. Export of static reports - The obvious drawback in this case is the loss of interactivity of the report. When it comes to formats, we have a few options (depending on whether we are working with standard PBI reports or paginated reports). If we want to automate the export, the PBI Rest API is the way to go. The catch is that the workspace with the report needs to be assigned a Power BI Premium / Fabric capacity, otherwise, you end up with an error. NOTE: I didn\u0026rsquo;t include the method of sharing PBIX files as I consider it a bad practice for a number of reasons when it comes to sharing data with stakeholders.\nThe choice of the option depends on the requirements and available licenses in the organization. In our scenario, we decided to go with the fourth option (exporting static reports), despite not having any dedicated Premium / Fabric capacity available and no chance to get one in the short term.\nThe enabler # Power BI Premium / Fabric capacities are not the only options. The enabler in our case is Power BI Embedded (yes, the same SKU that is used for embedding reports into other applications). This SKU is primarily designed for embedding Power BI reports into other applications, but its feature parity with regular Premium capacity allows us to utilize the REST API for exporting reports to files.\nWith the lowest tier of the PBI Embedded SKU, we managed to cover around 2000 export tasks for a cost of around $8 for the capacity. These 2000 tasks were generated in various batch sizes over a period of an entire year. Just for comparison, the P1 SKU costs around $5000 per month.\nThe pattern # Now that we\u0026rsquo;ve established the cost enabler, let\u0026rsquo;s take a look at the pattern itself.\nThe main idea is to utilize the PBI Embedded capacity only for the necessary time to run the export tasks and then switch it off, as we don\u0026rsquo;t need it for anything else. Let\u0026rsquo;s break it down into a few steps:\nStep 0: Prerequisites # These are the necessary components that we need to have in place to implement the solution:\nA PBI workspace with the published report Provisioned PBI Embedded capacity in Azure (switched off, so there are no unnecessary costs) Some kind of an orchestration tool that would allow sending the HTTP requests to Azure and the PBI service (Power Automate, Azure Automation, Azure Functions, etc.). This pretty much depends on your scenario and existing tech stack. Step 1: Switch on the PBI Embedded capacity # The PBI Embedded is an Azure resource, which can be switched on and off on demand. In our case, we\u0026rsquo;ve used an Azure Automation PowerShell runbook, so we could utilize its 500 minutes of job time included in the free tier and it\u0026rsquo;s very simple to implement.\nparam( [Parameter(Mandatory = $true)] [string]$WorkspaceId, [Parameter(Mandatory = $true)] [string]$ResourceGroupName, [Parameter(Mandatory = $true)] [string]$AutomationAccountName, [Parameter(Mandatory = $true)] [string]$SubscriptionName ) # Getting Automation account credentials $Credential = Get-AutomationPSCredential -Name $AutomationAccountName Update-AzConfig -EnableLoginByWam $false # Starting PowerBI Embedded capacity Connect-AzAccount -Credential $Credential Select-AzSubscription -SubscriptionName $SubscriptionName Resume-AzPowerBIEmbeddedCapacity -Name $PBIEmbeddedName -ResourceGroupName $ResourceGroupName -PassThru NOTE: This is a simplified implementation that utilizes authentication without MFA or service principal. I highly recommend using a service principal such as app registration or managed identity if supported. It\u0026rsquo;s also very likely that Microsoft will enforce MFA for the Az module in the near future (early 2025).\nStep 2: Apply the PBI Embedded capacity to the workspace # Once the capacity is started, we need to apply it to the workspace. Again, we\u0026rsquo;ve used an Azure Automation runbook with the MicrosoftPowerBIMgmt module to achieve that (it\u0026rsquo;s likely that this part can be easily combined with the previous step as we want to apply the capacity right after it\u0026rsquo;s started).\nparam( [Parameter(Mandatory = $true)] [string]$WorkspaceId, [Parameter(Mandatory = $true)] [string]$AutomationAccountName, [Parameter(Mandatory = $true)] [string]$PBIEmbeddedCapacityId ) $Credential = Get-AutomationPSCredential -Name $AutomationAccountName Connect-PowerBIServiceAccount -Credential $Credential $body = \u0026#34;{\u0026#39;capacityId\u0026#39;: $PBIEmbeddedCapacityId}\u0026#34; Invoke-PowerBIRestMethod -Url \u0026#34;https://api.powerbi.com/v1.0/myorg/groups/${WorkspaceId}/AssignToCapacity\u0026#34; -Method POST -Body $body Step 3: Export the report to a file # Now that the workspace is assigned to the capacity, we are able to export the report. The tech-agnostic way to do that is to use the PBI Rest API. In my scenario, we were planning to distribute the reports using a combination of email attachments and SharePoint Online, so we\u0026rsquo;ve decided to simplify and do the export job directly in Power Automate as the distribution would be done also via PA. Microsoft provides some helpful examples.\nThe export options are very flexible, so you can pass various filters, RLS, specify which page to export, etc.\nStep 4: Switch off the PBI Embedded capacity # The export job is done, so we can switch off the PBI Embedded capacity using the Suspend-AzPowerBIEmbeddedCapacity cmdlet.\nparam( [Parameter(Mandatory = $true)] [string]$WorkspaceId, [Parameter(Mandatory = $true)] [string]$ResourceGroupName, [Parameter(Mandatory = $true)] [string]$AutomationAccountName, [Parameter(Mandatory = $true)] [string]$SubscriptionName ) # Getting Automation account credentials $Credential = Get-AutomationPSCredential -Name $AutomationAccountName Update-AzConfig -EnableLoginByWam $false # Suspending PowerBI Embedded capacity Connect-AzAccount -Credential $Credential Select-AzSubscription -SubscriptionName $SubscriptionName Suspend-AzPowerBIEmbeddedCapacity -Name $PBIEmbeddedName -ResourceGroupName $ResourceGroupName -PassThru Step 5: Remove the PBI Embedded capacity from the workspace # The GUID 00000000-0000-0000-0000-000000000000 changes the workspace type back to the Pro license mode, as the workspace is normally used by other users with Pro licenses.\nparam( [Parameter(Mandatory = $true)] [string]$WorkspaceId, [Parameter(Mandatory = $true)] [string]$AutomationAccountName ) $Credential = Get-AutomationPSCredential -Name $AutomationAccountName Connect-PowerBIServiceAccount -Credential $Credential $body = \u0026#34;{\u0026#39;capacityId\u0026#39;: \u0026#39;00000000-0000-0000-0000-000000000000\u0026#39;}\u0026#34; Invoke-PowerBIRestMethod -Url \u0026#34;https://api.powerbi.com/v1.0/myorg/groups/${WorkspaceId}/AssignToCapacity\u0026#34; -Method POST -Body $body Step 6: Share the report with stakeholders # The last step is to share the report with stakeholders/users. In our scenario, we\u0026rsquo;ve already exported the report in Power Automate, so we just need to attach it to an email and send it out (as demonstrated in the example by Microsoft) or upload it to a SharePoint Library in the same workflow.\nTips # Dedicated report(s) for export # I\u0026rsquo;ve already mentioned that the export of the report is lossy, as it doesn\u0026rsquo;t include any interactivity of the original report. I recommend creating a dedicated report just for the export purposes, which would be formatted for the best possible output.\nPaginated reports # These are great for this scenario, especially when it comes to exporting table visuals, as they can automatically span to multiple pages to fit the data.\nMerging multiple pages into a single PDF # In our scenario, we were exporting pages from multiple reports (combination of paginated and standard reports) and then merging them into a single PDF file. You have a few options, but if your volume is not too high (e.g., less than 500 reports per month), you can, for example, utilize the free tier of the Adobe PDF Services (also existing as a Power Automate connector).\nFocus on the PBI Embedded capacity management # What I mean by that is that you should have proper error handling in place, as you don\u0026rsquo;t want failed export tasks (which will occasionally happen) to leave the capacity running. Additionally, depending on your scenario, you might want to implement logic to prevent accidentally switching off the capacity during overlapping export batches/jobs.\nWhen to use this pattern # To summarize, this pattern is not a silver bullet and should be used in scenarios where:\nNo dedicated Power BI Premium / Fabric capacity is available No other use cases would benefit from Premium / Fabric capacity Interactive features of Power BI reports can be omitted Reports need to be distributed to stakeholders who don\u0026rsquo;t have user accounts (either internal or guest) in your M365 tenant or don\u0026rsquo;t have Power BI Pro licenses There is a predictable cadence of report distribution ","date":"11 November 2024","permalink":"https://www.tompivny.com/posts/automating-power-bi-report-distribution-for-smes-a-cost-efficient-pattern/","section":"Posts","summary":"\u003cp\u003eFor SME customers, Power BI licensing costs can be a significant barrier when distributing reports to stakeholders who don\u0026rsquo;t have licenses or the dedicated Premium / Fabric capacity. I previously worked with a customer who needed a highly cost-effective solution for sharing Power BI reports. We developed an automated system that efficiently distributes Power BI reports as static PDFs while minimizing licensing expenses.\u003c/p\u003e\n\u003cp\u003eIn this post, I want to share the solution as a pattern, as it might be useful for others to solve a similar problem.\u003c/p\u003e","title":"Automating Power BI Report Distribution for SMEs: A Cost-Efficient Pattern"},{"content":"","date":null,"permalink":"https://www.tompivny.com/tags/power-bi/","section":"Tags","summary":"","title":"Power BI"},{"content":"","date":null,"permalink":"https://www.tompivny.com/categories/","section":"Categories","summary":"","title":"Categories"}]