<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-5285970135510371565</id><updated>2026-10-19T08:00:00.125-04:00</updated><category term="Events"/><category term="Article"/><category term="Troubleshooting"/><category term="Functionality"/><category term="Around the Blogosphere"/><category term="Code"/><category term="Dexterity"/><category term="Installation"/><category term="SQL Scripting"/><category term="Convergence"/><category term="Web Client"/><category term="Best Practices"/><category term="GPUG"/><category term="SQL Server"/><category term="Training"/><category term="Technical Conference"/><category term="Dynamics GP 12"/><category term="Dynamics GP 2013"/><category term="Integration"/><category term="VBA"/><category term="Roadmap"/><category term="Sales Order Processing"/><category term="Architecture"/><category term="Dynamics GP 2010"/><category term="Support Debugging Tool"/><category term="Visual Studio Tools"/><category term="Integration Manager"/><category term="PowerApps"/><category term="Reporting"/><category term="Compliance"/><category term="Azure"/><category term="Dynamics GP 2015"/><category term="PowerAddicts"/><category term="Security"/><category term="Inventory"/><category term="Modifier"/><category term="Dynamics GP 2010 R2"/><category term="General Ledger"/><category term="Power Automate"/><category term="Power Platform"/><category term="Purchasing"/><category term="MVPs"/><category term="Report Writer"/><category term="C#"/><category term="Dynamics GP 11"/><category term="Payables Management"/><category term="Microsoft Office"/><category term="SmartList and SmartList Builder"/><category term="Amplify"/><category term="Dynamics GP 2016"/><category term="Flow"/><category term="Payroll"/><category term="Word Templates"/><category term="Dynamics GP 2018"/><category term="Receivables Management"/><category term="Upgrades"/><category term="DevOps"/><category term="SQL Reporting Services"/><category term="VBScript"/><category term="GPPC"/><category term="DEX.INI"/><category term="DocWatch"/><category term="From the Newsgroups"/><category term="Power Apps"/><category term="System Manager"/><category term="Business Central"/><category term="Extender"/><category term="PowerPlatform"/><category term="VB.NET"/><category term="Windows 8"/><category term="eConnect"/><category term="Home Page"/><category term="Copilot Studio"/><category term="Field Services"/><category term="Season Greetings"/><category term="Service Architecture"/><category term="The Technology Corner"/><category term="Visual Studio 2008"/><category term="Visual Studio Team Services"/><category term="Community"/><category term="Human Resources"/><category term="Management Reporter"/><category term="Workflow"/><category term="Business Analyzer"/><category term="COM"/><category term="Corporate Performance Management"/><category term="Decisions Conference"/><category term="Fixed Assets"/><category term="Maintenance"/><category term="OLE Container"/><category term="Power BI"/><category term="SharePoint"/><category term="Source Code Control"/><category term="reimagine"/><category term="ALM"/><category term="CRM"/><category term="Macros"/><category term="Navigation Pane"/><category term="PowerShell"/><category term="Product Feedback"/><category term="Tax Updates"/><category term="Web Services"/><category term="Intelligent Edge"/><category term="Internet Explorer"/><category term="Project Accounting"/><category term="Bank Reconciliation"/><category term="Best of Series"/><category term="Books"/><category term="FRx"/><category term="Humor"/><category term="MVP Summit"/><category term="Named Printers"/><category term="Table Import"/><category term="Visual Studio"/><category term="Windows 10"/><category term="AI Builder"/><category term="Ad Campaigns"/><category term="Cognitive Services"/><category term="Dexterity Shared Components"/><category term="Dynamics 365"/><category term="Dynamics NAV"/><category term="EFT"/><category term="Electronic Banking"/><category term="Encumbrance"/><category term="Line Sequence Number"/><category term="MOSS"/><category term="Manufacturing"/><category term="Mixed Reality"/><category term="Office Web Components"/><category term="UI flows"/><category term="Windows 7"/><category term="Analysis Cubes"/><category term="Analytical Accounting"/><category term="Automated Solutions"/><category term="Business Alerts"/><category term="Database Maintenance Utility"/><category term="Docker"/><category term="Document Attach"/><category term="Fargo Floods"/><category term="Internet Information Services"/><category term="MCP"/><category term="Menus for Visual Studio Tools"/><category term="Multicurrency Management"/><category term="Office 365"/><category term="Online Services"/><category term="Power Virtual Agents"/><category term="Printer Compatibility"/><category term="Silverlight"/><category term="Software Development Kits"/><category term="XBox"/><category term="AL language"/><category term="Augmented Reality"/><category term="Bing Maps Enterprise"/><category term="Bots"/><category term="Chrome"/><category term="Connect"/><category term="Continuum"/><category term="DEXSQL"/><category term="Dataverse"/><category term="Drill-Down Builder"/><category term="Flic"/><category term="GP Power Tools"/><category term="IMHO"/><category term="Kinnect"/><category term="Newsgroups"/><category term="Notes"/><category term="OpenXML"/><category term="Performance"/><category term="PowerPlatform PowerAddicts"/><category term="Professional Services Tool Library"/><category term="Robotic Process Automation"/><category term="SafePay"/><category term="Service Call Management"/><category term="Team Foundation Server"/><category term="Telemetry"/><category term="Translation"/><category term="VAT"/><category term="VS Code"/><category term="Windows Server"/><category term="ADO"/><category term="AI"/><category term="Accounts Rollup"/><category term="Adobe Acrobat"/><category term="Agents"/><category term="Application Insights"/><category term="Azure Application Insights"/><category term="Azure Key Vault"/><category term="Best of 2009"/><category term="Business Portal"/><category term="COVID19"/><category term="Certificates"/><category term="Check Links"/><category term="Claude Code"/><category term="Clear Data"/><category term="Common Data Services"/><category term="Computer Vision"/><category term="Coronavirus"/><category term="DEX_ROW_ID"/><category term="DUOS"/><category term="Deferrals"/><category term="Discussions"/><category term="Dynamics GP"/><category term="Dynamics GP   14"/><category term="Dynamics GP 10"/><category term="Dynamics GP Russian"/><category term="Flexicoder"/><category term="FlowFam"/><category term="Forecaster"/><category term="Form Processing"/><category term="GP PowerShell"/><category term="GitHub"/><category term="HTML 5"/><category term="Hololense"/><category term="IoT"/><category term="LINQ to SQL"/><category term="Mentor"/><category term="Microsoft Band"/><category term="Microsoft Edge"/><category term="Microsoft Gadgets"/><category term="Multitenancy"/><category term="Next Document Number"/><category term="ODBC"/><category term="Open Source"/><category term="PO Commitments"/><category term="Partner Connections"/><category term="Process Server"/><category term="Project Madeira"/><category term="RPA"/><category term="RemoteApp"/><category term="Requisitions"/><category term="SmartLists"/><category term="SmartLists Designer"/><category term="Splash Screen"/><category term="Surface"/><category term="Task Scheduler"/><category term="Text Analytics"/><category term="The Partner Event"/><category term="Twilio"/><category term="Use Tax"/><category term="Virtual Earth"/><category term="Virtualization"/><category term="Web API"/><category term="XBRL"/><category term="pac CLI"/><title type='text'>The Workbench</title><subtitle type='html'>Hands-on with AI, Copilot Studio, and the Power Platform</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>1109</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-5647093423069685251</id><published>2026-10-19T08:00:00.000-04:00</published><updated>2026-10-19T08:00:00.120-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><title type='text'>Copilot Studio | The Skill That Isn&#39;t There</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;The skill that isn&#39;t there: Copilot Studio skips a skill that fails a load-time check, so check it before you share it&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-23-copilot-studio-skill-preflight.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Put an unquoted colon in the description of a Copilot Studio skill and nothing will tell you. Copilot Studio skips the skill, loads the rest of the agent, and the agent carries on as though the skill had never been installed. That isn&#39;t a bug report, by the way: it is documented behavior, spelled out in a troubleshooting table that lists eight reasons a skill gets skipped.&lt;/p&gt;

&lt;p&gt;For most of the feature&#39;s life, that was a problem for whoever uploaded the file. The October 8, 2026 revision of the skills documentation changed the audience. Skills can now be shared through a marketplace (preview), and once a skill is something other people add to their own agents, a failure that makes no noise deserves a check that makes plenty.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;A skill, in Copilot Studio&#39;s sense, is a set of instructions written in a Markdown file named &lt;code&gt;SKILL.md&lt;/code&gt;. At the top of that file sits a small block of YAML front matter, which is simply a few &lt;code&gt;key: value&lt;/code&gt; lines fenced between two &lt;code&gt;---&lt;/code&gt; lines, carrying the skill&#39;s &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;. A skill can be a single &lt;code&gt;SKILL.md&lt;/code&gt;, or a ZIP package that bundles it with scripts, templates and reference documents. The agent&#39;s orchestrator reads every skill&#39;s description and activates a skill when a user&#39;s request matches it, which is why the description matters as much as the instructions. Skills follow the open &lt;a href=&quot;https://agentskills.io/specification&quot;&gt;Agent Skills specification&lt;/a&gt;, which the Copilot Studio &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/skills-overview&quot;&gt;skills overview&lt;/a&gt; links directly, and they are available to agents powered by the GitHub Copilot harness, which bill in Copilot Credits.&lt;/p&gt;

&lt;p&gt;The change is in &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/skills-manage&quot;&gt;Manage and delete skills in an agent (GitHub Copilot)&lt;/a&gt;, which gained two preview sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Share a skill through the marketplace (preview).&lt;/b&gt; You can keep a skill private or share it with specific people or groups in your organization, for use &quot;across Copilot Studio agents and other supported experiences&quot;. If you change a skill you already shared, you share it again to publish the new version.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Browse and consume marketplace skills (preview).&lt;/b&gt; You add a shared skill to your agent from a browse list. The agent gets a &lt;i&gt;snapshot&lt;/i&gt; of the skill while keeping a reference to the original, and when a newer version exists you get an update notification the next time you open the agent, with an update button to take it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same page notes that general plugin support in Copilot Studio isn&#39;t available yet, so this is skills only. And the same page already carried the table I opened with. A skill is skipped for invalid YAML front matter, a missing &lt;code&gt;name&lt;/code&gt;, a missing &lt;code&gt;description&lt;/code&gt;, a description that is too long, a duplicate skill name, a &lt;code&gt;SKILL.md&lt;/code&gt; that isn&#39;t UTF-8 without a byte-order mark, a supporting file that couldn&#39;t be written, or a package that fails structural validation.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;&lt;i&gt;For makers.&lt;/i&gt; A skill you write once can now reach every agent in your team without anyone passing files around, and the snapshot model is a considerate design. Your update never changes somebody else&#39;s agent behind their back; they see the notification and decide when to take it. The flip side is worth knowing on day one: a fix you publish doesn&#39;t reach anyone until they accept it, so &quot;I already fixed that&quot; and &quot;their agent has the fix&quot; are two different statements.&lt;/p&gt;

&lt;p&gt;&lt;i&gt;For professional developers.&lt;/i&gt; A skill is a file, and a file belongs in source control with a check in front of it. That troubleshooting table reads like a linter specification, and every row on it except two can be verified on disk before the skill ever reaches Copilot Studio. The duplicate-name row deserves particular attention now. With one author, two skills sharing a name is a typo. With a marketplace, it is a naming collision between teams who have never met. The docs don&#39;t describe how the marketplace handles two shared skills with the same name landing in one agent, but the loader&#39;s documented answer to a duplicate is to skip one, so I would settle on a naming convention, such as a team prefix, before the first share rather than after.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The demo is a preflight check: run every documented load-time rule against a skill on disk, package it, upload it, share it, then ship an update. Here is the checker against a folder of ten skills, each carrying one planted defect (output trimmed to four of its twelve findings):&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;PS&amp;gt; ./Test-SkillPackage.ps1 -Path ./samples/bad
[FAIL] colon-in-description  FM-YAML  Key &#39;description&#39;: unquoted value contains a colon followed by a space, which is not valid YAML. Quote it, for example description: &quot;...&quot;.
[WARN] hash-in-description  FM-COMMENT  Key &#39;description&#39;: unquoted &#39; #&#39; starts a YAML comment, so everything after it is dropped from the value. Quote it if the # is meant to be text.
[FAIL] long-description  DESC-LENGTH  description is 1100 characters. Copilot Studio skips a skill whose description is too long but doesn&#39;t state the number; the open specification&#39;s maximum is 1024.
[FAIL] lowercase-filename  PKG-SKILLMD  No SKILL.md in the folder. Found &#39;skill.md&#39;; the file name is SKILL.md in capitals.
10 skill(s) checked: 8 FAIL, 4 WARN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every finding carries a check ID, and the split between &lt;code&gt;FAIL&lt;/code&gt; and &lt;code&gt;WARN&lt;/code&gt; is deliberate. &lt;code&gt;FAIL&lt;/code&gt; means the Copilot Studio docs say the skill will not load. &lt;code&gt;WARN&lt;/code&gt; means something else disagrees with the skill (the open specification, or plain YAML) but Copilot Studio&#39;s own docs don&#39;t say it will be skipped. The &lt;code&gt;hash-in-description&lt;/code&gt; case is my favorite of the lot: a description like &lt;i&gt;Looks up a ticket by its number, for example #4521&lt;/i&gt; is perfectly valid YAML, and YAML silently throws away everything from the &lt;code&gt; #&lt;/code&gt; onward because it treats that as a comment. Since the file is valid, nothing stops the skill from loading; it just has half a description, and the orchestrator is matching requests against the half that survived. The script exits with code 1 on any &lt;code&gt;FAIL&lt;/code&gt;, which is what lets it gate a pipeline.&lt;/p&gt;

&lt;p&gt;And here is the front matter of the skill that passes, showing the fix for the most common failure:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
name: expense-policy-check
description: &quot;Checks an expense against company policy: spending limits, receipt rules and approvals. Use when someone asks whether an expense is allowed or what they need to submit it.&quot;
---&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The description contains a colon, so it is wrapped in double quotes, which is exactly the remedy the troubleshooting table gives. The &lt;code&gt;name&lt;/code&gt; uses only lowercase letters, numbers and hyphens, and it matches the folder the skill lives in, which the open specification requires and which keeps the duplicate check meaningful. Notice also that the description says both what the skill does and when to use it. That second half is what the orchestrator matches against, so write it with the words a user would actually type. Change the policy text to your own, keep the shape.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The Copilot Studio paths below come from &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/skills-add-existing&quot;&gt;Add an existing skill to an agent&lt;/a&gt; and the manage article above. You will need an agent powered by the GitHub Copilot harness, and PowerShell 7 for the scripts.&lt;/p&gt;

&lt;p&gt;1) Clone &lt;a href=&quot;https://github.com/dgpblogster/skill-preflight-lab&quot;&gt;skill-preflight-lab&lt;/a&gt; and run &lt;code&gt;./tests/Invoke-Tests.ps1&lt;/code&gt;. All 25 tests should pass, which proves the checker behaves on your machine before you trust it with your own skills.&lt;/p&gt;

&lt;p&gt;2) Run &lt;code&gt;./Test-SkillPackage.ps1 -Path ./samples/good/expense-policy-check&lt;/code&gt;, then point it at your own skill folder. Point it at a folder &lt;i&gt;of&lt;/i&gt; skills to catch duplicate names across them.&lt;/p&gt;

&lt;p&gt;3) Package with &lt;code&gt;./New-SkillPackage.ps1 -SkillPath ./samples/good/expense-policy-check -OutDir ./out&lt;/code&gt;. It refuses to package a skill with any &lt;code&gt;FAIL&lt;/code&gt;, puts &lt;code&gt;SKILL.md&lt;/code&gt; at the root of the ZIP, and re-checks the finished package.&lt;/p&gt;

&lt;p&gt;4) In Copilot Studio, open your agent, select the &lt;b&gt;Build&lt;/b&gt; tab, select &lt;b&gt;Skills&lt;/b&gt; in the components panel, then in &lt;b&gt;Add skill&lt;/b&gt; select &lt;b&gt;Upload a skill&lt;/b&gt;. Drag the ZIP onto the upload box.&lt;/p&gt;

&lt;p&gt;5) On the &lt;b&gt;Preview&lt;/b&gt; tab, ask &quot;Is a 180 USD hotel night within policy?&quot;. The sample policy sets lodging at 250 USD per night, so the answer should lead with &quot;within policy&quot; and then list what to attach, including the hotel folio.&lt;/p&gt;

&lt;p&gt;6) To share it, select the skill to open its configuration panel, select the three dots (&lt;b&gt;...&lt;/b&gt;) and then &lt;b&gt;Share&lt;/b&gt;. On the details page, select &lt;b&gt;Share&lt;/b&gt; in the upper-right corner, choose &lt;b&gt;Specific users or groups in your organization&lt;/b&gt;, add a colleague or a group, and select &lt;b&gt;Apply&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;7) As that colleague, open an agent on the &lt;b&gt;Build&lt;/b&gt; tab, select the &lt;b&gt;+&lt;/b&gt; icon next to &lt;b&gt;Skills&lt;/b&gt;, then &lt;b&gt;Browse skills (preview)&lt;/b&gt;. Select the skill, and on its details page select &lt;b&gt;Add&lt;/b&gt;. If &lt;b&gt;Browse skills (preview)&lt;/b&gt; isn&#39;t there, the docs don&#39;t describe a setting that turns it on, so treat it as not yet available to you.&lt;/p&gt;

&lt;p&gt;8) Now ship an update. Change the lodging limit in &lt;code&gt;references/policy-limits.md&lt;/code&gt;, rerun steps 2 and 3, then in your agent select the skill, the three dots (&lt;b&gt;...&lt;/b&gt;), and &lt;b&gt;Replace&lt;/b&gt;, and pick the new ZIP. Share the skill again, because a changed skill doesn&#39;t republish itself. The next time your colleague opens their agent, they get the update notification. Until they accept it, they are still on the old limit!&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Everything above is in &lt;a href=&quot;https://github.com/dgpblogster/skill-preflight-lab&quot;&gt;skill-preflight-lab&lt;/a&gt; on GitHub: the checker, the packager, two skills that pass, ten that each fail in one documented way, and a 25-test suite that asserts the good ones pass and every planted defect is reported by its ID. The README maps each check ID to its source. Credit where it&#39;s due: the open specification ships its own reference validator, &lt;a href=&quot;https://github.com/agentskills/agentskills/tree/main/skills-ref&quot;&gt;skills-ref&lt;/a&gt;, and if you work in Python you should use it. This checker adds the parts specific to Copilot Studio&#39;s loader, such as encoding, package shape and duplicates across an agent.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;I like the marketplace design a great deal, the opt-in update most of all. But a sharing model raises the price of a quiet failure, and Copilot Studio&#39;s loader is quiet on purpose: skipping one bad skill so the rest of the agent keeps working is the right call for the person chatting, and the wrong signal for the person who wrote it. The fix is the same one we have always used for that kind of gap. The documentation already tells you the rules, so put them in a script, put the script in front of the share button, and let it be the thing that makes noise.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/5647093423069685251/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/5647093423069685251' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5647093423069685251'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5647093423069685251'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/10/copilot-studio-skill-that-isnt-there.html' title='Copilot Studio | The Skill That Isn&#39;t There'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-2559375599455477088</id><published>2026-10-16T08:00:00.000-04:00</published><updated>2026-10-16T08:00:00.120-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><title type='text'>Copilot Studio | The Workflow That Runs Every Time</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;The workflow that runs every time: hooks in Copilot Studio run a workflow on agent lifecycle events&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-22-copilot-studio-hooks.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Copilot Studio agents built on the GitHub Copilot harness can now run one of your workflows before every tool call, and that workflow is allowed to say no. The feature is called &lt;b&gt;hooks&lt;/b&gt;, it arrived in preview at the end of September, and it answers the question a governance reviewer eventually asks about any generative agent: &quot;how do I make sure the agent &lt;i&gt;always&lt;/i&gt; does X, instead of only when it decides X is relevant?&quot;&lt;/p&gt;

&lt;p&gt;Until now the honest answer was that you couldn&#39;t, not completely. You could write better instructions, you could give a tool a sharper description, and you could hope. Hooks replace the hope with a mechanism.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The new article is &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/hooks-overview&quot;&gt;Hooks (GitHub Copilot) (preview)&lt;/a&gt;, last updated September 29, 2026, and it is a brand new page in the agents experience documentation rather than an edit to an existing one. Like everything in that section, it applies to agents powered by the GitHub Copilot harness, which I wrote about on September 21st in &lt;a href=&quot;https://www.theworkbench.blog/2026/09/copilot-studio-agent-your-flow-cant-call.html&quot;&gt;The Agent Your Flow Can&#39;t Call&lt;/a&gt;, and it consumes Copilot Credits under usage-based billing.&lt;/p&gt;

&lt;p&gt;A hook has two parts: an &lt;b&gt;event&lt;/b&gt;, which is a point in the agent&#39;s lifecycle, and an &lt;b&gt;action&lt;/b&gt;, which today is always a workflow. When the event fires, Copilot Studio calls your workflow, passes it the details of what just happened, and reads the workflow&#39;s response back into the conversation. The page puts the difference from a tool in one line that is worth memorizing: a tool runs when the agent judges it relevant, a hook runs every time its event occurs.&lt;/p&gt;

&lt;p&gt;There are six events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Start (pre-loop)&lt;/b&gt;: a new conversation begins. Can return &lt;code&gt;additionalContext&lt;/code&gt;, background text for the whole session.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;User prompt submitted&lt;/b&gt;: before each user message is processed. Can return a &lt;code&gt;modifiedPrompt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Error&lt;/b&gt;: an error outside a tool call. Can return &lt;code&gt;retry&lt;/code&gt;, &lt;code&gt;skip&lt;/code&gt; or &lt;code&gt;abort&lt;/code&gt;, plus the message the user sees.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pre tool use&lt;/b&gt;: before a tool call. Can deny it, rewrite its arguments, or add context.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Post tool use&lt;/b&gt;: after a tool succeeds. Can replace the result the model sees.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;After tool failure&lt;/b&gt;: a tool returned an error. Can add guidance for the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every event also sends three common inputs: &lt;code&gt;event&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, and a &lt;code&gt;metadata&lt;/code&gt; object carrying the conversation ID, the user&#39;s channel ID, their Entra object ID and display name, the channel, and the agent&#39;s ID, name and schema name. The three tool events can be scoped to specific tools, or left empty to apply to all of them.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;&lt;i&gt;For makers.&lt;/i&gt; This is the first time a maker can attach a deterministic rule to a generative agent without restructuring it into topics. A Start hook that looks up the user&#39;s region and open cases gives the agent the same context on every conversation, no matter how the user opens. A Post tool use hook that writes an audit row runs on every call, so &quot;did the agent actually log that?&quot; stops being a question. And you build all of it in the workflow designer you already use for tools.&lt;/p&gt;

&lt;p&gt;&lt;i&gt;For professional developers.&lt;/i&gt; If you have worked with lifecycle hooks in coding agents, the shape will be familiar, and the details deserve a careful read. Two in particular. First, &lt;b&gt;Pre tool use is the only event that can block anything&lt;/b&gt;; every other event can add context or change values, but it cannot stop the agent. Second, and more important, &lt;b&gt;hooks fail open&lt;/b&gt;. The page is explicit that if the workflow fails, times out, or returns something the agent cannot read, the agent continues as though the hook returned nothing, and it advises against relying on a hook as your only safeguard for a business-critical rule. I think that is the right default for a conversational product, and it is also the sentence your security reviewer needs to see before anyone calls a hook a control. Pair it with whatever the connector or the downstream system enforces on its own.&lt;/p&gt;

&lt;p&gt;Something else to keep in mind is that the inputs are untrusted. The &lt;code&gt;prompt&lt;/code&gt;, tool &lt;code&gt;result&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; values can carry content the agent did not produce, and the docs say so on each of them. A Pre tool use workflow that pattern-matches the agent&#39;s arguments is evaluating text a user may have shaped on purpose.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Take an agent with one sensitive tool, say the Office 365 Outlook &lt;b&gt;Send an email&lt;/b&gt; connector action, and attach two hooks. A Pre tool use hook scoped to that tool only denies the call when any recipient is outside your own domain, and tells the model why. A Post tool use hook scoped to all tools writes one audit row per successful call, using the &lt;code&gt;metadata&lt;/code&gt; object to record who asked and which agent acted. Then try to talk the agent into emailing an external address.&lt;/p&gt;

&lt;p&gt;The deny branch of the Pre tool use workflow ends in a &lt;b&gt;Respond to the agent&lt;/b&gt; node that returns these values:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;permissionDecision&quot;: &quot;deny&quot;,
  &quot;permissionDecisionReason&quot;: &quot;External recipients are not allowed from this agent. Offer to draft the message so the user can send it from Outlook.&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every output on every event is optional, and leaving one empty means &quot;carry on with the default&quot;, so the allow branch can simply return nothing at all. The reason text is not a log message: it goes back to the model, which uses it to decide what to tell the user, so write it as an instruction the agent can act on. That is the difference between a refusal that dead-ends the conversation and one that offers the user a way forward.&lt;/p&gt;

&lt;p&gt;The audit workflow needs nothing back. Its interesting part is the mapping into the row it writes:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Audit row column   &amp;lt;-  hook input
-----------------      ----------------------------
Timestamp          &amp;lt;-  timestamp
Agent              &amp;lt;-  metadata.agentName
User (Entra ID)    &amp;lt;-  metadata.userAadObjectId
Channel            &amp;lt;-  metadata.channel
Conversation       &amp;lt;-  metadata.conversationId
Tool               &amp;lt;-  toolName
Arguments          &amp;lt;-  parameters&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Use &lt;code&gt;userAadObjectId&lt;/code&gt; rather than &lt;code&gt;userId&lt;/code&gt; to identify the person: &lt;code&gt;userId&lt;/code&gt; is the user&#39;s ID in the channel they are chatting on, such as Teams, while the object ID is their Entra identity. The docs also warn that every metadata field can be empty, and the user fields always are when nobody is signed in, so give the audit row a fallback value instead of letting the write fail. A failed audit hook does not stop the agent, which is exactly the situation you don&#39;t want to discover from a gap in the table.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The paths below come from the hooks article and from &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/tools-add-workflow&quot;&gt;Add a workflow to your agent as a tool (GitHub Copilot) (preview)&lt;/a&gt;. You will need an agent created in the new agents experience and powered by the GitHub Copilot harness.&lt;/p&gt;

&lt;p&gt;1) Open your agent in &lt;a href=&quot;https://copilotstudio.microsoft.com&quot;&gt;Copilot Studio&lt;/a&gt;. On the &lt;b&gt;Build&lt;/b&gt; tab, in the components panel, select &lt;b&gt;Tools&lt;/b&gt; and add the Office 365 Outlook &lt;b&gt;Send an email&lt;/b&gt; action, so the agent has a tool to guard.&lt;/p&gt;

&lt;p&gt;2) On the top menu bar, select the three dots (&lt;b&gt;...&lt;/b&gt;), and then select &lt;b&gt;Hooks&lt;/b&gt;. Hooks live on the agent&#39;s command bar, not on the Build tab, which is the first thing you&#39;ll go looking for.&lt;/p&gt;

&lt;p&gt;3) In the &lt;b&gt;Hooks&lt;/b&gt; dialog, select &lt;b&gt;Create&lt;/b&gt;. On the &lt;b&gt;Add hook&lt;/b&gt; page, set &lt;b&gt;Event type&lt;/b&gt; to &lt;b&gt;Pre tool use&lt;/b&gt;, accept or change the suggested &lt;b&gt;Name&lt;/b&gt;, and in &lt;b&gt;Tools&lt;/b&gt; select only the email action.&lt;/p&gt;

&lt;p&gt;4) Select &lt;b&gt;New workflow&lt;/b&gt;. The designer opens with a starter workflow that already has a &lt;b&gt;When an agent calls the flow&lt;/b&gt; node carrying the event&#39;s inputs and a &lt;b&gt;Respond to the agent&lt;/b&gt; node carrying its outputs. Do not rename those fields: Copilot Studio uses the names to decide whether a workflow fits the event, and a renamed field drops the workflow from the list.&lt;/p&gt;

&lt;p&gt;5) Add a condition between the two nodes that inspects &lt;code&gt;parameters&lt;/code&gt; for the recipients, and set the two outputs shown above on the deny branch. The article does not document the internal shape of &lt;code&gt;parameters&lt;/code&gt; for a given tool, so run the agent once against an internal address first and read the workflow&#39;s run to see exactly what arrives before you write the condition.&lt;/p&gt;

&lt;p&gt;6) Select the &lt;b&gt;Save&lt;/b&gt; icon, then &lt;b&gt;Publish&lt;/b&gt;. A saved but unpublished workflow does not run. You return to &lt;b&gt;Add hook&lt;/b&gt; with the new workflow selected; select &lt;b&gt;Done&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;7) Repeat steps 2 to 6 for a &lt;b&gt;Post tool use&lt;/b&gt; hook with &lt;b&gt;Tools&lt;/b&gt; left empty (all tools), whose workflow writes the audit row to a Dataverse table or a SharePoint list.&lt;/p&gt;

&lt;p&gt;8) Select the &lt;b&gt;Save&lt;/b&gt; icon on the agent&#39;s top menu bar. Selecting &lt;b&gt;Done&lt;/b&gt; only adds the hook to the draft; it is not stored until the agent is saved, and it does not run for users until you publish the agent.&lt;/p&gt;

&lt;p&gt;9) Test with three prompts (the exact wording and expected outcomes are in the repo&#39;s &lt;a href=&quot;https://github.com/dgpblogster/agent-hooks-lab/blob/main/tests/test-plan.md&quot;&gt;test plan&lt;/a&gt;): an email to a colleague (allowed, audited), an email to an external address (denied, with the agent offering an alternative), and an attempt to argue the agent out of the refusal. The third one is the real test of whether your reason text was written for the model.&lt;/p&gt;

&lt;p&gt;Keep the workflows quick. The hooks article says a hook that times out is treated as though it returned nothing, but it doesn&#39;t give the timeout itself; the documented limit for a workflow used as an agent tool is 100 seconds, so design well under that. A hook that times out is a hook that silently didn&#39;t run.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Workflows themselves live in the environment, so &lt;a href=&quot;https://github.com/dgpblogster/agent-hooks-lab&quot;&gt;agent-hooks-lab&lt;/a&gt; on GitHub is the specification and test harness around them. The &lt;code&gt;README.md&lt;/code&gt; condenses the event reference into one table, the &lt;code&gt;contracts&lt;/code&gt; folder holds the input and output field names for each of the six events as JSON (the single most useful thing to have open while building), the &lt;code&gt;payloads&lt;/code&gt; folder has the deny response and the audit mapping from above, and &lt;code&gt;tests/test-plan.md&lt;/code&gt; has the three prompts with their expected outcomes and what each kind of failure tells you.&lt;/p&gt;

&lt;p&gt;What it does not have yet is an exported solution with the agent and both workflows. That goes in once it has been built and run in a real environment, not before.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;What I find most encouraging about hooks is that they move a category of requirement out of the prompt and into something you can point at. &quot;Always log&quot;, &quot;never email outside the company&quot;, &quot;always know the user&#39;s region before answering&quot; were, until the end of September, instructions you asked a model to follow. Now they are workflows bound to events, visible in a list, editable, and testable.&lt;/p&gt;

&lt;p&gt;Just don&#39;t let &quot;runs every time&quot; turn into &quot;cannot be bypassed&quot; in anybody&#39;s slide deck. A hook is a strong, consistent nudge on the agent&#39;s path, and it fails open by design. Treat it as the layer that makes the agent behave well, keep the hard controls where they have always been, and you&#39;ll get the best of both!&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/2559375599455477088/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/2559375599455477088' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/2559375599455477088'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/2559375599455477088'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/10/copilot-studio-workflow-that-runs-every.html' title='Copilot Studio | The Workflow That Runs Every Time'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-1661691785673000041</id><published>2026-10-13T08:00:00.000-04:00</published><updated>2026-10-13T08:00:00.219-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><title type='text'>Power Apps | The Page That Reports Itself</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;The page that reports itself: generative pages export telemetry to Azure Application Insights with no code&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-21-genpage-appinsights.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;A generative page is React code that an agent wrote on your behalf. Which means that the first time one fails in front of a user, somebody is going to be debugging code they did not write, from a bug report that says &quot;the page is blank&quot;, with no telemetry of any kind to go on. That was the situation until the end of September, when the generative pages documentation picked up a new section that changes it: published generative pages now send their own page loads, crashes and data calls to Azure Application Insights, and neither you nor the agent has to write a single line of telemetry code to make that happen.&lt;/p&gt;

&lt;p&gt;That&#39;s right! No SDK, no connection string, no &lt;code&gt;trackException()&lt;/code&gt; sprinkled through generated code that the next prompt will happily rewrite anyway.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The change landed on September 28, 2026 in two articles at once: &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/generative-pages&quot;&gt;Generate a page using natural language&lt;/a&gt;, which covers the in-browser experience, and &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/generative-page-external-tools&quot;&gt;Create and edit generative pages with AI code generation tools&lt;/a&gt;, which covers building pages from GitHub Copilot CLI and similar tools. Both gained a section titled &lt;i&gt;Monitor a generative page with Application Insights (preview)&lt;/i&gt;, and the second one is explicit that it works the same whether the page came out of the maker portal or out of a coding agent on your laptop.&lt;/p&gt;

&lt;p&gt;For those new to the term, a generative page is a model-driven app page you describe in plain English and an agent produces as React and TypeScript, talking to Dataverse through an object called &lt;code&gt;dataApi&lt;/code&gt;. Application Insights is the Azure Monitor service that stores telemetry in fixed tables you query with Kusto Query Language (KQL).&lt;/p&gt;

&lt;p&gt;The new telemetry maps onto those standard tables as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;b&gt;pageViews&lt;/b&gt;: completed page loads, with the page ID, page name, app, host type, load duration and navigation origin. Navigation started &lt;i&gt;from&lt;/i&gt; a generative page also lands here, with source and target page information.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;exceptions&lt;/b&gt;: unhandled page-rendering errors and unhandled promise rejections attributed to the page.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dependencies&lt;/b&gt;: the duration and result of supported &lt;code&gt;dataApi&lt;/code&gt; operations, meaning create, update, delete, retrieve and query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the third time I have written about Application Insights on this blog, and the three posts make a neat little timeline. Back in August 2021 I covered &lt;a href=&quot;https://www.theworkbench.blog/2021/08/power-apps-application-monitoring-with.html&quot;&gt;monitoring canvas apps with Application Insights&lt;/a&gt;, where the maker pastes an instrumentation key into the app and calls &lt;code&gt;Trace()&lt;/code&gt; by hand. On August 24th of this year I covered &lt;a href=&quot;https://www.theworkbench.blog/2026/08/power-automate-cloud-flow-telemetry.html&quot;&gt;cloud flow telemetry down to the action level&lt;/a&gt;, which is a platform export configured by an administrator. Generative pages take the second road, not the first. That&#39;s always a good thing, and here is why.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;&lt;i&gt;For makers.&lt;/i&gt; You get a straight answer to &quot;is my page working for people?&quot; without learning anything about instrumentation. The catch is that you probably cannot turn it on yourself: the export is configured in the Power Platform admin center by an administrator, and it is supported for &lt;b&gt;Managed Environments only&lt;/b&gt;. In a plain developer environment there is nothing to switch on, and you&#39;ll need a conversation with whoever runs your tenant.&lt;/p&gt;

&lt;p&gt;&lt;i&gt;For professional developers.&lt;/i&gt; The interesting part is the &lt;code&gt;dependencies&lt;/code&gt; table. Every &lt;code&gt;dataApi&lt;/code&gt; call the agent generated is timed and recorded with its result, so &quot;which query is slow&quot; stops being a guess. And because the platform does the instrumenting rather than the page, it survives regeneration. If you have ever added logging to generated code and watched the next prompt remove it, you&#39;ll appreciate that more than anything else here.&lt;/p&gt;

&lt;p&gt;NOTE: this is a preview, and the docs say availability might vary by environment while it rolls out. The platform&#39;s delivery commitment for exported telemetry is 24 hours, so an empty table on the first afternoon is not a failure.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The demo is small on purpose: one generative page, one Application Insights resource, one deliberately broken button, and four queries. The broken button is what makes it convincing, because a page that works only proves that page loads are recorded. To prove crashes are, you have to crash it.&lt;/p&gt;

&lt;p&gt;Once the page is generated, open the &lt;b&gt;Code&lt;/b&gt; tab, select &lt;b&gt;Edit&lt;/b&gt;, and add a button whose click handler throws inside an &lt;code&gt;async&lt;/code&gt; function:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;button
  type=&quot;button&quot;
  onClick={async () =&amp;gt; {
    // Deliberate failure for the telemetry demo. Remove before sharing the page.
    throw new Error(&quot;Telemetry smoke test: unhandled promise rejection&quot;);
  }}
&amp;gt;
  Break me
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Because the handler is &lt;code&gt;async&lt;/code&gt;, the thrown error does not surface as an ordinary exception: it becomes a rejected promise that nothing catches, which is precisely one of the two error classes the docs say are recorded. I used a plain HTML &lt;code&gt;button&lt;/code&gt; rather than whatever component library the agent picked so it pastes cleanly into any generated page. Change the message text to something you can search for, and select &lt;b&gt;Save&lt;/b&gt; to commit the edit as a new iteration.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The admin center path below comes straight from &lt;a href=&quot;https://learn.microsoft.com/en-us/power-platform/admin/set-up-export-application-insights&quot;&gt;Export data to Application Insights&lt;/a&gt;, and the page steps from the generative pages article. Be sure to read the prerequisites first, because each one can stop you cold:&lt;/p&gt;

&lt;p&gt;1) In the Azure portal, create an Application Insights resource dedicated to this environment. The docs warn that the out-of-the-box reports misbehave if one resource receives data from several environments, and that the integration requires &lt;b&gt;local authentication&lt;/b&gt; to remain enabled on the resource. Make sure you hold Contributor or Owner on it.&lt;/p&gt;

&lt;p&gt;2) Confirm that the Power Platform environment is a Managed Environment, and that you hold Power Platform administrator or Dynamics 365 administrator at the tenant level &lt;i&gt;and&lt;/i&gt; System Administrator in the environment itself. Both are required.&lt;/p&gt;

&lt;p&gt;3) Go to the &lt;a href=&quot;https://admin.powerplatform.microsoft.com&quot;&gt;Power Platform admin center&lt;/a&gt; &amp;gt; &lt;b&gt;Manage&lt;/b&gt; &amp;gt; &lt;b&gt;Data export&lt;/b&gt; &amp;gt; &lt;b&gt;App Insights&lt;/b&gt; tab &amp;gt; &lt;b&gt;New data export&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;4) Enter a friendly name and pick the data type. Here is a gap worth knowing about: the generative pages articles do not say which data type carries their telemetry. The documented choices include &lt;b&gt;Dataverse diagnostics and performance&lt;/b&gt;, &lt;b&gt;Power Automate&lt;/b&gt; and &lt;b&gt;Copilot Studio&lt;/b&gt;, and model-driven app telemetry is documented under the Dataverse diagnostics feed, so that is the one I would select. Treat that as my inference until the docs say so.&lt;/p&gt;

&lt;p&gt;5) Choose your filters and the source environment, select &lt;b&gt;Next&lt;/b&gt;, choose the subscription, resource group and Application Insights resource, select &lt;b&gt;Next&lt;/b&gt;, then &lt;b&gt;Create&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;6) Go to &lt;a href=&quot;https://make.powerapps.com&quot;&gt;make.powerapps.com&lt;/a&gt;, open a model-driven app for editing, and in the app designer select &lt;b&gt;Add page&lt;/b&gt; &amp;gt; &lt;b&gt;Generative page&lt;/b&gt; &amp;gt; &lt;b&gt;Describe a page&lt;/b&gt;. The in-browser experience requires an environment in the United States, Great Britain, Australia or Singapore; elsewhere, use the AI code generation tools route instead.&lt;/p&gt;

&lt;p&gt;7) Describe the page (the &lt;a href=&quot;https://github.com/dgpblogster/genpage-telemetry-lab/blob/main/page/generation-prompt.md&quot;&gt;companion repo has an example prompt&lt;/a&gt; if you would rather not write one), select &lt;b&gt;Add data&lt;/b&gt; &amp;gt; &lt;b&gt;Add table&lt;/b&gt; to link a Dataverse table (Account is fine), then select &lt;b&gt;Generate page&lt;/b&gt;. Add the broken button from the previous section on the &lt;b&gt;Code&lt;/b&gt; tab.&lt;/p&gt;

&lt;p&gt;8) Select &lt;b&gt;Save and Publish&lt;/b&gt;. Only published pages send telemetry, so a page you have only previewed in the designer will stay silent.&lt;/p&gt;

&lt;p&gt;9) Open the app as a normal user, load the page several times, page through some records, and click &lt;b&gt;Break me&lt;/b&gt; twice. Then wait, up to a day the first time.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The queries&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Start by looking at what actually arrives, before writing anything clever. The documentation names the information each row carries but not the column or &lt;code&gt;customDimensions&lt;/code&gt; key each value lives in, so the first query is reconnaissance:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-kusto&quot;&gt;union pageViews, exceptions, dependencies
| where timestamp &amp;gt; ago(2d)
| where cloud_RoleInstance == &quot;CDS Data Export&quot;
| project timestamp, itemType, name, duration, success, operation_Id, session_Id, customDimensions
| order by timestamp desc
| take 50&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;cloud_RoleInstance&lt;/code&gt; filter is the documented marker for rows written by the Power Platform export, which keeps anything else your resource collects out of the picture. Read the &lt;code&gt;customDimensions&lt;/code&gt; column carefully on a few &lt;code&gt;pageViews&lt;/code&gt; rows: that is where you will learn the actual key names for page ID and page name, and you will need them for anything you build on top. If the filter returns nothing while the unfiltered version returns your page, drop it and note the value you see instead.&lt;/p&gt;

&lt;p&gt;Next, page load performance per page:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-kusto&quot;&gt;pageViews
| where timestamp &amp;gt; ago(7d)
| where cloud_RoleInstance == &quot;CDS Data Export&quot;
| summarize loads = count(), p50 = percentile(duration, 50), p95 = percentile(duration, 95) by name
| order by p95 desc&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This groups by the standard &lt;code&gt;name&lt;/code&gt; column. If the reconnaissance query showed the page name living in &lt;code&gt;customDimensions&lt;/code&gt; instead, replace &lt;code&gt;by name&lt;/code&gt; with &lt;code&gt;by pageName = tostring(customDimensions[&quot;&amp;lt;key you found&amp;gt;&quot;])&lt;/code&gt;. The 95th percentile is the number to watch, since an average hides the users who are having the worst time.&lt;/p&gt;

&lt;p&gt;Then, the crash you caused on purpose:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-kusto&quot;&gt;exceptions
| where timestamp &amp;gt; ago(7d)
| where cloud_RoleInstance == &quot;CDS Data Export&quot;
| summarize occurrences = count(), users = dcount(user_Id), lastSeen = max(timestamp) by problemId, outerMessage
| order by occurrences desc&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Your &quot;Telemetry smoke test&quot; message should show up in &lt;code&gt;outerMessage&lt;/code&gt; with two occurrences. The &lt;code&gt;users&lt;/code&gt; column is the one that turns a crash list into a priority list: one user hitting something forty times is a very different conversation from forty users hitting it once. Wire this query to an Azure Monitor alert rule and you have crash notification for code nobody on your team wrote.&lt;/p&gt;

&lt;p&gt;Finally, the data calls:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-kusto&quot;&gt;dependencies
| where timestamp &amp;gt; ago(7d)
| where cloud_RoleInstance == &quot;CDS Data Export&quot;
| summarize calls = count(), failed = countif(tobool(success) == false), p95 = percentile(duration, 95) by type, name
| order by p95 desc&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Run this before filtering on &lt;code&gt;type&lt;/code&gt;. The docs do not list the &lt;code&gt;type&lt;/code&gt; value used for &lt;code&gt;dataApi&lt;/code&gt; operations, and Dataverse&#39;s own server-side calls also land in &lt;code&gt;dependencies&lt;/code&gt;, so let the first run tell you which value belongs to your page, then add a &lt;code&gt;where type == &quot;...&quot;&lt;/code&gt; line. The &lt;code&gt;tobool()&lt;/code&gt; is not decoration: the classic &lt;code&gt;dependencies&lt;/code&gt; table documents &lt;code&gt;success&lt;/code&gt; as a string, and only the workspace version of the table (&lt;code&gt;AppDependencies&lt;/code&gt;) makes it a true bool, so a bare &lt;code&gt;success == false&lt;/code&gt; would compare two different types. A retrieve that is consistently slow at the 95th percentile is usually a query asking for every column of a large table, which is something you can fix with one more sentence in your next prompt.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Everything in this post is packaged in &lt;a href=&quot;https://github.com/dgpblogster/genpage-telemetry-lab&quot;&gt;genpage-telemetry-lab&lt;/a&gt; on GitHub. The &lt;code&gt;README.md&lt;/code&gt; carries the prerequisites and the admin center path from above, the &lt;code&gt;queries&lt;/code&gt; folder holds the four KQL files numbered in the order you should run them (reconnaissance first), and the &lt;code&gt;page&lt;/code&gt; folder holds an example generation prompt and the broken-button snippet, ready to paste.&lt;/p&gt;

&lt;p&gt;One thing the README covers that is worth repeating here: these queries use the classic Application Insights table names, so run them from &lt;b&gt;Logs&lt;/b&gt; on the Application Insights resource itself. Query the underlying Log Analytics workspace instead and the same data lives in &lt;code&gt;AppPageViews&lt;/code&gt;, &lt;code&gt;AppExceptions&lt;/code&gt; and &lt;code&gt;AppDependencies&lt;/code&gt;, under different column names. An Azure Monitor workbook charting the three tables side by side is the natural next addition, and it goes into the repo once the real column names are confirmed against live data, not before.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;What I like most about this change is where it put the responsibility. When an agent writes the code, any telemetry inside that code is only as durable as the next prompt. Instrumentation in the platform, configured once and applied to every published page, is the only arrangement that holds up when the page is regenerated every week, and in my humble view it is the right precedent for whatever else gets generated on this platform.&lt;/p&gt;

&lt;p&gt;It is a preview with honest gaps in the documentation, chiefly which export data type to pick and which column holds the page name. The reconnaissance query is there so you close those gaps with your own data rather than with assumptions. Then go break a button on purpose. It is the cheapest proof you&#39;ll get that the crash reporting works!&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/1661691785673000041/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/1661691785673000041' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1661691785673000041'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1661691785673000041'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/10/power-apps-page-that-reports-itself.html' title='Power Apps | The Page That Reports Itself'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-5316235235891917840</id><published>2026-10-08T07:00:00.000-04:00</published><updated>2026-10-08T16:16:06.013-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><title type='text'>Power Automate | Flows Without the Designer</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;Flows without the designer: the Power Automate plugin for GitHub Copilot CLI and Claude Code&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-20-pa-plugin.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Back on September 4th I wrote about &lt;a href=&quot;https://www.theworkbench.blog/2026/09/power-apps-describe-app-approve-plan.html&quot;&gt;the model app builder&lt;/a&gt;, the Power Platform plugin that stands up a model-driven app from a description instead of from four hours of clicking. I said at the time that the interesting thing was not the app builder itself but the delivery mechanism sitting underneath it, and that there were other plugins in the same marketplace worth looking at. Well, here we are.&lt;/p&gt;

&lt;p&gt;The article is &lt;a href=&quot;https://learn.microsoft.com/en-us/power-automate/power-automate-plugin-external-tools&quot;&gt;Get started with the Power Automate plugin for GitHub Copilot CLI and Claude Code&lt;/a&gt;, dated 2026-09-16 in the Power Automate documentation. Same marketplace, different plugin, and this one goes after cloud flows: create, edit, run, debug and manage, all from a terminal.&lt;/p&gt;

&lt;p&gt;Which is where I owe you a pointer, because this is not my first look at it. &lt;a href=&quot;https://www.theworkbench.blog/2026/08/building-power-automate-flows-with.html&quot;&gt;Back on August 13&lt;/a&gt; I wrote about building flows with Claude Code and FlowAgent, and FlowAgent is the MCP server that ships inside this very plugin. I wrote about it under the server&#39;s name rather than the plugin&#39;s, which is partly why the two do not obviously look like the same thing.&lt;/p&gt;

&lt;p&gt;That piece ended on a list of rough edges, and one of them is now closed: the resubmit and diagnose tools both threw type errors at the time, and I worked around them by calling the Power Automate REST API directly. That fix shipped in v3.0.4 on August 25. So if you read that article and have been resubmitting runs the long way ever since, you can stop. The plugin is several releases on from the one in that post, and what follows is what changed and what I still could not make work.&lt;/p&gt;

&lt;p&gt;Now, I did not want to write this one from the page. Anybody can summarize a doc. So I installed it, pointed it at a throwaway Developer environment, and tried to build something. What follows is what actually happened, which turned out to be considerably more interesting than the happy path.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;First, a distinction the documentation assumes you already have. &lt;b&gt;Power Platform Skills&lt;/b&gt; is a &lt;i&gt;marketplace&lt;/i&gt;, not a plugin. It lives at &lt;a href=&quot;https://github.com/microsoft/power-platform-skills&quot;&gt;microsoft/power-platform-skills&lt;/a&gt; and it currently carries eight of them: &lt;code&gt;power-pages&lt;/code&gt;, &lt;code&gt;model-apps&lt;/code&gt;, &lt;code&gt;mcp-apps&lt;/code&gt;, &lt;code&gt;canvas-apps&lt;/code&gt;, &lt;code&gt;code-apps-preview&lt;/code&gt;, &lt;code&gt;mobile-app&lt;/code&gt;, &lt;code&gt;power-apps-mobile-extension&lt;/code&gt; and &lt;code&gt;power-automate&lt;/code&gt;. The marketplace is the delivery channel. The plugin is the thing delivered, which is why the install line reads &lt;code&gt;power-automate@power-platform-skills&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I covered the installer and the Azure CLI sign-in in that earlier post, so I am not going to repeat any of it here. What is new is the surface: &lt;b&gt;ten skills&lt;/b&gt;, each available as a slash command or just described conversationally.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/setup&lt;/code&gt; checks Node, the Azure CLI, Power Automate access and connectivity&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/browse-flows&lt;/code&gt; browses environments and flows, and helps you pick one&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/create-flow&lt;/code&gt; and &lt;code&gt;/build-flow&lt;/code&gt; create a flow, guided or from a description&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/debug-flow&lt;/code&gt; and &lt;code&gt;/diagnose-flow&lt;/code&gt; inspect a failed run and chase root causes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/manage-flows&lt;/code&gt; handles publishing, testing, batch operations and inventory reports&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/manage-desktop-flows&lt;/code&gt; lists and runs desktop flows and machine groups&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/route-environments&lt;/code&gt; explains how your default environment is resolved&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/report-issue&lt;/code&gt; collects the details and files a public GitHub issue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin ships its own Power Automate MCP server, so there is no separate npm package and no remote MCP host to configure. It also bundles the Microsoft Learn MCP server as a companion knowledge source, which means the agent can go and read the official guidance on child flows or expressions while it is working on yours. That second detail is easy to skim past and it is genuinely clever.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why It Matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;If you are a maker, the honest answer is: not yet, and that is fine. This is a terminal tool that assumes an Azure CLI session and a comfort with JSON flow definitions. The designer is not going anywhere.&lt;/p&gt;

&lt;p&gt;If you are a professional developer, this is the piece that has been missing. Flows have always been the part of a solution you could not sensibly review, diff or automate, because the authoring surface was a canvas in a browser. A tool that reads and writes flow definitions from a terminal, under your own identity and your tenant&#39;s policies, is a different proposition entirely. Be sure to notice that last part: the plugin calls Power Automate as &lt;i&gt;you&lt;/i&gt;, so your licensing, your connector policies and your DLP rules all still apply. Nothing here is a way around governance.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Before You Build&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Start with &lt;code&gt;/setup&lt;/code&gt;, or the underlying doctor check, and actually read the output. Here is mine, lightly redacted, after I signed in to a different tenant:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;az-login          pass  Signed in as me@example.com (tenant 1111....)
token             pass  Acquired a token for https://service.flow.microsoft.com (tenant 2222....)
identity-match    FAIL  Identity mismatch: az is signed in to tenant 1111....
                        but the token FlowAgent would send belongs to tenant 2222....
                        This is what surfaces as EnvironmentAccessDenied.
                  fix   Call the reconnect tool (clears the token cache and re-acquires).
environment-access pass  Listed 36 environments.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This one nearly caught me out. The Azure CLI was signed in to the new tenant and the plugin was still holding a cached token for the old one, and notice that &lt;code&gt;environment-access&lt;/code&gt; still reported &lt;b&gt;pass&lt;/b&gt;: it cheerfully listed 36 environments belonging to the tenant I thought I had left. Had I skipped the check, I would have been operating against the wrong tenant entirely while believing otherwise. The &lt;code&gt;reconnect&lt;/code&gt; tool clears the cache and re-acquires, and after that the same call listed two environments, which was the correct answer. Running that check first is not ceremony. It is the difference between working where you think you are and working somewhere else.&lt;/p&gt;

&lt;p&gt;NOTE: there is a second, sharper edge in the same area. The plugin keeps a current working environment, and passing a one-off &lt;code&gt;env&lt;/code&gt; argument to any call silently makes that the new session default, overriding what you set deliberately. Worse, the remembered display name does not follow, so afterwards the tool reported &lt;code&gt;displayName: Workbench&lt;/code&gt; against a completely different environment&#39;s id. I reproduced it on purpose to be sure. The practical defence is simple: &lt;b&gt;pass the environment explicitly on anything that writes&lt;/b&gt;, and never trust the session default for a create or an update.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Building It&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;My test case was deliberately dull: a button-triggered flow that takes an order id and an amount, compares the amount against an approval threshold, and composes one of two decisions. No connectors at all, so nothing needed an interactive consent.&lt;/p&gt;

&lt;p&gt;The sequence that matters is validate, then preflight, then create:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;validate_flow   -&gt; valid: false
  premium-trigger-warning: HTTP Request triggers require a Premium license.
  Use &quot;Button&quot; kind for free/seeded plans.       [triggers.manual.kind]

validate_flow   -&gt; valid: true                   (after switching to Button)

preflight_flow  -&gt; overall: ready
  validation: valid, 0 errors, 0 warnings
  connectionRefs: []
  solutionWrap: not detected
  summary: &quot;Ready to save + publish&quot;

create_flow     -&gt; name: 8b4caf55-....  state: Stopped  created: 03:30:53Z&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first thing here is a genuine win. My original definition used an HTTP Request trigger, and &lt;code&gt;validate_flow&lt;/code&gt; rejected it &lt;i&gt;before anything existed&lt;/i&gt;, on licensing grounds, naming the exact property at fault. That is a class of problem that normally surfaces much later and much more expensively. Then &lt;code&gt;preflight_flow&lt;/code&gt; goes further: it checks connection references and whether a solution wrap is going to block publishing, which are the two things that most often turn a save into a bad afternoon. And when I read the created flow back, the stored definition matched what I sent exactly, with only the key order rearranged. Nothing was silently rewritten, which is precisely what you want from a tool that edits your work.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;When It Breaks&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Now, the part I did not expect. The flow was created in a &lt;b&gt;Stopped&lt;/b&gt; state, which is by design and sensible. Starting it from the terminal did not work:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;publish_flow -&gt; success: false, alreadyStarted: true, actualState: &quot;Stopped&quot;
   warning: &quot;It may have an unauthenticated or missing connection. Check
             list_connections and ensure all connection references are in
             Connected status before publishing.&quot;

run_flow     -&gt; 404 tokenexchange
   &quot;The connection (.../logicflows/8b4caf55-....) is not found. Please create
    new connection and change your application to use the new connection.&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. That first response contradicts itself: &lt;code&gt;success: false&lt;/code&gt; alongside &lt;code&gt;alreadyStarted: true&lt;/code&gt; alongside an actual state of Stopped. And the warning sends you to check connection references on a flow that has &lt;b&gt;none&lt;/b&gt;, which &lt;code&gt;preflight_flow&lt;/code&gt; had confirmed minutes earlier with &lt;code&gt;connectionRefs: []&lt;/code&gt;. The token exchange failure downstream is the honest signal: it is looking for a trigger registration that was never provisioned, because the flow never actually started. I turned the flow on by hand in the maker portal, which worked immediately, and that is worth knowing too: &lt;b&gt;the flow came back with a different id&lt;/b&gt;. Any script holding the id that &lt;code&gt;create_flow&lt;/code&gt; handed you is now pointing at nothing.&lt;/p&gt;

&lt;p&gt;With the flow running, it ran. And failed. Here is where the tooling earns its keep:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;run_flow         -&gt; status: Failed, errorCode: ActionFailed
   Approval_threshold  Succeeded   1ms
   Check_amount        Failed      3ms   InvalidTemplate
   Needs_review        Skipped           ActionDependencyFailed
   Auto_approved       Skipped           ActionDependencyFailed

get_run_actions  -&gt; Check_amount
   &quot;Unable to process template language expressions for action &#39;Check_amount&#39;:
    The template language expression &#39;triggerBody()[&#39;number&#39;]&#39; cannot be
    evaluated because property &#39;number&#39; cannot be selected.&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is the strongest part of the whole plugin. It did not tell me the flow failed. It told me &lt;i&gt;which action&lt;/i&gt; failed, which &lt;i&gt;expression&lt;/i&gt; inside that action could not be evaluated, and which &lt;i&gt;property&lt;/i&gt; could not be selected, with the two downstream actions correctly marked as skipped rather than failed. Then &lt;code&gt;get_past_trigger_inputs&lt;/code&gt;, the same primitive the designer uses for &quot;test with previous run data&quot;, showed me the trigger envelope carried the schema but not my values. So the body I passed never reached &lt;code&gt;triggerBody()&lt;/code&gt; at all. Chasing that to the end: switching the trigger to the Power Apps V2 kind and retrying produced a refusal that I want to quote, because it is an unusually good error message.&lt;/p&gt;

&lt;p&gt;It explained that trigger inputs for that kind are delivered through the Logic Flows connector runtime, that the refusal is a &lt;b&gt;per-flow authorization condition rather than a limit of the trigger kind&lt;/b&gt;, that the same kind runs fine on a flow you own, and that I could either run it from the app that owns it or resubmit a past run that already carried inputs. That is a message written by somebody who has actually watched people get stuck on this. Slick.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;What Worked and What Did Not&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Being concrete, because a post that says &quot;it is promising&quot; helps nobody:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Worked well:&lt;/b&gt; the connectivity and identity checks, the pre-creation validation, the preflight, creating the flow, updating it in place, stopping it, and above all the run-level diagnosis, which is more precise than what I get from the portal.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Did not work:&lt;/b&gt; starting a freshly created flow from the terminal, and passing trigger inputs into a run headlessly.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Worth knowing:&lt;/b&gt; the session environment default can be changed out from under you, and turning an API-created flow on by hand can change its id.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the shape of it is this. &lt;b&gt;Building a flow from the terminal works. Getting data into one from the terminal is where the seam still shows.&lt;/b&gt; That is a preview finding, not a verdict, and the limitations section of the article is upfront that features change as the marketplace updates. If you hit these too, &lt;code&gt;/report-issue&lt;/code&gt; is right there and it files against the public repository, which is a considerably better feedback loop than most preview tooling offers.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The companion repo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Sketching it out, a &lt;code&gt;flows-from-the-terminal-lab&lt;/code&gt; would carry the flow definition JSON for both the working and the deliberately broken version, a small script that validates a definition before you ever call create, and a README recording which operations need the portal today. The flow definition is the artifact here, and the fact that it is now a file you can diff and review is most of the point.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Two closing thoughts.&lt;/p&gt;

&lt;p&gt;The first is that the failures above are the reason I bothered. Had everything worked, this would have been a nicer read and a much less useful one. The tool is in preview, it says so, and the things that broke are exactly the things you would want a colleague to warn you about before you spent an afternoon on them.&lt;/p&gt;

&lt;p&gt;The second is the one I keep coming back to. Eight plugins in that marketplace and I have now looked at two. Flows, model-driven apps, canvas apps, code apps, Power Pages, and an MCP one I am particularly curious about. Whatever you make of any individual plugin, the pattern underneath is the story: the Power Platform is growing a terminal-shaped surface, under your own identity, governed by your own tenant&#39;s rules. For those of us who have spent years explaining why the good stuff was locked behind a canvas, that is a genuinely interesting direction.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/5316235235891917840/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/5316235235891917840' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5316235235891917840'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5316235235891917840'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/10/power-automate-flows-without-designer.html' title='Power Automate | Flows Without the Designer'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-3655368986616062262</id><published>2026-10-05T07:00:00.000-04:00</published><updated>2026-10-05T10:25:58.647-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><title type='text'>Copilot Studio | Your MCP Server Now Needs a Key Vault</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;Your MCP server now needs a vault: the updated Microsoft MCP server certification path&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-19-mcp-certification.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;If you have an MCP server manifest sitting on your disk right now, there is a good chance it is already out of date. And you have until &lt;b&gt;October 31, 2026&lt;/b&gt; to do something about it.&lt;/p&gt;

&lt;p&gt;On September 26 the &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-certification&quot;&gt;Microsoft MCP server certification overview (preview)&lt;/a&gt; grew by a few hundred characters. Carried in that small diff: a schema version bump off the preview schema, a hard close date on the submission path you already know, and a new requirement that obliges you to stand up Azure infrastructure before you can submit anything at all. Quite a lot of consequence for a small diff!&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;First, the vocabulary. The &lt;b&gt;Model Context Protocol&lt;/b&gt;, almost always written MCP, is an open protocol for exposing tools and actions to an AI agent, so the agent can call your service without anyone hand building a one-off integration. An &lt;b&gt;MCP server&lt;/b&gt; is the thing on your side that speaks it, and &lt;b&gt;certification&lt;/b&gt; is the review Microsoft runs before yours becomes broadly discoverable.&lt;/p&gt;

&lt;p&gt;Now, the core of the process did not change, and the article is careful to say so: verified publishers submit a package, Microsoft validates it and gets issues remediated before approval, and publishers remain responsible for the certified experience afterward. What changed is nearly everything around that core:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The manifest example moved off the preview schema. It was &lt;code&gt;vDevPreview&lt;/code&gt; with &lt;code&gt;&quot;manifestVersion&quot;: &quot;devPreview&quot;&lt;/code&gt;. It is now the &lt;b&gt;v1.30&lt;/b&gt; Teams schema with a plain &lt;code&gt;&quot;version&quot;: &quot;1.30&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Submissions go through the Partner Center offer type &lt;b&gt;Apps and Agents for M365 and Copilot&lt;/b&gt;, and the legacy path stays open only &lt;b&gt;through October 31, 2026&lt;/b&gt;. The article blames high submission volume during preview, and warns the new path might be slower.&lt;/li&gt;
&lt;li&gt;Every submission now requires a manifest file, a tool file, an intro file and an &lt;b&gt;Azure Key Vault authentication configuration&lt;/b&gt;. That last one is the requirement with real setup work behind it.&lt;/li&gt;
&lt;li&gt;The list of publishing surfaces grew from Azure Foundry and Copilot Studio to &lt;b&gt;Azure Foundry, Cowork, Copilot chat and Copilot Studio&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;A new FAQ entry states plainly that &lt;b&gt;Dynamic Client Registration is not supported today&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;If you already ship an M365 Agent containing an MCP server, you do not submit it separately. Those become standalone MCP servers automatically later this year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NOTE: most of the byte count in that edit is prose being reflowed into bullet lists, which is exactly why a change like this is easy to skim past. The schema version and the Key Vault requirement were sitting in the middle of what looked like a formatting pass.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The Problem&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Two problems, really, on different clocks. The first is the manifest on your disk right now: if you built against the preview schema, it declares &lt;code&gt;manifestVersion&lt;/code&gt; and points at a &lt;code&gt;vDevPreview&lt;/code&gt; URL, and the documented example no longer resembles it. The second is the Key Vault requirement, which is not a field you fill in. It is an Azure resource, precisely named secrets, a Microsoft service principal and a role assignment, all of which must exist before validation can retrieve your OAuth configuration.&lt;/p&gt;

&lt;p&gt;Here is the manifest skeleton as the article now documents it, trimmed to the parts that carry the change:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;$schema&quot;: &quot;https://developer.microsoft.com/json-schemas/teams/v1.30/MicrosoftTeams.schema.json&quot;,
  &quot;version&quot;: &quot;1.30&quot;,
  &quot;id&quot;: &quot;&amp;lt;APP_ID&amp;gt;&quot;,
  &quot;agentConnectors&quot;: [
    {
      &quot;id&quot;: &quot;&amp;lt;CONNECTOR_ID&amp;gt;&quot;,
      &quot;displayName&quot;: &quot;&amp;lt;CONNECTOR_DISPLAY_NAME&amp;gt;&quot;,
      &quot;toolSource&quot;: {
        &quot;remoteMcpServer&quot;: {
          &quot;mcpServerUrl&quot;: &quot;&amp;lt;MCP_SERVER_URL&amp;gt;&quot;,
          &quot;mcpToolDescription&quot;: { &quot;file&quot;: &quot;mcptools.json&quot; },
          &quot;authorization&quot;: {
            &quot;type&quot;: &quot;AzureKeyVault&quot;,
            &quot;referenceId&quot;: &quot;https://contoso-mcp-kv.vault.azure.net/&quot;
          }
        }
      }
    }
  ],
  &quot;icons&quot;: { &quot;outline&quot;: &quot;Outline.png&quot;, &quot;color&quot;: &quot;Color.png&quot; }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server is declared under &lt;code&gt;agentConnectors&lt;/code&gt;, with &lt;code&gt;toolSource.remoteMcpServer&lt;/code&gt; holding the endpoint, so a certified MCP server is modeled as a connector rather than its own top level object. Your tool schemas sit in the separate file named by &lt;code&gt;mcpToolDescription.file&lt;/code&gt;, here &lt;code&gt;mcptools.json&lt;/code&gt;. And look closely at &lt;code&gt;referenceId&lt;/code&gt;: the article says twice that this must be the &lt;b&gt;Key Vault URI and not a secret URI&lt;/b&gt;, which tells you how often it arrives wrong. One more that bites quietly, Microsoft supports &lt;b&gt;ASCII header names and values only&lt;/b&gt; in the manifest and tool files.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why It Matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;If you are a maker, the effect is about what you can expect to find and trust. Certification decides which MCP servers appear as vetted options, and the surface list growing to include Cowork and Copilot chat means a server certified once becomes discoverable in more places without further work by its publisher. You are not submitting anything, but you are the beneficiary of the review.&lt;/p&gt;

&lt;p&gt;If you are a professional developer, this is a work item with a date attached. The new path is the one to target, October 31 is when the old one closes, and the Key Vault configuration touches your Azure subscription and somebody else&#39;s change process. Read the eligibility rules first: you must be a verified publisher whose Partner Center account has completed business verification, be enrolled in the Microsoft 365 and Copilot program, and own or control the endpoint you submit.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Implementation&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The article documents the Key Vault setup as a portal exercise. It is also the part most likely to go wrong, so I am going to give you the whole Azure side as a script first, and then walk the steps behind it:&lt;/p&gt;

&lt;p&gt;1) Create an Azure Key Vault in your Azure tenant. The documentation specifies the Azure portal, so what follows is my Az PowerShell equivalent of steps 1 through 3, which you can run end to end:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# Install-Module Az -Scope CurrentUser   # if you do not already have it
Connect-AzAccount

$subscription = &#39;&amp;lt;SUBSCRIPTION_ID&amp;gt;&#39;
$resourceGroup = &#39;rg-mcp-cert&#39;
$location = &#39;eastus&#39;
$vaultName = &#39;contoso-mcp-kv&#39;                             # must be globally unique
$certServiceAppId = &#39;8e91e74f-afe9-41cd-8c3f-17a9562a74ea&#39;  # Microsoft&#39;s certification service

Set-AzContext -Subscription $subscription | Out-Null

# Step 1: the vault itself. On Az 12.0.0 and later, RBAC authorization is the default
New-AzResourceGroup -Name $resourceGroup -Location $location -Force | Out-Null
$vault = New-AzKeyVault -Name $vaultName `
                        -ResourceGroupName $resourceGroup `
                        -Location $location

# Step 2: the secrets. These names are case sensitive and must match exactly
$secrets = [ordered]@{
    ClientId     = &#39;&amp;lt;CLIENT_ID&amp;gt;&#39;
    ClientSecret = &#39;&amp;lt;CLIENT_SECRET&amp;gt;&#39;
    TokenUrl     = &#39;https://login.example.com/oauth2/token&#39;
    # AuthorizationUrl               = &#39;...&#39;   # required for an OAuth2 provider
    # RefreshUrl                     = &#39;...&#39;   # when you issue refresh tokens
    # Scopes                         = &#39;...&#39;   # when your provider needs scopes
    # AzureActiveDirectoryResourceId = &#39;...&#39;   # required for an Azure AD provider
}

foreach ($name in $secrets.Keys) {
    $secure = ConvertTo-SecureString $secrets[$name] -AsPlainText -Force
    Set-AzKeyVaultSecret -VaultName $vaultName -Name $name -SecretValue $secure | Out-Null
    Write-Host &quot;stored $name&quot;
}

# Step 3: Microsoft&#39;s service principal, then read access to the vault
$sp = Get-AzADServicePrincipal -ApplicationId $certServiceAppId -ErrorAction SilentlyContinue
if (-not $sp) { $sp = New-AzADServicePrincipal -ApplicationId $certServiceAppId }

New-AzRoleAssignment -ObjectId $sp.Id `
                     -RoleDefinitionName &#39;Key Vault Secrets User&#39; `
                     -Scope $vault.ResourceId

# Step 4: this is the value that goes into the manifest
Write-Host &quot;authorization.referenceId = $($vault.VaultUri)&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Two of these will cost you a submission cycle. The first is what is &lt;i&gt;not&lt;/i&gt; in that &lt;code&gt;New-AzKeyVault&lt;/code&gt; call. The article grants access with an RBAC role, and an RBAC role assignment does nothing on a vault running legacy access policies, so you may reach for &lt;code&gt;-EnableRbacAuthorization&lt;/code&gt;. Do not: it was removed in Az 12.0.0, replaced by &lt;code&gt;-DisableRbacAuthorization&lt;/code&gt;, and &lt;b&gt;RBAC is now the default&lt;/b&gt;, which is why the call omits it. See the &lt;a href=&quot;https://learn.microsoft.com/powershell/azure/migrate-az-12.0.0&quot;&gt;Az 12.0.0 migration guide&lt;/a&gt;. The trap that survives is reusing an &lt;i&gt;existing&lt;/i&gt; vault built the old way, where the role assignment looks like it succeeded and grants nothing. Fix that with &lt;code&gt;Update-AzKeyVault -DisableRbacAuthorization $false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second is that those secret names are the literal keys of the hash table on purpose: the article devotes a whole FAQ entry to confirming &lt;b&gt;secret names are case sensitive and should match exactly&lt;/b&gt;, and &lt;code&gt;ClientId&lt;/code&gt; is not &lt;code&gt;clientid&lt;/code&gt;. Note too that &lt;code&gt;New-AzADServicePrincipal&lt;/code&gt; is not creating an application of yours, it is instantiating Microsoft&#39;s in your tenant so the role assignment has something to bind to. And the last line prints &lt;code&gt;$vault.VaultUri&lt;/code&gt;, exactly what the manifest wants for &lt;code&gt;referenceId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;2) Store the required secrets in the vault: &lt;code&gt;ClientId&lt;/code&gt;, &lt;code&gt;ClientSecret&lt;/code&gt; and &lt;code&gt;TokenUrl&lt;/code&gt;. Then add the optional ones your identity provider needs. &lt;code&gt;AuthorizationUrl&lt;/code&gt; is required for an OAuth2 provider, &lt;code&gt;AzureActiveDirectoryResourceId&lt;/code&gt; is required for an Azure AD provider, and &lt;code&gt;RefreshUrl&lt;/code&gt; and &lt;code&gt;Scopes&lt;/code&gt; come into play for refresh tokens and scoped access respectively.&lt;/p&gt;

&lt;p&gt;3) Create a service principal for the Microsoft application &lt;code&gt;8e91e74f-afe9-41cd-8c3f-17a9562a74ea&lt;/code&gt;, and grant it &lt;b&gt;Key Vault Secrets User&lt;/b&gt;, or an equivalent RBAC read role, on the vault. This is what lets the certification service retrieve your OAuth configuration during validation.&lt;/p&gt;

&lt;p&gt;4) Add the vault URI to the manifest as &lt;code&gt;authorization.referenceId&lt;/code&gt;, exactly as shown above.&lt;/p&gt;

&lt;p&gt;5) Go to Partner Center and proceed to create a new offer, selecting the offer type &lt;b&gt;Apps and Agents for M365 and Copilot&lt;/b&gt;. Upload the package and supply the required commercial, legal, support and publisher information.&lt;/p&gt;

&lt;p&gt;6) Let automated validation run against package structure, required fields, schema correctness and metadata completeness. Blocking issues must be fixed before the functional and safety review continues. Including evaluation evidence, meaning representative functional and safety test results, can help accelerate that review.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo, and a companion repo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;So I built one, and you can clone it: &lt;a href=&quot;https://github.com/dgpblogster/mcp-cert-lab&quot;&gt;github.com/dgpblogster/mcp-cert-lab&lt;/a&gt;. Very nearly every requirement on that page is mechanically verifiable before you ever open Partner Center, which makes a readiness checker the obvious companion piece: one PowerShell script that takes a package folder, and optionally looks at the vault behind it.&lt;/p&gt;

&lt;p&gt;Laying it out the way I laid out &lt;a href=&quot;https://github.com/dgpblogster/project-orion-lab&quot;&gt;project-orion-lab&lt;/a&gt;, the hands-on companion to my &lt;i&gt;Same Agent, Two Architectures: When MCP Wins (And When It Doesn&#39;t)&lt;/i&gt; session at Community Summit NA 2026, which is to say a README that carries the prerequisites and one folder per moving part:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;mcp-cert-lab/
  README.md                     what each check maps to in the article
  Test-McpCertReadiness.ps1     the checker
  samples/
    good-package/               passes cleanly
      manifest.json             v1.30, vault URI, every field populated
      mcptools.json             three tools, JSON Schema inputs
      intro.md                  with a Known issues and limitations section
      Color.png  Outline.png    192x192 and 32x32
    bad-package/                eight planted defects in one file
      manifest.json             devPreview schema, secret URI, a smart quote
  tests/
    Invoke-Tests.ps1            asserts good passes AND bad is caught, by name&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On the shape of that. The checker runs with no Azure connection at all unless you pass &lt;code&gt;-CheckVault&lt;/code&gt;, which is deliberate: most of what fails a submission fails on disk. It exits &lt;b&gt;0&lt;/b&gt; when nothing failed and &lt;b&gt;1&lt;/b&gt; when anything did, so it works as a pipeline gate. And &lt;code&gt;bad-package&lt;/code&gt; is not filler, it is the instrument that proves the checker still works.&lt;/p&gt;

&lt;p&gt;Pointed at the deliberately broken package, it reports:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FAIL   Schema     $schema points at v1.30  (still on the retired vDevPreview schema)
FAIL   Schema     manifestVersion removed  (replaced by version in the v1.30 shape)
FAIL   Schema     version is 1.30  (found: 1.0.0)
FAIL   Metadata   developer.termsOfUseUrl  (placeholder left in: &amp;lt;TERMS_OF_USE_URL&amp;gt;)
FAIL   Icons      color icon on disk  (Color.png not found)
FAIL   Connector  [contoso-shopfloor] tool file on disk  (mcptools.json not found)
FAIL   Connector  [contoso-shopfloor] referenceId is a vault URI  (this is a SECRET URI, use the vault URI)
FAIL   Ascii      manifest.json ASCII only  (U+2019 at line 18 col 55)

16 passed, 9 failed, 1 warnings
Not ready to submit.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That last line is the one I care about most. &lt;code&gt;U+2019 at line 18 col 55&lt;/code&gt; is a curly apostrophe, the kind that arrives when somebody pastes a company name out of an email, and the article is explicit that non-ASCII characters might cause validation failures. It is invisible in every editor you are likely to be using. Finding it by line and column in half a second, rather than by elimination after a rejected submission, is the whole argument for writing the thing.&lt;/p&gt;

&lt;p&gt;Two places where I deliberately stopped short, because a checker that overreaches is one people turn off. Icon &lt;b&gt;dimensions are reported, not asserted&lt;/b&gt;, since the exact sizes live in Teams guidance outside this article. And the intro file is a &lt;b&gt;warning rather than a failure&lt;/b&gt;, for the reason in the Final Notes below. Everything else is a hard pass or fail, including the vault checks, where secret names are compared &lt;b&gt;case sensitively&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Three closing thoughts.&lt;/p&gt;

&lt;p&gt;First, this is preview documentation and says so at the top, so the schema version, the package requirements and the surface list are all fair game to change again before general availability. The one item with a fixed date is the October 31, 2026 close of the legacy path, and that is what I would plan around.&lt;/p&gt;

&lt;p&gt;Second, a small inconsistency worth knowing about rather than tripping over. The &lt;i&gt;What&#39;s changing&lt;/i&gt; table states that all submissions require a manifest file, tool file, intro.md file and Key Vault configuration, while a section further down the same page is headed &lt;i&gt;Intro file (Optional)&lt;/i&gt;. The two disagree. The safe course is to include the intro file, which you want anyway, with a &lt;i&gt;Known issues and limitations&lt;/i&gt; section in it.&lt;/p&gt;

&lt;p&gt;Finally, the observation I keep coming back to. Read that list of publishing surfaces once more: Azure Foundry, Cowork, Copilot chat, Copilot Studio. One certification, four destinations, with Microsoft 365 admin governance applying across them where relevant. That is the real story hiding inside a small diff. Certifying an MCP server is drifting away from being a Copilot Studio errand and toward being how a tool gets admitted to the Microsoft agent ecosystem generally, and the entry fee is a manifest, a vault and a review.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/3655368986616062262/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/3655368986616062262' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/3655368986616062262'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/3655368986616062262'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/10/copilot-studio-your-mcp-server-now.html' title='Copilot Studio | Your MCP Server Now Needs a Key Vault'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-8499113114456703763</id><published>2026-10-02T07:00:00.000-04:00</published><updated>2026-10-02T14:46:35.728-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><title type='text'>Power Apps | Rows Without an Owner</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;Rows without an owner: filtered record ownership brings predicate-based row-level security to Dataverse&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-18-filtered-record-ownership.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;If you have spent any real time modeling security in Dataverse, you have had this conversation. A customer wants &quot;this group of people should only see the rows for their region&quot;, and you reach for the tools you have: business units, teams, hierarchy security, sharing, maybe an owning team per region and a great deal of patient explaining about why the org chart and the data model now have to agree with each other forever. It works. It has worked for the better part of two decades. But you have also had the other half of that conversation, the one where the customer says &quot;but the region is just a column on the record&quot;, and you have to explain why a column cannot grant access, and why the answer involves restructuring their business units.&lt;/p&gt;

&lt;p&gt;Well, in late September a new article appeared in the Power Apps developer documentation, dated September 23, 2026, and it describes a genuinely new answer to that conversation. It is called &lt;b&gt;filtered record ownership&lt;/b&gt;, and the short version is this: a table whose rows have no owner at all, where access is decided by a FetchXML predicate evaluated against the row&#39;s own column values.&lt;/p&gt;

&lt;p&gt;That is a new ownership type in Dataverse. Those do not come along very often.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The new page is &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/filtered-record-ownership&quot;&gt;Filtered record ownership by using code (preview)&lt;/a&gt;, roughly 400 lines covering the SDK for .NET and the Web API. It is the developer half of a pair. The maker half, &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/data-platform/filtered-view-record-ownership&quot;&gt;Filtered record ownership&lt;/a&gt;, covers the same ground through the Power Apps portal, and the developer article is careful to point you there first. Its opening is refreshingly blunt about it: you do not need to write code to use this feature.&lt;/p&gt;

&lt;p&gt;NOTE: everything below is preview. The record ownership dropdown in the maker portal literally reads &lt;b&gt;Filtered (preview)&lt;/b&gt;, and the developer article carries the preview banner.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The model, in three objects&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The design is clean once you see the pieces, and it is worth laying out before any code.&lt;/p&gt;

&lt;p&gt;First, the &lt;b&gt;table&lt;/b&gt;. You create it with its &lt;code&gt;OwnershipType&lt;/code&gt; set to a new member, &lt;code&gt;Filtered&lt;/code&gt;, which has the value &lt;code&gt;32&lt;/code&gt;. Rows in such a table have no owner. They cannot be assigned and they cannot be shared. And you cannot change the ownership type after the table is created, so this is a decision you make once, at design time, and live with.&lt;/p&gt;

&lt;p&gt;Second, the &lt;b&gt;record filter&lt;/b&gt;, stored in a new &lt;code&gt;recordfilter&lt;/code&gt; table. It holds a display name, a unique name, and a FetchXML query that defines which rows it selects. The FetchXML root entity must match the table you eventually associate it with.&lt;/p&gt;

&lt;p&gt;Third, the &lt;b&gt;entity record filter&lt;/b&gt;, stored in a new &lt;code&gt;entityrecordfilter&lt;/code&gt; table, which associates a record filter with a Dataverse table. A table can have several, so &quot;City equals Redmond&quot; and &quot;City in Kirkland or Sammamish&quot; can coexist as separate, independently activated predicates against the same table.&lt;/p&gt;

&lt;p&gt;A security role then grants a filter for one or more of the &lt;b&gt;Create&lt;/b&gt;, &lt;b&gt;Read&lt;/b&gt;, &lt;b&gt;Write&lt;/b&gt;, &lt;b&gt;Delete&lt;/b&gt;, &lt;b&gt;Append&lt;/b&gt; and &lt;b&gt;Append to&lt;/b&gt; privileges, and you assign the role to users or teams. So, the whole chain in one sentence: a FetchXML predicate becomes a named filter, the filter is attached to a table, and a security role hands that filter to people for specific operations.&lt;/p&gt;

&lt;p&gt;Access is &lt;i&gt;cumulative&lt;/i&gt;. The documentation puts it precisely: access is the union of the filters granted through all security roles assigned to the user or their teams. Grant somebody the Redmond filter through one role and the Kirkland filter through another, and they see both cities. Nobody is subtracting anything, so when a user sees more than you expected, the answer is almost always a second role you forgot about.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Creating the table in code&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Here is the table creation, the piece that makes it a filtered table at all:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var createTableRequest = new CreateEntityRequest
{
    SolutionUniqueName = &quot;&amp;lt;Solution Unique Name&amp;gt;&quot;,
    Entity = new EntityMetadata
    {
        SchemaName = &quot;new_Store&quot;,
        DisplayName = new Label(&quot;Store&quot;, 1033),
        DisplayCollectionName = new Label(&quot;Stores&quot;, 1033),
        Description = new Label(&quot;Stores organized by city.&quot;, 1033),
        OwnershipType = OwnershipTypes.Filtered,
        IsActivity = false
    },
    PrimaryAttribute = new StringAttributeMetadata
    {
        SchemaName = &quot;new_Name&quot;,
        DisplayName = new Label(&quot;Name&quot;, 1033),
        RequiredLevel = new AttributeRequiredLevelManagedProperty(
            AttributeRequiredLevel.None),
        MaxLength = 100,
        FormatName = StringFormatName.Text
    }
};

service.Execute(createTableRequest);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. The one line that matters is &lt;code&gt;OwnershipType = OwnershipTypes.Filtered&lt;/code&gt;. Everything else is an ordinary custom table. The &lt;code&gt;SolutionUniqueName&lt;/code&gt; is the optional parameter that associates the new component with an unmanaged solution as it is created, which you want, because every object in this feature is a solution component and you would rather not go hunting for them later. If you prefer the Web API, the same thing is a POST to &lt;code&gt;EntityDefinitions&lt;/code&gt; with &lt;code&gt;&quot;OwnershipType&quot;: &quot;Filtered&quot;&lt;/code&gt; in the body and the solution carried in an &lt;code&gt;MSCRM.SolutionUniqueName&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Creating the filter&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Next, the predicate itself, as a row in the new &lt;code&gt;recordfilter&lt;/code&gt; table:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;string fetchXml = &quot;&quot;&quot;
    &amp;lt;fetch&amp;gt;
      &amp;lt;entity name=&quot;new_store&quot;&amp;gt;
        &amp;lt;filter type=&quot;and&quot;&amp;gt;
          &amp;lt;condition attribute=&quot;new_city&quot; operator=&quot;eq&quot; value=&quot;Redmond&quot; /&amp;gt;
        &amp;lt;/filter&amp;gt;
      &amp;lt;/entity&amp;gt;
    &amp;lt;/fetch&amp;gt;
    &quot;&quot;&quot;;

Entity recordFilter = new(&quot;recordfilter&quot;)
{
    [&quot;displayname&quot;] = &quot;Stores in Redmond&quot;,
    [&quot;uniquename&quot;] = &quot;new_storesinredmond&quot;,
    [&quot;fetchxml&quot;] = fetchXml
};

CreateRequest createRecordFilterRequest = new() { Target = recordFilter };
createRecordFilterRequest[&quot;SolutionUniqueName&quot;] = &quot;&amp;lt;Solution Unique Name&amp;gt;&quot;;

CreateResponse createRecordFilterResponse =
    (CreateResponse)service.Execute(createRecordFilterRequest);
Guid recordFilterId = createRecordFilterResponse.id;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Two things in there are deliberate. The FetchXML is stored as plain text on the row, and its root &lt;code&gt;&amp;lt;entity&amp;gt;&lt;/code&gt; name must match the table this filter will be associated with. Notice also that this uses &lt;code&gt;CreateRequest&lt;/code&gt; rather than the more convenient &lt;code&gt;service.Create(entity)&lt;/code&gt;, and that is deliberate: &lt;code&gt;CreateRequest&lt;/code&gt; is what lets you set the &lt;code&gt;SolutionUniqueName&lt;/code&gt; optional parameter. Give the filter a unique name with your publisher prefix, the same as any other component.&lt;/p&gt;

&lt;p&gt;Then you associate the filter with the table:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;Entity entityRecordFilter = new(&quot;entityrecordfilter&quot;)
{
    [&quot;name&quot;] = &quot;Stores in Redmond&quot;,
    [&quot;objecttypecode&quot;] = &quot;new_store&quot;,
    [&quot;recordfilterid&quot;] = new EntityReference(&quot;recordfilter&quot;, recordFilterId)
};

CreateRequest createEntityRecordFilterRequest = new() { Target = entityRecordFilter };
createEntityRecordFilterRequest[&quot;SolutionUniqueName&quot;] = &quot;&amp;lt;Solution Unique Name&amp;gt;&quot;;

service.Execute(createEntityRecordFilterRequest);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;objecttypecode&lt;/code&gt; takes the table&#39;s logical name as a string, and &lt;code&gt;recordfilterid&lt;/code&gt; is a lookup back to the filter you just created. In the Web API the same association is made with the &lt;code&gt;RecordFilterId@odata.bind&lt;/code&gt; navigation property. This row is what &lt;i&gt;activates&lt;/i&gt; the predicate for that table, which is why the maker documentation describes creating one as the step that turns a filter on.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters, for makers&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The headline for a maker is that row-level security is now expressible as a condition on a column, in a dialog you already know how to use, and it never touches your business unit structure.&lt;/p&gt;

&lt;p&gt;Better still, the maker path has a genuinely nice touch: you do not have to hand-write FetchXML. You open a model-driven app, open the table, use the &lt;b&gt;Filters&lt;/b&gt; pane to build the condition visually, and then select &lt;b&gt;Download FetchXML&lt;/b&gt; to get exactly the XML you need to paste into the record filter. Building the predicate with the same filter builder your users already use, then shipping it as a security rule, is a good piece of design and whoever thought of it deserves the credit.&lt;/p&gt;

&lt;p&gt;The thing to be careful about is the one the documentation flags twice, and I will flag it a third time: &lt;b&gt;test with a nonadministrator account&lt;/b&gt;. The system administrator role carries a system-provided &lt;b&gt;All records&lt;/b&gt; filter, so an administrator sees everything and will never experience the restriction. Demo this as an admin and you will demo nothing at all.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters, for professional developers&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Three things I would want on the table before recommending this to anyone.&lt;/p&gt;

&lt;p&gt;The first is that the ownership type is immutable. &lt;code&gt;Filtered&lt;/code&gt; is chosen at &lt;code&gt;CreateEntityRequest&lt;/code&gt; time and cannot be changed afterward, which means this is not something you retrofit onto an existing table. It is a modeling decision, made up front, and the migration path from a user-owned or organization-owned table to a filtered one is &quot;create a new table and move the data&quot;. Plan accordingly.&lt;/p&gt;

&lt;p&gt;The second is that a surprising amount of the platform assumes an owner exists. Anything in your codebase or your process that reaches for &lt;code&gt;ownerid&lt;/code&gt;, or calls &lt;code&gt;AssignRequest&lt;/code&gt;, or grants access with &lt;code&gt;GrantAccessRequest&lt;/code&gt;, simply has no counterpart here. That is the point of the design rather than a criticism, but it turns up during testing rather than during design if nobody says it out loud early.&lt;/p&gt;

&lt;p&gt;The third is that this is now application lifecycle management surface area. Record filters and entity record filters are solution components, and the security roles that grant them are solution components too. Which means the predicate that decides who sees what is a versioned, deployable artifact that moves between environments with your solution. That is the part I find genuinely exciting, and I would even venture to say it is the more important half of this feature: your row-level security rules stop being environment configuration that somebody clicked together in production and start being something you can review in a pull request.&lt;/p&gt;

&lt;p&gt;One operational note for your runbook: deleting a record filter also deletes its associated entity record filters. Review every role that grants a filter before you change or remove it, because pulling a predicate out from under a role is not a change you want to hear about from a user who suddenly cannot see anything.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo I would build&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The demo writes itself, and it is the two-screenshot kind that makes a point in about four seconds.&lt;/p&gt;

&lt;p&gt;Build a &lt;code&gt;Store&lt;/code&gt; table with a &lt;code&gt;City&lt;/code&gt; column and populate it with a dozen rows across Redmond, Kirkland and Sammamish. Create two record filters, one per city, and two security roles, each granting Read on one filter. Then sign in as two nonadministrator users with one role each, open the same view of the same table, and put the two screens side by side. Same app, same table, same view definition, two different sets of rows, no business unit involved anywhere.&lt;/p&gt;

&lt;p&gt;Then, for the finale, assign the second role to the first user as well and refresh. The rows union together in front of the audience. That is the cumulative-access rule made visible, and it is the single behavior most likely to confuse somebody later, so it is worth burning into people&#39;s memory while you have their attention.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The companion repo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Which is no longer a sketch: it is at &lt;a href=&quot;https://github.com/dgpblogster/filtered-ownership-lab&quot;&gt;github.com/dgpblogster/filtered-ownership-lab&lt;/a&gt;, MIT, clone it or download the zip. The Web API calls are the part to trust first, since there is nothing between the request and the service. Laying it out:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;filtered-ownership-lab/
  README.md                    what preview means here, and the admin-blindness warning
  solution/
    src/                       unpacked unmanaged solution: table, filters, roles
  src/
    FilteredOwnership.Setup/
      Program.cs               verbs: create-table | create-filter | associate | teardown
      TableBuilder.cs          CreateEntityRequest with OwnershipTypes.Filtered
      FilterBuilder.cs         recordfilter + entityrecordfilter, both via CreateRequest
  http/
    01-create-table.http       Web API equivalents, one file per step
    02-create-recordfilter.http
    03-associate.http
    04-inspect-filters.http    the $expand query that shows the live configuration
  data/
    stores.csv                 sample rows across three cities
  docs/
    cumulative-access.md       the union rule, with the two-role walkthrough
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;http/&lt;/code&gt; folder is there because seeing the raw request makes this model click in a way an SDK wrapper hides: &lt;code&gt;&quot;OwnershipType&quot;: &quot;Filtered&quot;&lt;/code&gt; in a JSON body is more memorable than an enum. &lt;code&gt;04-inspect-filters.http&lt;/code&gt; is the one people will reuse, because it answers &quot;what filters are live on this table right now&quot;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET [Organization URI]/api/data/v9.2/entityrecordfilters?$select=name,objecttypecode,statecode&amp;$filter=objecttypecode eq &#39;new_store&#39; and statecode eq 0&amp;$expand=RecordFilterId($select=displayname,uniquename,fetchxml) HTTP/1.1
Accept: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;statecode eq 0&lt;/code&gt; condition restricts the result to active associations, and the &lt;code&gt;$expand&lt;/code&gt; on &lt;code&gt;RecordFilterId&lt;/code&gt; pulls the actual FetchXML of each filter into the same response, so one call gives you the complete picture of what predicates are in force on a table. That is your first stop when a user reports seeing the wrong rows.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The maker path is the one to start with, and &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/data-platform/filtered-view-record-ownership&quot;&gt;the maker article&lt;/a&gt; has the full click path. These are the steps that carry a decision:&lt;/p&gt;

&lt;p&gt;1) &lt;b&gt;Create the table inside a solution&lt;/b&gt;, and set &lt;b&gt;Record ownership&lt;/b&gt; to &lt;b&gt;Filtered (preview)&lt;/b&gt;. This is the one you cannot take back later.&lt;/p&gt;

&lt;p&gt;2) &lt;b&gt;Build the predicate without writing XML.&lt;/b&gt; Open your model-driven app for editing, open the table, use the &lt;b&gt;Filters&lt;/b&gt; pane to add a condition such as &lt;i&gt;City&lt;/i&gt; equals &lt;i&gt;Redmond&lt;/i&gt;, then select &lt;b&gt;Download FetchXML&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;3) &lt;b&gt;Create a Record Filter&lt;/b&gt; in the solution and paste that FetchXML into it, with a unique name carrying your prefix.&lt;/p&gt;

&lt;p&gt;4) &lt;b&gt;Create an EntityRecordFilter&lt;/b&gt; pointing at that filter, with the table&#39;s logical name as the related entity. This is the row that turns the predicate on.&lt;/p&gt;

&lt;p&gt;5) &lt;b&gt;Grant the filter on a security role&lt;/b&gt;, &lt;b&gt;Read&lt;/b&gt; being the one to start with, and assign the role to a user or team.&lt;/p&gt;

&lt;p&gt;6) &lt;b&gt;Sign in as a real nonadministrator account.&lt;/b&gt; Not your own: the system administrator role carries an &lt;b&gt;All records&lt;/b&gt; filter, so you will see everything and prove nothing.&lt;/p&gt;

&lt;p&gt;You need the system administrator role yourself to configure any of it.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;What I keep coming back to with this one is how much it changes the shape of a conversation I have had dozens of times. For years, &quot;who can see this row&quot; in Dataverse has been a question about &lt;i&gt;structure&lt;/i&gt;: which business unit, which team, which position in the hierarchy, who owns it. Filtered record ownership makes it a question about &lt;i&gt;the data&lt;/i&gt;. The row&#39;s own values decide, and the rule that reads them is a versioned artifact that ships in a solution.&lt;/p&gt;

&lt;p&gt;It does not replace the ownership model we have. Nothing here helps you with a table where the answer really is &quot;the person who owns it and their manager&quot;, and for that, user and team ownership remains exactly the right tool. But for the very large category of requirements that were always secretly about a column, this is a much more direct road, and it means you stop having to reorganize a company&#39;s business units to express a rule about a city name.&lt;/p&gt;

&lt;p&gt;It is preview, so build a lab, not a production rollout. But build the lab. This one is worth your Saturday.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/8499113114456703763/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/8499113114456703763' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8499113114456703763'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8499113114456703763'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/10/power-apps-rows-without-owner.html' title='Power Apps | Rows Without an Owner'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-1574297771853814289</id><published>2026-09-30T07:00:00.000-04:00</published><updated>2026-09-30T07:00:00.117-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><title type='text'>Power Apps | Ask in English, Get Rows Back</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;Ask in English, get rows back: the Dataverse Ask APIs and semantic models&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-17-dataverse-ask-api.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Every so often a question comes back around wearing a different costume. The version I have fielded most often, in one form or another, from just about every organization that has ever put a serious amount of data into Dataverse, goes something like this: &quot;can we let people type a question in plain English and get an answer out of the system, without first teaching them what a logical name is?&quot; It is a perfectly reasonable thing to want. And for a long time the honest answer was that yes, you could build it yourself, with a language model out front, a retrieval layer in the middle, and a great deal of careful plumbing to make sure the thing never returned a row the person asking was not entitled to see. That last part is the part that keeps you up at night.&lt;/p&gt;

&lt;p&gt;Well, a set of four new articles landed in the Power Apps developer documentation, all of them dated September 25, 2026, and together they describe a first-party answer to that question. Microsoft calls it the &lt;b&gt;Dataverse Ask APIs&lt;/b&gt;. Let us take a look.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Four brand new pages, all in preview, all under a new &lt;code&gt;ask&lt;/code&gt; folder in the Dataverse developer documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/ask/&quot;&gt;Use the Dataverse Ask APIs (preview)&lt;/a&gt;, the overview and the operation list.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/ask/get-started&quot;&gt;Get started with the Dataverse Ask APIs (preview)&lt;/a&gt;, covering the environment prerequisite, authentication, privileges, error handling and retry rules.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/ask/manage-semantic-models&quot;&gt;Manage Dataverse semantic models (preview)&lt;/a&gt;, the list, create and delete operations.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/ask/ask-semantic-model&quot;&gt;Ask questions by using a Dataverse semantic model (preview)&lt;/a&gt;, the operation that does the actual work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, the concept first, because there are exactly two nouns here and everything else follows from them.&lt;/p&gt;

&lt;p&gt;A &lt;b&gt;semantic model&lt;/b&gt; is a named scope over one or more Dataverse tables. That is the entire definition. You give it a unique name and a list of table logical names, and Dataverse indexes those tables so that questions can be answered from them. An &lt;b&gt;Ask&lt;/b&gt; request submits a natural-language question against one of those named models and gets back rows, plus a written summary, plus links to the source records.&lt;/p&gt;

&lt;p&gt;So the shape of the thing is: define the scope once, ask against it many times. Makes sense.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Where this lives, and where it does not&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Here is the first detail worth slowing down for, because it will surprise anyone who has spent years in the Web API. The Dataverse Ask APIs are not part of the OData service. They live at their own base address:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://&amp;lt;environment-url&amp;gt;/api/iq/v1.0/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. That is &lt;code&gt;/api/iq/&lt;/code&gt;, not the &lt;code&gt;/api/data/&lt;/code&gt; you have been typing since roughly forever. There is no service document and no metadata document. None of the OData query options work, so &lt;code&gt;$select&lt;/code&gt;, &lt;code&gt;$filter&lt;/code&gt; and &lt;code&gt;$expand&lt;/code&gt; are all off the table. And there are no SDK for .NET or SDK for Python client classes for these operations at all: you bring an &lt;code&gt;HttpClient&lt;/code&gt;, you bring a Microsoft Entra ID access token for the environment, and you talk JSON over HTTPS like it is 2012 again. The documentation is quite direct about the division of labor, and I think it gets it right: use the Web API when your application knows precisely which rows it wants, and use the Ask APIs when the input is a question and you want the service to work out how to answer it.&lt;/p&gt;

&lt;p&gt;Authentication is the same Microsoft Entra ID OAuth you already use for Dataverse. Request the &lt;code&gt;&amp;lt;environment-url&amp;gt;/user_impersonation&lt;/code&gt; scope for a delegated, interactive application, or &lt;code&gt;&amp;lt;environment-url&amp;gt;/.default&lt;/code&gt; for a confidential client running as an application user.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The part that actually matters: whose privileges answer the question&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;I want to pull one sentence out of the get-started article and put it under a spotlight, because it is the sentence that decides whether this feature is usable in a regulated shop or not:&lt;/p&gt;

&lt;p&gt;&quot;The service only uses data that the caller is authorized to read.&quot;&lt;/p&gt;

&lt;p&gt;Record ownership, business unit access, sharing and column-level security all still apply. The answer you get is trimmed to the identity that asked. That is exactly the property that is so expensive to build correctly yourself, and it is the reason a first-party implementation is worth paying attention to even if you already have a working retrieval stack of your own.&lt;/p&gt;

&lt;p&gt;NOTE: an application using server-to-server authentication runs with the privileges of its Dataverse application user, which means a lazily over-privileged application user quietly turns your carefully trimmed question endpoint into a firehose. Grant that application user only what it actually needs. This is not new advice, but the blast radius is larger than usual here, because the caller is no longer writing the query.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Creating a semantic model&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Creating the scope is a single POST. The following creates a model named &quot;Sales pipeline&quot; over the &lt;code&gt;account&lt;/code&gt; and &lt;code&gt;opportunity&lt;/code&gt; tables:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST [Organization URI]/api/iq/v1.0/semanticmodel HTTP/1.1
Authorization: Bearer &amp;lt;access token&amp;gt;
Accept: application/json
Content-Type: application/json

{
  &quot;uniqueName&quot;: &quot;Sales pipeline&quot;,
  &quot;tables&quot;: [
    &quot;account&quot;,
    &quot;opportunity&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. The &lt;code&gt;tables&lt;/code&gt; array takes table &lt;b&gt;logical names&lt;/b&gt;, so &lt;code&gt;account&lt;/code&gt; and &lt;code&gt;opportunity&lt;/code&gt;, and the documentation goes out of its way to say not to use display names, entity set names or collection names. A successful call returns &lt;code&gt;201 Created&lt;/code&gt; with the new model&#39;s &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;uniqueName&lt;/code&gt;. Hold on to both: the &lt;code&gt;uniqueName&lt;/code&gt; is what you pass when you ask a question, and the &lt;code&gt;id&lt;/code&gt; is what you pass when you delete the model. They are not interchangeable, and the delete operation specifically wants the identifier.&lt;/p&gt;

&lt;p&gt;Something else to keep in mind is that semantic model names must be unique within an environment, which makes create the one operation in this API that is not safe to blindly retry. The documentation handles this properly and tells you to list the models first to find out whether your uncertain first attempt actually succeeded before you send it again. That is the kind of small, unglamorous guidance that tells you a real engineer wrote the article.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Asking the question&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;And here is the payoff:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST [Organization URI]/api/iq/v1.0/ask HTTP/1.1
Authorization: Bearer &amp;lt;access token&amp;gt;
Accept: application/json
Content-Type: application/json

{
  &quot;query&quot;: &quot;Which open opportunities have the highest estimated revenue?&quot;,
  &quot;semanticModelName&quot;: &quot;Sales pipeline&quot;,
  &quot;searchMode&quot;: &quot;Auto&quot;,
  &quot;count&quot;: 100
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. The &lt;code&gt;semanticModelName&lt;/code&gt; must be the exact &lt;code&gt;uniqueName&lt;/code&gt; of a model you created. &lt;code&gt;count&lt;/code&gt; caps the rows in this page and defaults to 100. And &lt;code&gt;searchMode&lt;/code&gt; is the interesting knob: &lt;code&gt;Auto&lt;/code&gt; lets the service choose, &lt;code&gt;QuickResponse&lt;/code&gt; favors a faster answer, and &lt;code&gt;ThinkDeeper&lt;/code&gt; favors a more considered one when the question warrants it. The documentation notes plainly that search mode affects response time and that you should set a sensible client timeout and show the user some progress. Good. An interactive control that sits there doing nothing for eight seconds is a support ticket waiting to happen.&lt;/p&gt;

&lt;p&gt;The response comes back looking like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;rawResult&quot;: [
    {
      &quot;opportunity&quot;: &quot;Contoso renewal&quot;,
      &quot;account&quot;: &quot;Contoso&quot;,
      &quot;estimatedRevenue&quot;: 125000
    }
  ],
  &quot;citationLinks&quot;: [
    &quot;[Organization URI]/main.aspx?pagetype=entityrecord&amp;etn=opportunity&amp;id=00000000-0000-0000-0000-000000000002&quot;
  ],
  &quot;summary&quot;: &quot;The Contoso renewal has the highest estimated revenue.&quot;,
  &quot;totalResultCount&quot;: 1,
  &quot;pagingToken&quot;: null
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note, and this is where I would spend my design time. The shape of each object inside &lt;code&gt;rawResult&lt;/code&gt; depends on the question and on the query the service generated to answer it. It is not a fixed row type, and the documentation explicitly warns you not to deserialize every Ask response into one. Treat those items as dynamic JSON objects. Nearly every property in the response is nullable too, &lt;code&gt;summary&lt;/code&gt; and &lt;code&gt;citationLinks&lt;/code&gt; and &lt;code&gt;pagingToken&lt;/code&gt; among them, so display the summary when it is there and do not build a user interface that falls over when it is not.&lt;/p&gt;

&lt;p&gt;Paging works by echoing the token back. When &lt;code&gt;pagingToken&lt;/code&gt; comes back non-null, you send the same question, the same model, the same search mode and the same count, with the token added, and you keep going until the token comes back null or empty. Copy it exactly. Do not decode it, do not modify it, and do not try to construct one.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters, for makers&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The thing a maker should take away is that a semantic model is not a black box you have to accept as-is. Once it exists, you can fine-tune it on the Semantic model page in Power Apps, and there are three levers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Signals&lt;/b&gt;: turn system-inferred signal types such as table summaries and form summaries on or off across tables, exclude tables holding sensitive or irrelevant data, and switch off individual views or relationships that are outdated or misleading.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Glossary&lt;/b&gt;: add the business vocabulary the system cannot infer. Acronyms, internal product names, the words your organization uses that nobody else does. This is the lever that decides whether the thing understands your people when they ask about &quot;an RFA&quot; or &quot;the Q3 book&quot;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Refresh&lt;/b&gt;: the semantic model regenerates automatically every 12 hours, and you can trigger a manual regeneration when you need a change to land now rather than tonight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That 12-hour cadence is worth writing on the whiteboard before anybody demos this to a business sponsor. Add a glossary term at nine in the morning, and without a manual regeneration you are potentially explaining all day why the agent still does not know what an RFA is.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters, for professional developers&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Three things, in the order I would worry about them.&lt;/p&gt;

&lt;p&gt;First, storage. When you create a semantic model, Dataverse indexes the tables you selected, and that indexing consumes Dataverse &lt;i&gt;database&lt;/i&gt; storage. You can see the consumption in the &lt;b&gt;DataverseSearch&lt;/b&gt; table, formerly called RelevanceSearch, and it counts toward database storage on the Summary and Dataverse tabs in the admin center. So a semantic model is not a free logical view. Scoping a model to &quot;every table we have, just in case&quot; has a bill attached, which is a good reason to do what the documentation suggests and select only the tables relevant to the questions you actually need answered.&lt;/p&gt;

&lt;p&gt;Second, throttling. The Ask API sets its own limit, deliberately lower than the general Dataverse service protection limits: &lt;b&gt;30 requests per user, per organization, per minute&lt;/b&gt;. Go past it and you get a &lt;code&gt;429&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header carrying the number of seconds to wait. Thirty a minute is generous for a human typing questions and very tight indeed for anything resembling a batch job, which tells you plainly what this API was built for.&lt;/p&gt;

&lt;p&gt;Third, dependencies. Before you change or remove a model, you can ask which applications and agents are using it by adding a query parameter:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET [Organization URI]/api/iq/v1.0/semanticmodel?include=botinfo HTTP/1.1
Authorization: Bearer &amp;lt;access token&amp;gt;
Accept: application/json
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. With &lt;code&gt;include=botinfo&lt;/code&gt;, each model in the response carries the model-driven apps that reference it and a &lt;code&gt;bots&lt;/code&gt; collection naming the bot components and agents that use it. The value is not case-sensitive and &lt;code&gt;include&lt;/code&gt; accepts a comma-separated list, which suggests more values are coming. Run this before you delete anything. And keep in mind that the delete operation only removes models &lt;i&gt;you&lt;/i&gt; created through the create operation: models associated with an app module, a bot or an agent cannot be deleted through this API at all.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo I would build&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The demo concept I like here is a small, boring command-line tool, because a boring tool makes the interesting part visible. Call it &lt;code&gt;dvask&lt;/code&gt;. It does four things: creates a semantic model from a list of table logical names, lists the models in the environment, asks a question and prints both the summary and the rows, and deletes a model by identifier.&lt;/p&gt;

&lt;p&gt;The demo that lands, though, is not the happy path. It is running the exact same question twice, from two identities: once as a system administrator, and once as a salesperson whose security role only reaches their own business unit. Same question, same semantic model, two different answers, both correct. That is the whole value proposition of a first-party implementation in one side-by-side screenshot, and it is the thing that is genuinely hard to build yourself.&lt;/p&gt;

&lt;p&gt;The second demo, for the developers in the room, is the paging loop, because it is the one piece of the protocol where it is easy to write a bug that only shows up on the third page in production.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The companion repo&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Sketching this the way I would lay it out, in the project-orion-lab style:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dataverse-ask-lab/
  README.md                 prerequisites, the Work IQ toggle, role requirements
  src/
    DvAsk/
      Program.cs            verb dispatch: create | list | ask | delete
      AskClient.cs          HttpClient wrapper over /api/iq/v1.0/
      TokenProvider.cs      MSAL: delegated and client-credentials flows
      Models.cs             request types; responses stay JsonDocument
  samples/
    sales-pipeline.json     semantic model definition: account, opportunity
    questions.txt           the question set used in the demo
  scripts/
    compare-identities.ps1  same question, two identities, side by side
  docs/
    throttling.md           the 30/minute limit and the backoff implementation
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note about that layout. &lt;code&gt;Models.cs&lt;/code&gt; deliberately holds request types only: the responses stay as &lt;code&gt;JsonDocument&lt;/code&gt;, because as we established, &lt;code&gt;rawResult&lt;/code&gt; has no fixed shape and pretending otherwise is how you ship something that works beautifully against your demo data and falls apart on the customer&#39;s. &lt;code&gt;compare-identities.ps1&lt;/code&gt; is the star of the repo even though it is the smallest file in it. And &lt;code&gt;throttling.md&lt;/code&gt; earns its place because the retry guidance in the documentation is unusually specific, and specific guidance deserves a reference implementation: honor &lt;code&gt;Retry-After&lt;/code&gt; first, fall back to exponential backoff with jitter, cap the attempts and the total elapsed time, do not fire retries in parallel, and never retry a &lt;code&gt;400&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;1) Go to the Power Platform admin center, select your environment, and turn on &lt;b&gt;Business Applications in Work IQ for this environment&lt;/b&gt;. Nothing in this API works until that is on, and a request against an environment without it comes back as a &lt;code&gt;400 Bad Request&lt;/code&gt; rather than anything more helpful.&lt;/p&gt;

&lt;p&gt;2) Register an application in Microsoft Entra ID. For an interactive tool, configure delegated access and request the &lt;code&gt;&amp;lt;environment-url&amp;gt;/user_impersonation&lt;/code&gt; scope. For a service, create a Dataverse application user and request &lt;code&gt;&amp;lt;environment-url&amp;gt;/.default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;3) In the Power Platform admin center, give the calling identity a security role that can create and delete semantic models. The documentation names four that already include the privileges: &lt;b&gt;Dataverse Search Role&lt;/b&gt;, &lt;b&gt;Environment Maker&lt;/b&gt;, &lt;b&gt;System Administrator&lt;/b&gt; and &lt;b&gt;System Customizer&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;4) Make sure the identity that will ask questions has &lt;b&gt;Read&lt;/b&gt; access to every table and column in the model. This is a separate concern from step 3: managing models and querying through them are different privileges.&lt;/p&gt;

&lt;p&gt;5) POST to &lt;code&gt;/api/iq/v1.0/semanticmodel&lt;/code&gt; with a &lt;code&gt;uniqueName&lt;/code&gt; and a &lt;code&gt;tables&lt;/code&gt; array of logical names. Save the &lt;code&gt;id&lt;/code&gt; and the &lt;code&gt;uniqueName&lt;/code&gt; from the &lt;code&gt;201 Created&lt;/code&gt; response.&lt;/p&gt;

&lt;p&gt;6) POST to &lt;code&gt;/api/iq/v1.0/ask&lt;/code&gt; with your &lt;code&gt;query&lt;/code&gt; and the &lt;code&gt;semanticModelName&lt;/code&gt;. Read &lt;code&gt;summary&lt;/code&gt; for the prose and &lt;code&gt;rawResult&lt;/code&gt; for the rows.&lt;/p&gt;

&lt;p&gt;7) To tune it, go to &lt;a href=&quot;https://make.powerapps.com&quot;&gt;make.powerapps.com&lt;/a&gt; and open the Semantic model page, then work through &lt;b&gt;Signals&lt;/b&gt; and &lt;b&gt;Glossary&lt;/b&gt;. Trigger a manual regeneration when you want your changes to take effect before the next automatic 12-hour refresh.&lt;/p&gt;

&lt;p&gt;8) Before deleting anything, GET &lt;code&gt;/api/iq/v1.0/semanticmodel?include=botinfo&lt;/code&gt; and read the &lt;code&gt;bots&lt;/code&gt; collection to see what depends on the model.&lt;/p&gt;

&lt;p&gt;NOTE: the documentation does not give a click path for creating a semantic model from the Power Apps maker portal, only for fine-tuning one that already exists. The create, list and delete operations are documented as API operations. So for now, treat the API as the way models get created and the maker portal as the way they get tuned. If a maker-portal creation path exists, these four articles do not describe it, and I would rather tell you that than guess at a menu.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Two closing thoughts.&lt;/p&gt;

&lt;p&gt;The first is that this is preview documentation, with everything that implies. Property names, the &lt;code&gt;searchMode&lt;/code&gt; values and the shape of the response are all fair game for change before this reaches general availability, and the articles are marked accordingly. Build the client, do not build the company on it. Not yet.&lt;/p&gt;

&lt;p&gt;The second is more of an observation. Look at where these articles were filed. Not under a copilot or an AI heading, but in the Dataverse developer documentation, alongside the Web API, with an error-code table, a retry policy, service protection limits and operation-safety guidance for every verb. Natural-language querying, in other words, is being documented as an ordinary data access API that happens to take a sentence as input. That is a quiet shift in framing, and it is a very healthy one. It is a good deal easier to reason about something when it arrives with a &lt;code&gt;429&lt;/code&gt; and a &lt;code&gt;Retry-After&lt;/code&gt; header attached.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/1574297771853814289/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/1574297771853814289' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1574297771853814289'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1574297771853814289'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/09/power-apps-ask-in-english-get-rows-back.html' title='Power Apps | Ask in English, Get Rows Back'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-4103367055235025259</id><published>2026-09-21T07:00:00.000-04:00</published><updated>2026-09-21T07:00:00.115-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><title type='text'>Copilot Studio | The Agent Your Flow Can&#39;t Call</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;The agent your flow can&#39;t call: Copilot Studio harness choice&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-14-copilot-harness-split.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;There is a particular kind of bad afternoon in this business. You have built the thing, it works, you demo it, everyone is happy. Then somebody asks the obvious follow-up question, the one that was always coming: &quot;great, now can the nightly flow call it?&quot; And you go to wire it up and discover that the answer is no, not because you did anything wrong, but because of a choice you made on day one without knowing it was a choice.&lt;/p&gt;

&lt;p&gt;Microsoft published a short new article in mid-August that will save somebody that afternoon. It is worth reading before you build your next agent rather than after.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The article is &lt;a href=&quot;https://learn.microsoft.com/en-us/power-automate/call-copilot-studio-agent&quot;&gt;Call Microsoft Copilot Studio agents from Power Automate&lt;/a&gt;, new in the Power Automate documentation and last updated August 18, 2026. It opens with a two-row table, and that table is the whole post:&lt;/p&gt;

&lt;blockquote&gt;Standard harness: Call the agent from a Power Automate cloud flow by using the Microsoft Copilot Studio connector.&lt;br/&gt;&lt;br/&gt;GitHub Copilot harness: Use the agent node in a Copilot Studio workflow. You can&#39;t call this type of agent from a Power Automate cloud flow by using the Microsoft Copilot Studio connector.&lt;/blockquote&gt;

&lt;p&gt;Now, a &lt;b&gt;harness&lt;/b&gt; needs defining, because the word is new enough that plenty of people building agents right now have never seen it. Per the &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/harnesses-overview&quot;&gt;harnesses overview&lt;/a&gt;, you design the agent and the model supplies the reasoning, and the harness is the runtime sitting between the two: it decides when to call the model, what to send it, how to interpret what comes back, and which tools to call. Everything you build in Copilot Studio runs on one.&lt;/p&gt;

&lt;p&gt;There are three. The &lt;b&gt;GitHub Copilot harness&lt;/b&gt; powers agents and workflows built for reasoning-heavy, multistep work, and bills through Copilot Credits on usage. The &lt;b&gt;standard harness&lt;/b&gt; powers rule-based agents and agent flows, where you define the topics and paths and get predictable behavior. The &lt;b&gt;Copilot chat harness&lt;/b&gt; is for extending Microsoft 365 Copilot.&lt;/p&gt;

&lt;p&gt;So the harness is not a setting you flip later. It determines how your agent reasons, how you are billed, and, as of this article, what can call it.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;This was not one article&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Here is the part that makes me confident this is a real platform move rather than a documentation footnote. That new Power Automate page did not arrive alone. Across the same week, roughly a hundred Copilot Studio articles were touched, and the substantive edits among them all point the same direction: Microsoft is retiring the informal vocabulary and replacing it with harness names everywhere.&lt;/p&gt;

&lt;p&gt;Pages that used to say &quot;the classic experience&quot; and &quot;the new agent experience&quot; now say &quot;the standard harness&quot; and &quot;agents powered by the GitHub Copilot harness.&quot; The telemetry articles were split along the same line: agent-level telemetry is now documented as available &lt;i&gt;only&lt;/i&gt; with the standard harness, while environment-level telemetry works with both. The article on adding tools to an agent was amended to say that coding-agent skills can only be added to agents powered by the GitHub Copilot harness. Even the old skills article was reworded to name both harnesses explicitly.&lt;/p&gt;

&lt;p&gt;One renamed page is housekeeping. A hundred pages renamed in one week, with capability boundaries drawn along the new names, is a platform telling you that this distinction is now load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;makers&lt;/b&gt;, the practical consequence is a question you now have to ask at the start: does anything else need to call this agent? If a scheduled flow, an approval process, or an existing automation has to invoke it, you want the standard harness, because that is the one the Power Automate connector can reach. If the agent is going to be talked to by humans and needs to reason its way through messy multistep work, the GitHub Copilot harness is the more capable runtime, and you accept that your integration path runs through Copilot Studio workflows instead.&lt;/p&gt;

&lt;p&gt;You may be asking whether that is a real constraint or just a temporary gap. The documentation does not say, and I am not going to guess. What it does say is what works today, and today the connector supports one harness and not the other.&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;professional developers&lt;/b&gt;, the sharper point is that this is an architectural decision wearing the costume of a product setting. It sits alongside two others made at the same moment: your billing model, since the GitHub Copilot harness runs on Copilot Credits and the standard harness runs on the licensing model, and your observability model, since agent-level telemetry export is standard-harness only. Three consequences, one choice, made in a dropdown before you have written anything.&lt;/p&gt;

&lt;p&gt;NOTE: there is a workaround, and it is a legitimate one rather than a hack, but it does change your topology. You call a GitHub Copilot harness agent from an &lt;b&gt;agent node&lt;/b&gt; inside a Copilot Studio workflow. That means the orchestration lives in Copilot Studio rather than in Power Automate. If your organization&#39;s automation estate, source control, and deployment pipeline are all built around cloud flows, that is not a small relocation.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo concept&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Build the same trivial agent twice, once on each harness, and then try to call both from one cloud flow. The standard harness agent answers. The other one is not selectable, because it never appears in the connector&#39;s agent picker at all.&lt;/p&gt;

&lt;p&gt;That is the demonstration worth recording, because it is a negative result and negative results are what documentation tends to bury. Then build the workflow-plus-agent-node path for the second agent and show it working. Two agents, two integration paths, one screen recording, and nobody on your team ever has that bad afternoon.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;First, the supported path. These steps are quoted from the new article.&lt;/p&gt;

&lt;p&gt;1) Sign in to Power Automate.&lt;/p&gt;

&lt;p&gt;2) Create a cloud flow, or open an existing cloud flow and select &lt;b&gt;Edit&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;3) In the designer, select the plus sign (&lt;b&gt;+&lt;/b&gt;) where you want to call the agent, and then select &lt;b&gt;Add an action&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;4) Search for &lt;b&gt;Microsoft Copilot Studio&lt;/b&gt;, and then select &lt;b&gt;Execute Agent and wait&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;5) If prompted, sign in with Microsoft Entra ID to create a connection.&lt;/p&gt;

&lt;p&gt;6) In &lt;b&gt;Agent&lt;/b&gt;, select the published standard harness agent that you want to call.&lt;/p&gt;

&lt;p&gt;7) In &lt;b&gt;Message&lt;/b&gt;, enter the instructions for the agent. You can add dynamic content from earlier steps in the flow.&lt;/p&gt;

&lt;p&gt;8) Configure the optional inputs your scenario needs: &lt;b&gt;Locale&lt;/b&gt; (the language of the message, as a BCP-47 locale), &lt;b&gt;Conversation ID&lt;/b&gt; (an existing conversation ID, to continue an agent conversation), and &lt;b&gt;Environment ID&lt;/b&gt; (the environment that contains the agent).&lt;/p&gt;

&lt;p&gt;9) Add any actions that should use the agent&#39;s response, then select &lt;b&gt;Save&lt;/b&gt; and test the flow.&lt;/p&gt;

&lt;p&gt;A few things to note about that sequence. The word &lt;b&gt;published&lt;/b&gt; in step 6 is not filler: an unpublished agent will not be there to pick, and that is the first thing to check when the dropdown looks empty. &lt;b&gt;Execute Agent and wait&lt;/b&gt; blocks the flow until the agent responds, which is what you want when the next action consumes the answer; the article notes that if you do not need to wait, the plain &lt;b&gt;Execute Agent&lt;/b&gt; action sends the message and returns the conversation ID instead. And that conversation ID is the interesting output, because passing it back in on the next call is what turns a series of one-shot invocations into an actual conversation. The action surfaces the last response, a list of responses, and the conversation ID as dynamic content.&lt;/p&gt;

&lt;p&gt;Now the other path, for a GitHub Copilot harness agent:&lt;/p&gt;

&lt;p&gt;1) In Copilot Studio, go to &lt;b&gt;Workflows&lt;/b&gt; and open an existing workflow, or create a new one.&lt;/p&gt;

&lt;p&gt;2) On the &lt;b&gt;Add&lt;/b&gt; panel, select the &lt;b&gt;Agent&lt;/b&gt; icon.&lt;/p&gt;

&lt;p&gt;3) Under &lt;b&gt;Agent&lt;/b&gt;, select &lt;b&gt;An existing agent&lt;/b&gt;, and then select the published GitHub Copilot harness agent you want to call.&lt;/p&gt;

&lt;p&gt;4) In &lt;b&gt;Message&lt;/b&gt;, enter the instructions for this run, adding dynamic content from earlier workflow steps as needed.&lt;/p&gt;

&lt;p&gt;5) Add the later workflow steps that use the agent response, and test the workflow before you publish it.&lt;/p&gt;

&lt;p&gt;Notice the symmetry. Both paths are an agent-invoking step with a message and a response, and both want the target published first. The difference is entirely which product hosts the orchestration, which is exactly why the constraint is easy to miss until it bites: the shape of the work is identical, only the address changes.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo sketch&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;A small comparison lab. Two exported solutions, &lt;code&gt;agent-standard/&lt;/code&gt; and &lt;code&gt;agent-ghcopilot/&lt;/code&gt;, each holding the same deliberately boring agent so the harness is the only variable. A &lt;code&gt;flow-caller/&lt;/code&gt; solution with the cloud flow that calls the standard harness agent through &lt;b&gt;Execute Agent and wait&lt;/b&gt;. A &lt;code&gt;workflow-caller/&lt;/code&gt; solution with the Copilot Studio workflow that reaches the other one through an agent node.&lt;/p&gt;

&lt;p&gt;Then the file that makes the repository worth cloning: a &lt;code&gt;MATRIX.md&lt;/code&gt; recording what each harness supports, with the observed result and the date beside every row. Connector callable, agent-level telemetry, coding-agent skills, billing model. Date every row, because this is a moving platform and an undated compatibility matrix is worse than none at all. It ages into confident misinformation.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The change itself is one table in one short article. What makes it worth your time is the timing: it landed in the same week that a hundred sibling pages were rewritten to make harness the organizing noun of the whole product. That is not a coincidence, and it is a reasonable bet that more capability boundaries get drawn along this line before the year is out.&lt;/p&gt;

&lt;p&gt;So ask the integration question first. Before the topics, before the knowledge sources, before anybody sees a demo: what needs to be able to call this? The answer picks your harness, and the harness picks a great deal else.&lt;/p&gt;

&lt;p&gt;What I learned from this exercise: the most expensive decisions on a platform are rarely the ones that look like decisions. They look like defaults.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/4103367055235025259/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/4103367055235025259' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/4103367055235025259'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/4103367055235025259'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/09/copilot-studio-agent-your-flow-cant-call.html' title='Copilot Studio | The Agent Your Flow Can&#39;t Call'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-4424552724186502185</id><published>2026-09-17T07:00:00.000-04:00</published><updated>2026-09-17T07:00:00.116-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><title type='text'>Power Apps | Seven Skills, One Prompt: The Dataverse Plugin for Coding Agents</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;Seven skills, one prompt: the Dataverse plugin for AI coding agents&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-13-dataverse-agent-plugin.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Anyone who has built anything nontrivial on Microsoft Dataverse knows the tax. You want to stand up a small data model, load a few thousand rows into it, and move the whole thing to a test environment. That is one sentence to describe and four tools to execute: the SDK for Python for the bulk write, the Dataverse CLI for authentication, the Power Platform CLI for the solution export, and the Web API for whatever the other three do not cover. None of it is hard. All of it is friction, and the friction is mostly syntax you have to remember rather than decisions you have to make.&lt;/p&gt;

&lt;p&gt;Well, in late August Microsoft published something that goes after exactly that friction, and it arrived quietly, as two new articles with no fanfare attached.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Two new pages landed in the &lt;code&gt;MicrosoftDocs/powerapps-docs&lt;/code&gt; repository, both dated 07/31/2026 and both published in the week ending August 24:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/agents-plugin/&quot;&gt;Microsoft Dataverse plugin for AI coding agents (preview)&lt;/a&gt;, the overview and install article.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/agents-plugin/reference&quot;&gt;Microsoft Dataverse plugin for AI coding agents reference (preview)&lt;/a&gt;, the per-skill reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are new files, not edits, and the developer table of contents was amended to carry them, which is the tell that this is a shipped surface rather than a stray draft. The source is open, in the &lt;a href=&quot;https://github.com/microsoft/Dataverse-skills&quot;&gt;microsoft/Dataverse-skills&lt;/a&gt; repository, so you can read every instruction the agent is given before you let it near an environment. I would encourage you to do exactly that.&lt;/p&gt;

&lt;p&gt;Now, the term &lt;b&gt;coding agent&lt;/b&gt; deserves a definition, because it is doing a lot of work in these articles. A coding agent is an AI assistant that runs inside your development environment, reads and writes files in your project, and executes commands on your behalf. GitHub Copilot, Claude Code, Cursor and Codex are the four named as supported. The distinction that matters: a coding agent runs commands, where a chat assistant only suggests them.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The Solution: a plugin is instructions, not code&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Here is the part worth slowing down for, because it is easy to misread this as &quot;Microsoft shipped another connector.&quot;&lt;/p&gt;

&lt;p&gt;It did not. The documentation is precise: this is an open-source set of &lt;i&gt;instructions&lt;/i&gt; that help a supported coding agent perform Dataverse development and administration tasks. It does not replace the Dataverse development tools. It tells the agent which one to reach for.&lt;/p&gt;

&lt;p&gt;You may be asking why that is worth shipping at all. Very simple: tool selection is the hard part. An agent that knows the Web API exists will cheerfully use it to insert five hundred rows one HTTP call at a time, and it will be correct, and it will be terrible. The plugin&#39;s job is to know that five hundred rows means the Python SDK and &lt;code&gt;CreateMultiple&lt;/code&gt;, that a schema question means MCP, and that a solution export means the Power Platform CLI. The routing rule is stated plainly: MCP for interactive queries and small record sets, the Python SDK for bulk operations, transformations, paging and analytics.&lt;/p&gt;

&lt;p&gt;Seven skills ship with it. &lt;code&gt;dv-overview&lt;/code&gt; loads first and carries the tool-selection guidance; every other skill sits behind it. &lt;code&gt;dv-connect&lt;/code&gt; sets up the workspace. &lt;code&gt;dv-query&lt;/code&gt; reads and analyzes, up to server-side aggregation and pandas DataFrames. &lt;code&gt;dv-data&lt;/code&gt; handles creates, updates, upserts, CSV imports and sample data. &lt;code&gt;dv-metadata&lt;/code&gt; builds tables, columns, relationships, alternate keys, forms and views. &lt;code&gt;dv-solution&lt;/code&gt; runs the solution lifecycle. &lt;code&gt;dv-admin&lt;/code&gt; and &lt;code&gt;dv-security&lt;/code&gt; cover environment settings, retention, bulk delete, audit configuration and role assignment. A single request can pull several at once.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The two design decisions I would point at&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The first is buried in &lt;code&gt;dv-metadata&lt;/code&gt;, and every Dataverse developer will recognize it. The skill instructs the agent to follow an &lt;b&gt;environment-first metadata workflow&lt;/b&gt;: make the change in Dataverse through a managed API, export and unpack the solution, then commit the generated files. And then:&lt;/p&gt;

&lt;blockquote&gt;The agent shouldn&#39;t create new Dataverse components by writing solution XML manually.&lt;/blockquote&gt;

&lt;p&gt;That is the correct instinct, written down. Hand-authored solution XML is how you get components that import cleanly and then behave strangely, and an agent left alone would absolutely write it, because from a language model&#39;s point of view XML is just text and text is easy. A small sentence carrying a lot of hard-won experience.&lt;/p&gt;

&lt;p&gt;The second is the guardrail model, and it is more honest than I expected. The documentation states that Dataverse enforces the platform controls, that the agent-level guardrails guide how the agent plans and confirms, and that they &quot;aren&#39;t a substitute for reviewing the proposed action.&quot; Then it names the boundary that actually protects you: the agent cannot exceed the permissions of the authenticated identity. Table privileges, field-level security, ownership and sharing, and administrator permissions are enforced on every request.&lt;/p&gt;

&lt;p&gt;NOTE: the agent-level confirmations are still worth knowing. Before the first operation against an environment in a session, the agent must show the target environment URL and get confirmation. Before creating a publisher prefix it must show existing publishers, because a prefix is permanent. And an unfiltered bulk delete, which bypasses the recycle bin, requires an explicit acknowledgment naming the table logical name and stating that &lt;b&gt;all records&lt;/b&gt; should be deleted. That is the right shape for a destructive operation: make the human type the dangerous part.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;makers&lt;/b&gt;, the honest answer is: not yet, and that is fine. This is a developer surface, needing a local toolchain, a terminal and a coding agent. If your Dataverse work happens entirely in make.powerapps.com, nothing here changes your Tuesday. What it does change is who can help you, and how fast.&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;professional developers&lt;/b&gt;, there are two claims here and they are not the same size. The small one is convenience, real but unremarkable. The large one is that Microsoft has published, in the open, its own opinion about which Dataverse tool to use for which job, in a form a machine reads. That opinion has existed for years, scattered across documentation, conference sessions, and the accumulated instinct of people who have been burned enough times. Written down as executable guidance, it becomes reviewable, forkable, and arguable in a way instinct never is.&lt;/p&gt;

&lt;p&gt;Two cautions, both from the documentation rather than invented by me. This is &lt;b&gt;preview&lt;/b&gt;. And Dataverse MCP access carries an authorization chain no plugin can shortcut: the developer authenticates, a tenant administrator consents to the MCP client application, and an environment administrator allows that application in each target environment. The connection flow tells you what is needed; it does not bypass administrator approval. In a governed tenant, that is a ticket, not a command.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo concept&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Not &quot;watch the agent make a table,&quot; because that is a parlor trick that proves nothing you could not do faster by hand. The demonstration worth building is the four-tool problem from the top of this post, executed as one prompt, with the tool selection made visible. Build a small ticketing model, load a few hundred rows, package it, deploy it to a second environment. Then go back and look at which skill did what. If &lt;code&gt;dv-data&lt;/code&gt; reached for &lt;code&gt;CreateMultiple&lt;/code&gt; rather than five hundred single inserts, the routing did its job, and that is the whole point in one observation.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;1) Install the plugin. The command differs by agent:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GitHub Copilot:  /plugin install dataverse@awesome-copilot
Claude Code:     /plugin install dataverse@claude-plugins-official
Cursor:          /add-plugin dataverse&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. These are slash commands typed into the agent&#39;s own chat, not shell commands. The part after the &lt;code&gt;@&lt;/code&gt; is the marketplace the plugin comes from, and it differs because each vendor runs its own. In GitHub Copilot you can also run &lt;code&gt;/plugin&lt;/code&gt;, open &lt;b&gt;Discover&lt;/b&gt;, and search for &lt;b&gt;dataverse&lt;/b&gt;; in Cursor, go to &lt;b&gt;Settings&lt;/b&gt; &amp;gt; &lt;b&gt;Plugins&lt;/b&gt;, search for &lt;b&gt;Dataverse&lt;/b&gt;, open &lt;b&gt;Microsoft Dataverse&lt;/b&gt;, and select &lt;b&gt;Add to Cursor&lt;/b&gt;. Codex is the odd one out, having no default marketplace carrying this plugin: open &lt;b&gt;Plugins&lt;/b&gt; &amp;gt; &lt;b&gt;Add marketplace&lt;/b&gt;, set &lt;b&gt;Source&lt;/b&gt; to &lt;code&gt;https://github.com/microsoft/Dataverse-skills.git&lt;/code&gt;, leave &lt;b&gt;Git ref&lt;/b&gt; and &lt;b&gt;Sparse paths&lt;/b&gt; empty, add the marketplace, then install the &lt;code&gt;dataverse&lt;/code&gt; plugin from it.&lt;/p&gt;

&lt;p&gt;2) Connect, then verify, in plain language:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Connect to my Dataverse environment.
List the tables in my Dataverse environment.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first triggers &lt;code&gt;dv-connect&lt;/code&gt;, which checks whether the workspace is already configured, verifies and installs the required tools, discovers your environments and asks you to select one, configures Dataverse CLI and Power Platform CLI authentication, creates the local workspace configuration while excluding credentials and generated artifacts from source control, registers the Dataverse MCP server, and verifies the connection. That is nine steps behind one sentence, and the one I would call out is the source-control exclusion, because it is the step a hurried human skips. Note also that some coding agents must be &lt;b&gt;restarted&lt;/b&gt; after MCP registration before the MCP tools appear. If the second command comes back saying it has no tools, that is why.&lt;/p&gt;

&lt;p&gt;3) Do real work. This prompt is drawn from tests in the repository, which is a useful signal: it is known to work rather than merely plausible.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Write a Python script that efficiently creates 500 ticket records in my
Dataverse new_ticket table. Each ticket should have a unique title and a
priority value. Optimize for the fewest HTTP calls.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. &quot;Optimize for the fewest HTTP calls&quot; is the phrase doing the work, pushing the agent from a naive loop to &lt;code&gt;CreateMultiple&lt;/code&gt; with batching. Change &lt;code&gt;new_ticket&lt;/code&gt; to your own logical name, publisher prefix and all. And notice it asks for a &lt;i&gt;script&lt;/i&gt;, so you get an artifact you can read, commit, and run again, rather than an action that already happened.&lt;/p&gt;

&lt;p&gt;4) Package and move it:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Pack the CustomerService solution, import it into the test environment, and
verify that the ticket table, main form, and active tickets view are available.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The verification clause is not decoration. &lt;code&gt;dv-solution&lt;/code&gt; supports post-deployment validation of tables, forms, views, role assignments and import errors, so asking for it is what turns &quot;the import returned success&quot; into &quot;the components are actually there.&quot; Two different claims, and only one is worth trusting.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo sketch&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;A lab repository that is honest about being a lab. A &lt;code&gt;prompts/&lt;/code&gt; folder with the exact prompts in order, so someone else can reproduce the run. A &lt;code&gt;transcripts/&lt;/code&gt; folder with what the agent actually did, because the interesting artifact is the routing rather than the result: which skill was selected, which tool it chose, whether it batched. A &lt;code&gt;solution/&lt;/code&gt; folder holding the solution exactly as &lt;code&gt;dv-solution&lt;/code&gt; unpacked it, so the environment-first workflow shows up in the diff instead of being asserted in a README. And a short &lt;code&gt;FINDINGS.md&lt;/code&gt; recording where the agent chose well and where it did not, because a lab that records only successes is marketing.&lt;/p&gt;

&lt;p&gt;Point the README at a developer environment from the Power Apps Developer Plan, which the documentation names as the appropriate nonproduction option, and say so loudly. Nobody should be learning this against production.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;What I keep coming back to is not the convenience. It is that the routing table is public. Microsoft has written down, in a repository you can read, which Dataverse tool it thinks you should use for which job and why. Whether or not you install the plugin, that is worth twenty minutes of your time, because it is the closest thing we have had to an official answer to a question this platform has been quietly asking developers for a decade.&lt;/p&gt;

&lt;p&gt;And it is preview, so it will change. Read the skills before you trust them, run against a developer environment first, and remember that the confirmations are guidance to the agent rather than enforcement by the platform. The enforcement is your identity&#39;s privileges, and that is the line that actually holds.&lt;/p&gt;

&lt;p&gt;What I learned from this exercise: the valuable part of an agent integration is rarely the automation. It is the accumulated judgment somebody had to write down to make the automation behave.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/4424552724186502185/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/4424552724186502185' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/4424552724186502185'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/4424552724186502185'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/09/power-apps-seven-skills-one-prompt.html' title='Power Apps | Seven Skills, One Prompt: The Dataverse Plugin for Coding Agents'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-4792628291846088702</id><published>2026-09-14T07:00:00.000-04:00</published><updated>2026-09-14T07:00:00.115-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><title type='text'>Power Automate | Licensing the Flow Nobody Owns</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;The flow nobody owns: service principals, licensed at last&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-12-sp-flow-license.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;If you have done any serious application lifecycle management work in Power Platform, you know the pattern: you move flow ownership off a human being and onto a service principal, because people change roles, people leave, and a mission critical flow should not stop working because somebody&#39;s manager reassigned a license on a Tuesday. It is good practice, and I have watched it save projects.&lt;/p&gt;

&lt;p&gt;It also had a sharp edge, and in mid-August Microsoft filed it down.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Here is the edge. A service principal application user is a non-interactive user, which means you cannot assign it a license. So the moment your service principal owned flow used anything premium (a premium connector, a custom connector, an HTTP action), you had a compliance problem. Your options were a Power Automate Process license assigned to the flow itself, which requires the flow to be solution-aware, or a Process license on a flow group sharing that capacity. Both are real answers. Both cost money that, in a lot of cases, you had already spent on a perfectly good user license sitting right there.&lt;/p&gt;

&lt;p&gt;And if you did neither, the flow got suspended, with a message that will be familiar to anyone who has been on the receiving end of it:&lt;/p&gt;

&lt;blockquote&gt;This flow was suspended because flows owned by service principals are not compliant.&lt;/blockquote&gt;

&lt;p&gt;Now, as of &lt;a href=&quot;https://learn.microsoft.com/en-us/power-automate/assign-user-license-service-principal-flow&quot;&gt;a new article published on August 15, 2026&lt;/a&gt;, there is a third option: designate a licensed &lt;i&gt;user&lt;/i&gt;, and the flow runs under that user&#39;s Power Automate entitlement while the service principal remains the owner.&lt;/p&gt;

&lt;p&gt;This is not a documentation-only change. The existing &lt;a href=&quot;https://learn.microsoft.com/en-us/power-automate/service-principal-support&quot;&gt;Support for service principal owned flows&lt;/a&gt; article was edited the same week, and where it listed two licensing routes it now lists three. Two independent pages moving the same direction in the same week is usually how you tell something actually shipped.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;What you get, and what it costs you&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Designating a user license means the flow runs under that user&#39;s entitlement, and &lt;b&gt;that user&#39;s action limits apply&lt;/b&gt;. A Process license, by contrast, is capacity attached to the flow itself, entitling it to 250,000 actions per day independent of any user, and it can be shared across up to 25 flows in a flow group.&lt;/p&gt;

&lt;p&gt;So the new option is cheaper and simpler, and you pay for it in headroom. A high-volume integration flow that would chew through 250,000 daily actions is not a good candidate for somebody&#39;s personal entitlement. A nightly reconciliation flow that fires forty times a day absolutely is.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The two prerequisites that will bite you&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The first: &lt;b&gt;the designated user must be a co-owner of the flow.&lt;/b&gt; Not merely a licensed user in the tenant. A co-owner. If they are not, share the flow with them first.&lt;/p&gt;

&lt;p&gt;The second is the important one: &lt;b&gt;the user&#39;s license must cover every premium feature the flow uses.&lt;/b&gt; The documentation names premium connectors, custom connectors and HTTP actions, says a Power Automate Premium license covers these, and then says something worth reading twice: a Microsoft 365 seeded license does not, and the flow stays noncompliant if you designate a user who only has one.&lt;/p&gt;

&lt;p&gt;That is the failure mode. You designate a user, the save succeeds, the UI looks right, and the flow is still out of compliance because the person you picked has a seeded license. If you are troubleshooting a flow that stayed suspended after you &quot;fixed&quot; it, check that first.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;For makers, the ALM-correct thing is no longer the expensive thing. Moving ownership to a service principal used to come with an invoice attached, and that invoice was enough to talk a lot of teams out of doing it properly. The workflow itself is short: open the flow&#39;s &lt;b&gt;Details&lt;/b&gt; page, find &lt;b&gt;User license&lt;/b&gt; in the licensing section, pick the user, save.&lt;/p&gt;

&lt;p&gt;For professional developers, two things. It is cleanly scriptable, because the designation is stored in Dataverse as the &lt;code&gt;Licensee&lt;/code&gt; column on the flow&#39;s row in the Process (&lt;code&gt;workflow&lt;/code&gt;) table, a lookup to a User (&lt;code&gt;systemuser&lt;/code&gt;) record. Setting it programmatically is, in the documentation&#39;s words, equivalent to using the Details page.&lt;/p&gt;

&lt;p&gt;And then the one I would put on a wall: the designation points to a user record in a specific environment, so &lt;b&gt;it is not carried in a solution&lt;/b&gt;. Deploying through a managed solution or a pipeline does not reapply it in the target. Think about what that means. You promote the flow to production, everything imports green, the flow turns on, and it is unlicensed there because the designation never travelled. Nothing failed. Nothing warned you. You find out when it gets suspended.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo concept&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;A post-deployment licensing step: one script, run after solution import, that resolves the identifiers, sets the designation, and reads it back to prove it landed. Here is the request that does the work, straight from the documentation:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;PATCH [Organization URI]/api/data/v9.2/workflows(&amp;lt;workflowId&amp;gt;) HTTP/1.1
Content-Type: application/json

{
    &quot;licensee_systemuserid@odata.bind&quot;: &quot;/systemusers(&amp;lt;systemUserId&amp;gt;)&quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;@odata.bind&lt;/code&gt; annotation is how the Web API sets a lookup: you are binding a reference to a row in another table, which is why the value is a path and not a bare GUID. This is a PATCH against the &lt;code&gt;workflows&lt;/code&gt; entity set, which is the Process table, because cloud flows are rows there where Category is &lt;b&gt;Modern Flow&lt;/b&gt;. You need two identifiers: the flow&#39;s &lt;code&gt;workflowid&lt;/code&gt;, which also appears in its URL in the portal, and the user&#39;s &lt;code&gt;systemuserid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wired together, and trimmed to the essentials:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;$api = &quot;$OrgUrl/api/data/v9.2&quot;
$headers = @{ Authorization = &quot;Bearer $token&quot;; &#39;Content-Type&#39; = &#39;application/json&#39; }

# Category 5 is Modern Flow. Refuse to act unless exactly one row matches.
$flows = (Invoke-RestMethod -Headers $headers -Uri
    &quot;$api/workflows?`$filter=category eq 5 and name eq &#39;$FlowName&#39;&quot;).value
if ($flows.Count -ne 1) { throw &quot;Expected 1 flow, found $($flows.Count). Refusing to guess.&quot; }

$users = (Invoke-RestMethod -Headers $headers -Uri
    &quot;$api/systemusers?`$filter=domainname eq &#39;$UserUpn&#39;&quot;).value
if ($users.Count -ne 1) { throw &quot;Expected 1 user, found $($users.Count).&quot; }

$body = @{ &#39;licensee_systemuserid@odata.bind&#39; = &quot;/systemusers($($users[0].systemuserid))&quot; } |
    ConvertTo-Json
Invoke-RestMethod -Method Patch -Headers $headers -Body $body `
    -Uri &quot;$api/workflows($($flows[0].workflowid))&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note, because there is more opinion here than code. Both lookups refuse to proceed unless they match exactly one row: a script that silently picks the first of three similarly named flows will license the wrong one and you will not find out for a month. The backticks before &lt;code&gt;$filter&lt;/code&gt; are PowerShell escapes, because &lt;code&gt;$&lt;/code&gt; starts a variable in PowerShell and OData query options genuinely begin with a dollar sign; leave them out and you send a URL with the query options silently deleted. And &lt;code&gt;$token&lt;/code&gt; is whatever your pipeline already uses to authenticate against the environment.&lt;/p&gt;

&lt;p&gt;Then verify, because a PATCH returning 204 tells you the request was accepted, not that the designation is what you wanted:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET [Organization URI]/api/data/v9.2/workflows(&amp;lt;workflowId&amp;gt;)?$select=name,_licensee_value HTTP/1.1
Prefer: odata.include-annotations=&quot;OData.Community.Display.V1.FormattedValue&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That &lt;code&gt;Prefer&lt;/code&gt; header is the point. Without it you get a raw GUID in &lt;code&gt;_licensee_value&lt;/code&gt; and you will spend ten minutes looking it up by hand. With it, Dataverse also returns the user&#39;s display name, and your pipeline log becomes evidence rather than a promise. The underscore-prefixed column name is the standard convention for reading a lookup&#39;s value, and it trips up everyone exactly once.&lt;/p&gt;

&lt;p&gt;To remove a designation, &lt;code&gt;DELETE&lt;/code&gt; the same path with &lt;code&gt;/licensee_systemuserid/$ref&lt;/code&gt; appended. The article notes it succeeds even when no user is designated, which makes it safe in a teardown script.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Doing it from a flow instead&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;If your deployment process is itself a cloud flow, the documentation gives this path:&lt;/p&gt;

&lt;p&gt;1) Add the &lt;b&gt;Update a row&lt;/b&gt; action from the &lt;b&gt;Microsoft Dataverse&lt;/b&gt; connector.&lt;/p&gt;

&lt;p&gt;2) In &lt;b&gt;Table name&lt;/b&gt;, select &lt;b&gt;Processes&lt;/b&gt;.&lt;/p&gt;

&lt;p&gt;3) In &lt;b&gt;Row ID&lt;/b&gt;, enter the &lt;code&gt;workflowid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;4) In &lt;b&gt;Licensee (Users)&lt;/b&gt;, enter &lt;code&gt;/systemusers(&amp;lt;systemUserId&amp;gt;)&lt;/code&gt;. If the field is not shown, select &lt;b&gt;Show advanced options&lt;/b&gt;. Leave it empty to remove the designation.&lt;/p&gt;

&lt;p&gt;NOTE: the documentation does not spell out the navigation to the flow&#39;s &lt;b&gt;Details&lt;/b&gt; page for the manual route, it simply says to open it, so I am not going to invent a click path. It is the page you land on when you select the flow by name from &lt;b&gt;My flows&lt;/b&gt; or from the solution.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo sketch&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Three scripts and a pipeline step: &lt;code&gt;Set-FlowLicensee.ps1&lt;/code&gt; as above, &lt;code&gt;Remove-FlowLicensee.ps1&lt;/code&gt; for the &lt;code&gt;$ref&lt;/code&gt; teardown, and the one that earns its keep long term, &lt;code&gt;Get-FlowLicensee.ps1&lt;/code&gt;, which lists every Modern Flow owned by an application user and reports which have a designation and which do not. That is your compliance drift report in about thirty lines. The pipeline step exists purely because the designation does not travel in a solution, so it must be reapplied per environment by something that cannot forget.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;I like this change more than its size suggests. It removes a real disincentive to doing ALM properly, and the disincentive was purely financial rather than technical, which is the most annoying kind.&lt;/p&gt;

&lt;p&gt;If you carry away one thing: the designation is per environment and solutions do not move it. Everything else here is a five minute configuration change. That one is an architectural fact about your deployment pipeline.&lt;/p&gt;

&lt;p&gt;What I learned from this exercise: &quot;it deployed successfully&quot; and &quot;it is licensed&quot; are two different claims, and only one of them shows up green in your pipeline.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/4792628291846088702/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/4792628291846088702' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/4792628291846088702'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/4792628291846088702'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/09/power-automate-licensing-flow-nobody.html' title='Power Automate | Licensing the Flow Nobody Owns'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-113240145197573997</id><published>2026-09-10T07:00:00.000-04:00</published><updated>2026-09-10T11:50:14.581-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><title type='text'>Copilot Studio | Grounding a Copilot Studio Agent on Azure SQL</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;Grounding on Azure SQL: your schema is now the prompt&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-11-azuresql-knowledge.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Every Monday I read what moved in the Microsoft Learn documentation over the previous week, mostly because I got tired of finding out about a capability three months late, when a customer asks why we are not already using it. Most weeks the honest answer is &quot;nothing worth your time&quot;. The week of August 13th was not one of those weeks.&lt;/p&gt;

&lt;p&gt;A new article appeared in the Copilot Studio documentation, and it changes the answer to a question I have been asked more times than I can count: &lt;i&gt;can my agent answer questions straight out of my database?&lt;/i&gt;&lt;/p&gt;

&lt;p&gt;Now it can.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Until now, grounding a Copilot Studio agent meant pointing it at content that was, broadly speaking, prose: SharePoint documents, websites, uploaded files, &lt;a href=&quot;https://www.theworkbench.blog/2026/08/copilot-studio-adding-dataverse.html&quot;&gt;Dataverse tables&lt;/a&gt;. If your answer lived in a relational database in Azure, you either built a tool that queried it and shaped the result yourself, or you exported the data somewhere the agent could already read. Both work. Neither is what you would call delightful.&lt;/p&gt;

&lt;p&gt;Dataverse is the one I have spent real time on, in &lt;a href=&quot;https://www.theworkbench.blog/2026/08/copilot-studio-adding-dataverse.html&quot;&gt;an earlier article on adding those knowledge sources with the Web API&lt;/a&gt; instead of clicking them together in the maker portal. Worth keeping in mind as you read on, because Azure SQL is a different animal in one important respect: that Dataverse recipe works precisely because a Dataverse knowledge source is itself Dataverse rows, and here there is no intermediate copy of anything. Your data stays in your database.&lt;/p&gt;

&lt;p&gt;The new article is &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/knowledge-add-azure-sql-tables&quot;&gt;Add Azure SQL tables as a knowledge source&lt;/a&gt;. In its own words, you ground your agent in the structured data stored in your Azure SQL database, and the agent answers by reading from the tables you select.&lt;/p&gt;

&lt;p&gt;Select the tables. That is the whole configuration story. No tool authoring, no schema mapping, no intermediate copy of the data.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The part everybody will skip, and shouldn&#39;t&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;There is a security model here, and it is not the one most people will assume. Azure SQL knowledge uses the Power Platform SQL connection you create during setup, and then:&lt;/p&gt;

&lt;blockquote&gt;Users only receive answers based on data the maker can access through the maker&#39;s configured connection and database permissions.&lt;/blockquote&gt;

&lt;p&gt;Read that again. The ceiling is &lt;b&gt;the maker&#39;s&lt;/b&gt; connection, not the end user&#39;s own database rights. If you wire up a connection that can read the whole Sales schema, then every user who can talk to that agent can get answers derived from the whole Sales schema. The agent is not brokering each user&#39;s individual SQL permissions row by row.&lt;/p&gt;

&lt;p&gt;That is not a defect, it is how connection-based grounding works everywhere in Power Platform. But it does mean table selection is a &lt;i&gt;security&lt;/i&gt; decision, not just a relevance one, and I would even venture to say it is the sentence in that article most likely to be skimmed past and later regretted. Select the tables the agent needs. Not the tables you happen to have.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;What it means for you&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;If you are a maker, a whole category of question moves from &quot;that needs a developer&quot; to &quot;that needs a good description&quot;. Order status, purchase history, revenue by customer or date: ground an agent on it in an afternoon. The catch is that quality now depends on your schema. The documentation asks for clear column names over cryptic abbreviations (&lt;code&gt;CustomerName&lt;/code&gt;, not &lt;code&gt;CustNm&lt;/code&gt;), primary keys on every table, and descriptive names such as &lt;code&gt;OrderDate&lt;/code&gt; rather than &lt;code&gt;Date1&lt;/code&gt;. It also warns that extra tables dilute grounding. So, your schema quality is now prompt quality, and every badly named column you inherited is now, quite literally, a worse answer.&lt;/p&gt;

&lt;p&gt;If you are a professional developer, three things matter. The primary key requirement is not decorative: the agent uses primary keys to identify rows and return results, so heap tables will behave poorly. Network reachability is on you, and the prerequisites offer firewall rules allowing Azure services or private connectivity, the first of which will not survive most security reviews, so budget for that conversation early. And best of all, the documentation hands you a testability seam: create a diagnostic view returning a small, known dataset and confirm the connection before you point at production. That turns &quot;the agent gave a weird answer&quot; into something you can bisect in ten seconds.&lt;/p&gt;

&lt;p&gt;NOTE: the documentation hedges its own UI labels, saying you pick &lt;b&gt;Azure SQL&lt;/b&gt; &lt;i&gt;or&lt;/i&gt; &lt;b&gt;SQL Server&lt;/b&gt; &quot;depending on the label shown in your environment&quot;. I am reproducing that hedge rather than guessing.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;A demo worth building&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Something small and controlled: a three-table order schema, a diagnostic view with a known answer, and an agent grounded on exactly those tables, so you can change one thing at a time and watch what it does to answer quality. Here is the schema, written the way the documentation asks:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE dbo.Customers (
    CustomerId   INT           NOT NULL IDENTITY(1,1),
    CustomerName NVARCHAR(200) NOT NULL,
    CustomerCity NVARCHAR(100) NULL,
    CONSTRAINT PK_Customers PRIMARY KEY CLUSTERED (CustomerId)
);

CREATE TABLE dbo.Orders (
    OrderId     INT           NOT NULL IDENTITY(1,1),
    CustomerId  INT           NOT NULL,
    OrderDate   DATE          NOT NULL,
    OrderStatus NVARCHAR(40)  NOT NULL,
    OrderTotal  DECIMAL(18,2) NOT NULL,
    CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderId),
    CONSTRAINT FK_Orders_Customers FOREIGN KEY (CustomerId)
        REFERENCES dbo.Customers (CustomerId)
);

CREATE TABLE dbo.OrderLines (
    OrderLineId INT           NOT NULL IDENTITY(1,1),
    OrderId     INT           NOT NULL,
    ProductName NVARCHAR(200) NOT NULL,
    Quantity    INT           NOT NULL,
    UnitPrice   DECIMAL(18,2) NOT NULL,
    LineTotal   DECIMAL(18,2) NOT NULL,
    CONSTRAINT PK_OrderLines PRIMARY KEY CLUSTERED (OrderLineId),
    CONSTRAINT FK_OrderLines_Orders FOREIGN KEY (OrderId)
        REFERENCES dbo.Orders (OrderId)
);

CREATE VIEW dbo.AgentGroundingCheck
AS
SELECT &#39;GROUNDING-OK&#39; AS DiagnosticMarker, 42 AS DiagnosticNumber;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. Every table carries an explicit primary key, because that is what the agent uses to identify rows. Every column name is a phrase a human would say out loud, and you should resist the urge to shorten them. The view at the bottom is the diagnostic seam: add it to the knowledge source, ask &quot;what is the diagnostic marker?&quot;, and you learn whether the connection works at all, independent of whether your business data is right. Pick your own marker value, and drop the view from the knowledge source before real users arrive, because it is scaffolding, not a feature.&lt;/p&gt;

&lt;p&gt;Now the description, which is not code but is configuration, and which the documentation treats as the highest-leverage text you will write:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Azure SQL knowledge for Contoso order management. Includes Customers,
Orders, and OrderLines tables. Use this source for questions about order
status, order history, customer purchases, and revenue by customer or
date. Don&#39;t use for inventory on-hand or shipping carrier tracking.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is lifted from the article&#39;s own example, and the shape is what matters: domain, tables, what to use it for, and then what &lt;b&gt;not&lt;/b&gt; to use it for. That last sentence does real work, because generative orchestration is choosing between your knowledge sources, and an exclusion is often a stronger signal than an inclusion.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The steps&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The full click path is in &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/knowledge-add-azure-sql-tables&quot;&gt;the source&lt;/a&gt;, and given the label hedge above I would rather you read it there than trust my transcription. These are the steps that carry a decision:&lt;/p&gt;

&lt;p&gt;1) &lt;b&gt;Prepare the database.&lt;/b&gt; Primary keys defined, readable column names, and your Azure SQL server reachable from Power Platform.&lt;/p&gt;

&lt;p&gt;2) &lt;b&gt;Select the tables deliberately.&lt;/b&gt; This is the security decision from earlier, not a relevance one.&lt;/p&gt;

&lt;p&gt;3) &lt;b&gt;Write the description properly&lt;/b&gt;, in the shape above.&lt;/p&gt;

&lt;p&gt;4) &lt;b&gt;Publish the agent.&lt;/b&gt; The knowledge source is not available in the published experience until you do, and this is exactly the step that gets skipped and then generates a bug report.&lt;/p&gt;

&lt;p&gt;5) &lt;b&gt;Test with something that must come from those tables.&lt;/b&gt; The article&#39;s own example: &quot;Show me the top five customers by total sales last month.&quot;&lt;/p&gt;

&lt;p&gt;That is still a walk through a dialog, which raises the obvious question if you read the Dataverse piece: can this one be created from code instead? I have not checked, and I am not going to guess. The connection is a Power Platform connection and the knowledge source is a row somewhere, so there is a fair chance &lt;a href=&quot;https://www.theworkbench.blog/2026/08/copilot-studio-adding-dataverse.html&quot;&gt;the same approach&lt;/a&gt; applies. But that is a hypothesis, and it belongs in a follow-up where I can show it working rather than in a paragraph where I speculate about it.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo sketch&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Four folders is plenty: &lt;code&gt;sql/&lt;/code&gt; with schema, seed, diagnostic view and teardown; &lt;code&gt;agent/&lt;/code&gt; holding the knowledge description as its own version-controlled file, because it is configuration that changes behavior and deserves a diff history; a &lt;code&gt;test-questions.md&lt;/code&gt; with three columns, the question, the expected answer, and the table the answer must come from; and &lt;code&gt;docs/&lt;/code&gt; for the network and security notes. Use fixed dates in the seed data, never dates relative to today, or &quot;last month&quot; will mean something different in March than in September and your tests will rot. That third column in the test file is the piece people leave out, and without it you cannot tell a grounding failure from a data failure.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;This lands exactly where agents have been weakest: answering from the boring, structured, authoritative data that businesses actually run on. Slick.&lt;/p&gt;

&lt;p&gt;One caution, the same one: the maker&#39;s connection defines what every user of that agent can reach. And since the article is four days old as I write this, labels may still be moving, so trust your screen over my transcription and go read &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/knowledge-add-azure-sql-tables&quot;&gt;the source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What I learned from this exercise: the schema you were going to clean up &quot;someday&quot; is now on the critical path to answer quality, and that&#39;s always a good thing, because it finally gives you a business reason to do it.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/113240145197573997/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/113240145197573997' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/113240145197573997'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/113240145197573997'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/09/copilot-studio-grounding-copilot-studio.html' title='Copilot Studio | Grounding a Copilot Studio Agent on Azure SQL'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-1666200584224273562</id><published>2026-09-07T07:00:00.000-04:00</published><updated>2026-09-07T09:55:23.658-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><title type='text'>Copilot Studio | Migrate the Agent, Keep the Client ID</title><content type='html'>&lt;!--Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish.--&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;img alt=&quot;Migrate the agent, keep the client ID: Copilot Studio agents move to Microsoft Entra Agent ID&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-16-entra-agent-id.png&quot; style=&quot;max-width: 100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;There is a question I have been asked in one form or another by every administrator who has ever inherited a tenant full of Copilot Studio agents, and it always arrives with the same slightly worried tone: &quot;what are all these app registrations in Entra, and which agent does each one belong to?&quot; It is a fair question. For years the answer was that Copilot Studio quietly provisioned an Azure app registration for every agent you created, those registrations piled up in Microsoft Entra ID looking exactly like every other application in the tenant, and telling them apart was an exercise in cross-referencing GUIDs. Not exactly a governance story.&lt;/p&gt;

&lt;p&gt;Well, in late August three articles landed in the Copilot Studio documentation that finally close that loop, and one of them is the migration runbook administrators have been waiting for.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Two brand new pages and one substantially rewritten page, all last updated on August 27, 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/govern-agents-identities-overview&quot;&gt;Agent identities and authentication for Copilot Studio&lt;/a&gt;, new, a plain-language explainer written as a set of questions.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/govern-migrate-api-entra-agent-identity&quot;&gt;Migrate Copilot Studio agents to Microsoft Entra Agent ID&lt;/a&gt;, new, and the reason for this post.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-copilot-studio/admin-use-entra-agent-identities&quot;&gt;Microsoft Entra Agent IDs for Copilot Studio agents&lt;/a&gt;, rewritten with the migration guidance and the transition-period rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, the model itself, because the terminology is new and worth pinning down before anything else. A &lt;b&gt;Microsoft Entra Agent ID&lt;/b&gt; is a Microsoft Entra service principal with an &quot;Agent&quot; subtype. That is the whole definition. It is not a new protocol and not a new token type. The documentation is careful about this: the underlying OAuth-based authentication flow remains the same. What changes is that the agent is now a first-class object in Entra that an administrator can see, govern, and write policy against, rather than an anonymous app registration sitting in a list of four hundred others.&lt;/p&gt;

&lt;p&gt;Copilot Studio has been creating these automatically for every new agent since May 2026. Agents created before that date still carry the legacy app registration, and here is the sentence that turns this from trivia into a project: existing agents will be migrated by Microsoft automatically in a future update. So the migration is happening either way. The only decision in front of you is whether it happens on your schedule or on Microsoft&#39;s.&lt;/p&gt;

&lt;p&gt;NOTE: one other thing changed in the same rewrite and it is easy to miss. Previously you could opt out of Entra Agent ID at the environment level. That option is gone. All new agents get Microsoft Entra Agent IDs and you can no longer opt out of automatic agent identity creation. There is also now a tenant-level object called the &lt;b&gt;Microsoft Copilot Studio agent identity blueprint&lt;/b&gt;, added the first time an agent is created after the rollout, with a corresponding blueprint principal.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The Solution: an in-place conversion with a rollback&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Here is the design decision worth slowing down for, and it is the one that makes the whole thing usable in a real tenant.&lt;/p&gt;

&lt;p&gt;Migration converts an agent&#39;s existing app-registration identity &lt;i&gt;in place&lt;/i&gt;. The agent keeps its application (client) ID. Everything downstream that resolves against that identifier, channel registrations and connectors among them, continues to resolve to the same value. The agent additionally gains an Entra Agent ID that administrators can manage.&lt;/p&gt;

&lt;p&gt;You may be asking why that detail deserves its own paragraph. Frankly, the answer is simple: the alternative would have been catastrophic. An identity migration that issued a new client ID would break every channel registration, every connector configuration, and every downstream system that had that GUID written into a config file somewhere, all at once, across an entire estate. Keeping the client ID is what turns &quot;a migration nobody will ever run&quot; into &quot;a migration you can pilot on Tuesday.&quot;&lt;/p&gt;

&lt;p&gt;And it is reversible. There is a documented rollback that reverts an agent to its legacy app-registration identity, which means you can validate an agent after migrating it and put it back if it does not pass. A migration path with a documented way back is a rarer thing than it should be, and whoever fought for it deserves the credit.&lt;/p&gt;

&lt;p&gt;What you get in exchange for the trip: a first-class agent identity that administrators can view and govern in Microsoft Entra, Conditional Access and other access policies designed for agentic workloads and &lt;i&gt;scoped to agents&lt;/i&gt; rather than inherited from app registrations, and a consistent identity model across the services that work with your agents. The Conditional Access point is the substantive one. Writing a policy that targets agents specifically, rather than catching them incidentally because they happen to be applications, is a capability that did not exist before.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;makers&lt;/b&gt;, the honest answer is that this is not your job, but it will land on your calendar anyway. The documented process explicitly requires the administrator to coordinate a validation window with the makers who own the agents being migrated, and to wait for those makers to test before the next batch goes. If you own an agent, expect to be asked to exercise it across every channel it publishes to, run its actions and connectors, and confirm its authentication still behaves. Say yes and block the time. The alternative is finding out during the automatic migration, with nobody watching.&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;professional developers and administrators&lt;/b&gt;, there are two pieces of work here. The near one is the pilot: identify eligible agents, migrate a representative handful, validate, expand. The further one is Conditional Access, and it is the more interesting of the two. Once agents are first-class identities in Entra, &quot;which policies apply to our agents&quot; becomes a question with an actual answer, and the documentation tells you to review Microsoft Entra sign-in logs including Conditional Access results before you widen a batch. That is a sentence with real work behind it, and it is the part I would start reading up on now rather than during the migration.&lt;/p&gt;

&lt;p&gt;Two cautions, both from the documentation rather than invented by me. The manual migration process is currently a &lt;b&gt;preview&lt;/b&gt; feature. And the documentation states plainly that migrating agent identities affects live agents and can disrupt authentication, connectors, and integrations if you do not plan the migration carefully. This is not a maintenance-window-free operation dressed up as one.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo concept&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Not &quot;watch an agent get migrated,&quot; because the interesting part of a successful migration is that nothing visible happens. The demonstration worth building is the &lt;b&gt;evidence trail&lt;/b&gt;, and it has three parts.&lt;/p&gt;

&lt;p&gt;Take one noncritical agent. Before you touch it, capture its application (client) ID and its Entra sign-in log entries. Migrate it through the API so you get the response object rather than a screen full of green checkmarks. Then capture the same two things again and put them side by side: the client ID identical, the agent now present in Entra as a service principal with the Agent subtype, and a sign-in log you can point a Conditional Access policy at. That before-and-after pair is the artifact that gets a change advisory board to approve the rest of the estate, and it is far more persuasive than any amount of documentation quoting.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;First, the prerequisites, because two of them will stop you cold. You must be a &lt;b&gt;Power Platform Administrator&lt;/b&gt;, &lt;b&gt;Dynamics 365 Administrator&lt;/b&gt;, or &lt;b&gt;Global Administrator&lt;/b&gt;. Power Platform inventory must be enabled for your tenant, because that is how Advisor identifies eligible agents. And you must coordinate a validation window with the makers who own the agents. That third one is not a technical prerequisite, but the documentation lists it as one, and I would treat it exactly that way.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The administrator path, through the Power Platform admin center&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;1) Sign in to the Power Platform admin center.&lt;br /&gt;
2) In the left navigation pane, select &lt;b&gt;Actions&lt;/b&gt;.&lt;br /&gt;
3) Under &lt;b&gt;Actions&lt;/b&gt;, select &lt;b&gt;Recommendations&lt;/b&gt;.&lt;br /&gt;
4) On the &lt;b&gt;Recommendations&lt;/b&gt; tab, select &lt;b&gt;Active&lt;/b&gt;.&lt;br /&gt;
5) Search for and select &lt;b&gt;Migrate Copilot Studio agents to Microsoft Entra Agent ID for enhanced agent governance&lt;/b&gt;.&lt;br /&gt;
6) In the recommendation pane, expand &lt;b&gt;Why is this important?&lt;/b&gt; and read the migration guidance.&lt;br /&gt;
7) Review the eligible agents. Use the &lt;b&gt;Suggested migration order&lt;/b&gt; and &lt;b&gt;Migration notes&lt;/b&gt; columns to pick your pilot, and note that the table also gives you environment, environment type, owner, recent activity, and authentication method.&lt;br /&gt;
8) Select the checkbox next to each agent you want to migrate. The &lt;b&gt;Migrate&lt;/b&gt; button becomes available and the action bar shows how many agents you have selected.&lt;br /&gt;
9) Select &lt;b&gt;Migrate&lt;/b&gt;, review the confirmation, and confirm.&lt;br /&gt;
10) Review the &lt;b&gt;Action&lt;/b&gt;, &lt;b&gt;Action state&lt;/b&gt;, and &lt;b&gt;Action date&lt;/b&gt; columns for each agent. The &lt;b&gt;Action history&lt;/b&gt; tab shows actions across all recommendations.&lt;/p&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEio6Hvw2FB0EwA2jYruOhlNK9QuFHi4gQ0lE9s0oYbX6zlKDgRCdni8179FjeyKlaH_3W6L8aUGgC0ef1DoDUGr4tGp4Gp4kKWYjuD-UDbTi0oOvrOJ6FtuM3MgKNPHmHdUZSpZhQtKq8tuxKU0oOSs_N_uVSYiCQ9yITgtOmn7IzQHYzBgwLrddkzd9jXa/s1908/Screenshot%202026-09-07%20094808.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1065&quot; data-original-width=&quot;1908&quot; height=&quot;365&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEio6Hvw2FB0EwA2jYruOhlNK9QuFHi4gQ0lE9s0oYbX6zlKDgRCdni8179FjeyKlaH_3W6L8aUGgC0ef1DoDUGr4tGp4Gp4kKWYjuD-UDbTi0oOvrOJ6FtuM3MgKNPHmHdUZSpZhQtKq8tuxKU0oOSs_N_uVSYiCQ9yITgtOmn7IzQHYzBgwLrddkzd9jXa/w653-h365/Screenshot%202026-09-07%20094808.png&quot; width=&quot;653&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Power Platform Admin Center&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;p&gt;Then stop. Do not queue the next batch. Validate first, and the documentation is specific about what validating means: confirm the agent responds correctly in every channel where it is published, that its actions, connectors, flows and integrations run, that it authenticates as expected including any custom authentication, and that it behaves correctly with applicable agent access policies and Conditional Access policies. Then go read the sign-in logs for those agents in the Microsoft Entra admin center. If an agent fails validation, stop the rollout and revert that agent before continuing.&lt;/p&gt;

&lt;p&gt;Keep in mind that Advisor recommendations can remain visible for up to a week after you act on them while the recommendation data refreshes. Seeing the recommendation still sitting there does not mean your migration did not take.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The API path, for when you want your own automation&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The admin center is the recommended manual method, but there are Power Platform API endpoints if you would rather script it, and the documentation supplies the samples. Everything needs an OAuth2 bearer token for the Power Platform API:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;$token = (Get-AzAccessToken -ResourceUrl &quot;https://api.powerplatform.com&quot;).Token&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. This uses the Az PowerShell module, and the token has to be associated with an account holding one of the three admin roles listed earlier. There is no service-principal shortcut documented here, so if multifactor authentication or Conditional Access requires interactive sign-in, expect a browser window and complete the prompts. This is also the single most common failure in the troubleshooting table, listed as expired credentials or a token error.&lt;/p&gt;

&lt;p&gt;To migrate one agent:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;$token = (Get-AzAccessToken -ResourceUrl &quot;https://api.powerplatform.com&quot;).Token
$environmentId = &quot;&amp;lt;EnvironmentId&amp;gt;&quot;
$botId = &quot;&amp;lt;BotId&amp;gt;&quot;
$uri = &quot;https://api.powerplatform.com/copilotstudio/environments/$environmentId/bots/$botId/api/agentidentitymigration/migrate?api-version=2024-10-01&quot;
Invoke-RestMethod `
  -Method Post `
  -Uri $uri `
  -Headers @{
    Authorization = &quot;Bearer $token&quot;
  }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. The request takes &lt;b&gt;no body&lt;/b&gt;, which surprised me the first time I read it: the agent is fully identified by the two IDs in the path. You get those two values from the agent inventory in the Power Platform admin center under &lt;b&gt;Manage&lt;/b&gt; &amp;gt; &lt;b&gt;Copilot Studio&lt;/b&gt;. The API version is pinned at &lt;code&gt;2024-10-01&lt;/code&gt;, so keep it, and note that this endpoint lives on &lt;code&gt;api.powerplatform.com&lt;/code&gt; rather than any Copilot Studio host. The response is an &lt;code&gt;AgentIdentityMigrationResult&lt;/code&gt; with a status of either &lt;code&gt;Migrated&lt;/code&gt; or &lt;code&gt;AlreadyMigrated&lt;/code&gt;, which means the call is safely idempotent. Running it twice does not do the migration twice, and that matters a great deal if you are looping over a list of agents and something times out halfway through.&lt;/p&gt;

&lt;p&gt;A successful response looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;status&quot;: &quot;Migrated&quot;,
  &quot;cdsBotId&quot;: &quot;&amp;lt;bot-id&amp;gt;&quot;,
  &quot;environmentId&quot;: &quot;&amp;lt;environment-id&amp;gt;&quot;,
  &quot;tenantId&quot;: &quot;&amp;lt;tenant-id&amp;gt;&quot;,
  &quot;agentIdentityId&quot;: &quot;&amp;lt;agent-identity-id&amp;gt;&quot;,
  &quot;applicationId&quot;: &quot;&amp;lt;application-client-id&amp;gt;&quot;,
  &quot;servicePrincipalObjectId&quot;: &quot;&amp;lt;service-principal-object-id&amp;gt;&quot;,
  &quot;managedIdentityId&quot;: &quot;&amp;lt;managed-identity-id&amp;gt;&quot;,
  &quot;completedAtUtc&quot;: &quot;2026-08-21T12:00:00Z&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note, and this payload is the reason I would use the API even if I ran the migration from the admin center. Look at what comes back: &lt;code&gt;applicationId&lt;/code&gt;, the client ID you can compare against what you captured before, and &lt;code&gt;servicePrincipalObjectId&lt;/code&gt; and &lt;code&gt;agentIdentityId&lt;/code&gt;, the handles you need to find this agent in Entra afterward. Log the whole object. It is your evidence trail, produced for free, and reconstructing it later from the portal is tedious work you would rather not do.&lt;/p&gt;

&lt;p&gt;And to put one back:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;$uri = &quot;https://api.powerplatform.com/copilotstudio/environments/$environmentId/bots/$botId/api/agentidentitymigration/rollback?api-version=2024-10-01&quot;
Invoke-RestMethod `
  -Method Post `
  -Uri $uri `
  -Headers @{
    Authorization = &quot;Bearer $token&quot;
  }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. Same shape, same authentication, same absence of a body, and the same idempotent design: the response status is &lt;code&gt;RolledBack&lt;/code&gt; or &lt;code&gt;NotMigrated&lt;/code&gt;. Write this script at the same time you write the migrate script, not after something has gone wrong. A rollback you have already tested on a throwaway agent is a rollback you can actually reach for at four in the afternoon with a maker on the phone.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo sketch&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;A small runbook repository, honest about being a runbook. A &lt;code&gt;scripts/&lt;/code&gt; folder with three files and no more: &lt;code&gt;Get-AgentInventory.ps1&lt;/code&gt; to pull the agents and their environment and bot IDs, &lt;code&gt;Invoke-AgentMigration.ps1&lt;/code&gt; that takes a CSV of the batch and writes every response object to disk, and &lt;code&gt;Invoke-AgentRollback.ps1&lt;/code&gt; written first and tested first. A &lt;code&gt;batches/&lt;/code&gt; folder with the CSV per batch, so the pilot, the second wave and the long tail are separate committed files rather than a spreadsheet somebody is editing in place. An &lt;code&gt;evidence/&lt;/code&gt; folder holding the before-and-after client IDs and the raw JSON responses, because that is the artifact that survives the project. And a &lt;code&gt;VALIDATION.md&lt;/code&gt; with the checklist from the documentation turned into tick boxes per agent: channels, actions, connectors, flows, integrations, custom authentication, access policies, Conditional Access.&lt;/p&gt;

&lt;p&gt;Point the README at a nonproduction tenant for the first run and say so in bold. And put the troubleshooting table in there too, because the four failure modes Microsoft documents are the four you will hit: inventory returning no agents when Power Platform inventory is not enabled or your role is wrong, token and reauthentication prompts, agents skipped because they are already migrated or you left out an ID, and single-agent failures from eligibility, access, or throttling, where the answer is to wait and rerun.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;What I keep coming back to is not the identity model, welcome as it is. It is that Microsoft shipped the migration runbook &lt;i&gt;and&lt;/i&gt; the rollback &lt;i&gt;and&lt;/i&gt; a troubleshooting table with four named failure modes, for a migration it is eventually going to run for you anyway. It did not have to do any of that. It could have flipped the switch on a Tuesday and let us all find out.&lt;/p&gt;

&lt;p&gt;So take the invitation. Enable Power Platform inventory, open the Advisor recommendation, pick three agents nobody will miss, and run the pilot while the stakes are zero and the makers are available. The version of this project where you go first is a much better project than the version where you go last.&lt;/p&gt;

&lt;p&gt;What I learned from this exercise: the most valuable sentence in a migration document is rarely the one describing the migration. It is the one telling you how to undo it.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;
Mariano Gomez Bent&lt;br /&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/1666200584224273562/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/1666200584224273562' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1666200584224273562'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1666200584224273562'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/09/copilot-studio-migrate-agent-keep.html' title='Copilot Studio | Migrate the Agent, Keep the Client ID'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEio6Hvw2FB0EwA2jYruOhlNK9QuFHi4gQ0lE9s0oYbX6zlKDgRCdni8179FjeyKlaH_3W6L8aUGgC0ef1DoDUGr4tGp4Gp4kKWYjuD-UDbTi0oOvrOJ6FtuM3MgKNPHmHdUZSpZhQtKq8tuxKU0oOSs_N_uVSYiCQ9yITgtOmn7IzQHYzBgwLrddkzd9jXa/s72-w653-h365-c/Screenshot%202026-09-07%20094808.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-8604335111466068845</id><published>2026-09-04T07:00:00.000-04:00</published><updated>2026-09-04T07:00:00.113-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DocWatch"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><title type='text'>Power Apps | Describe the App, Approve the Plan</title><content type='html'>&lt;!-- Cover image is hosted in the public theworkbench-assets repo. If you prefer Google-hosted
     images, re-upload the PNG through the Blogger editor before you publish. --&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;img alt=&quot;Describe the app, approve the plan: the model app builder skill for Power Apps&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-15-model-app-builder.png&quot; style=&quot;max-width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Every one of us who has stood up a model-driven app from nothing has lived the same first afternoon. You know the business process cold, you could describe it to a colleague in ninety seconds, and yet the next four hours go to clicking: a table, then its columns, then the relationships, then a main form, then two views, then a chart nobody asked for, then the sitemap, then the security roles you will get wrong the first time and fix on Thursday. None of that is thinking. It is transcription, and it is the reason so many good internal apps never get built at all.&lt;/p&gt;

&lt;p&gt;Well, in late August a new article appeared in the Power Apps documentation that goes after that first afternoon directly, and it did not arrive alone.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Background&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;A brand new page landed in the &lt;code&gt;MicrosoftDocs/powerapps-docs&lt;/code&gt; repository, dated 08/27/2026, and it is live on Learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/model-driven-app-external-tools&quot;&gt;Build and edit model-driven apps with AI code generation tools (preview)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What tells you this is a real shipped surface rather than a stray draft is the company it keeps. In the same set of changes, &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/app-building-steps&quot;&gt;Steps to building a model-driven app&lt;/a&gt; and the &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/app-designer-overview&quot;&gt;Overview of the model-driven app designer&lt;/a&gt; both gained a TIP block pointing at the new article, the existing &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/generative-page-external-tools&quot;&gt;generative pages&lt;/a&gt; article gained a NOTE telling you which of the two skills to reach for, and the maker landing page and table of contents were amended to carry it. Microsoft does not wire four existing articles into a page it is not serious about.&lt;/p&gt;

&lt;p&gt;Now, the thing itself. It is called the &lt;b&gt;model app builder skill&lt;/b&gt;, and it runs inside an AI code generation tool such as GitHub Copilot CLI or Claude Code. A &lt;b&gt;skill&lt;/b&gt;, in this context, is a packaged set of instructions and commands that teaches one of those tools how to perform a specific job. It is not a connector, and it is not a service you call. It is guidance the tool loads, plus the command-line plumbing to act on it. The distinction matters, because it means everything the skill does, it does as &lt;i&gt;you&lt;/i&gt;, through tools you already have authenticated on your own machine.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;What the skill actually produces&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;This is the part that surprised me, so let me quote the capability list rather than paraphrase it into something vaguer. The documentation says the skill can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a model-driven app from a description of a business scenario.&lt;/li&gt;
&lt;li&gt;Plan the app around user personas and the jobs they need to accomplish.&lt;/li&gt;
&lt;li&gt;Create supporting tables, columns, relationships, forms, views, charts, and sample data.&lt;/li&gt;
&lt;li&gt;Create generative pages for experiences that go beyond standard forms and views.&lt;/li&gt;
&lt;li&gt;Create a sitemap with custom icons for each table.&lt;/li&gt;
&lt;li&gt;Add JavaScript validation rules to forms.&lt;/li&gt;
&lt;li&gt;Add security roles based on the planned personas and data access needs.&lt;/li&gt;
&lt;li&gt;Edit an existing model-driven app by describing the changes you want.&lt;/li&gt;
&lt;li&gt;Verify the deployed app against the approved specification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that list twice. Security roles derived from personas. Sitemap icons and form validation created as &lt;b&gt;web resources&lt;/b&gt; and wired to the right components during implementation. That is not a scaffolder producing a table and a form and calling it an app. That is the boring 80 percent of the first afternoon, including the parts people skip.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The Solution: the dry run is the whole feature&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Here is where I would slow down, because the interesting design decision is not the generation. It is the gate in front of the generation.&lt;/p&gt;

&lt;p&gt;The skill runs an interactive, multistep process, and the documentation is explicit about the order. You describe the business scenario, and the skill asks follow-up questions about users, data, workflows and the experience you want. It then presents an &lt;b&gt;app spec&lt;/b&gt;: personas, jobs to be done, tables, relationships, forms, views, charts, JavaScript validation rules, generative page intents, and the sitemap with a proposed icon for each table. You review and refine that specification. Only then does it validate the spec and present a &lt;b&gt;dry-run build plan&lt;/b&gt;, grouped by build phase, in the conversation. And then the sentence that carries the whole design:&lt;/p&gt;

&lt;blockquote&gt;No app artifacts are created until you approve the plan.&lt;/blockquote&gt;

&lt;p&gt;You may be asking why a plan step deserves this much attention when every generator ever built has had a preview mode. Very simple: the failure mode of an AI that writes to Dataverse is not a bad form. It is thirty artifacts you did not ask for, scattered across a solution, with a publisher prefix on all of them and no obvious way to tell which ones matter. A plan you can read and argue with before anything is written is the difference between a tool you can use in a real environment and a tool you can only use in a sandbox you are willing to throw away.&lt;/p&gt;

&lt;p&gt;After the build, there is an optional verification step that compares the deployed app with the approved app spec. That is worth noticing too. &quot;The build reported success&quot; and &quot;the app matches what I approved&quot; are two different claims, and only one of them is worth anything on Monday morning.&lt;/p&gt;

&lt;p&gt;NOTE: the editing path is the same skill and it works in reverse. Point it at an existing app and it retrieves the deployed app, builds an &lt;i&gt;editable app spec&lt;/i&gt; from it, and then proposes changes against that spec. Which means the spec is not just an input format. It is a two-way representation of the app, and that is the part of this design most likely to still be here in three years.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Why it matters&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;makers&lt;/b&gt;, this is genuinely for you, and that is a change from most of what lands in the developer docs. The article is tagged for makers and developers both, and the entry point is a sentence about your business process rather than a table designer. But be honest with yourself about the toolchain: this needs Node.js, the Power Platform CLI, the Azure CLI, and a terminal-based AI coding tool on your machine. If you have never opened a command prompt on purpose, the skill is not the barrier. The prerequisites are. That is a fair trade to know about up front rather than discover forty minutes in.&lt;/p&gt;

&lt;p&gt;For &lt;b&gt;professional developers&lt;/b&gt;, the claim worth weighing is the app spec. We have spent years arguing about how to describe a model-driven app in a form that survives review, diffing and source control, and the answers have all been either solution XML, which nobody enjoys reading, or a wiki page that goes stale in a week. A reviewable specification that generates the app and can be regenerated from the app is a different proposition. Whether the schema holds up is an open question, and the documentation says plainly that it might change between releases, but the shape is right.&lt;/p&gt;

&lt;p&gt;Two cautions, both taken from the documentation rather than invented by me. This is &lt;b&gt;preview&lt;/b&gt;, and Microsoft states that the app spec schema and command-line options might change between releases. And the skill does not support every model-driven app artifact or concept, so some of your app will still be built in the designers. The related warning is the one I would put on a sticky note: changes you make outside the skill can affect later AI-assisted edits, so before you approve another build, read the plan to confirm it preserves your manual customizations. An agent that regenerates from a spec has no idea what you changed by hand unless the spec caught it.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;The demo concept&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Not &quot;watch it build an app,&quot; because a video of artifacts appearing proves nothing about whether they were the right artifacts. The demonstration worth building is the &lt;b&gt;round trip&lt;/b&gt;, and it takes three moves.&lt;/p&gt;

&lt;p&gt;Build a small app from a prompt and keep the approved app spec. Then go into Power Apps and change something by hand that the spec never knew about, a business rule, a column on a form, a tweaked view. Then ask the skill for an unrelated change and read the new plan carefully to see whether your manual work survives. That single observation is more useful than any amount of generation footage, because it answers the only question a working developer actually has: can I use this on an app I care about, or only on one I am willing to lose?&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Steps to recreate&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;1) Get the prerequisites in place first. The documentation lists a current long-term support version of &lt;b&gt;Node.js&lt;/b&gt;, &lt;b&gt;Power Platform CLI (PAC CLI) 2.7.0 or later&lt;/b&gt;, the latest &lt;b&gt;Azure CLI&lt;/b&gt;, and &lt;b&gt;GitHub Copilot CLI&lt;/b&gt; or &lt;b&gt;Claude Code&lt;/b&gt;. You also need a Power Platform environment where you can create and modify Dataverse and model-driven app artifacts, an authenticated PAC CLI profile pointed at that environment, and an authenticated Azure CLI session.&lt;/p&gt;

&lt;p&gt;2) Make sure both authentications are the same person. This is the one prerequisite that reads like a footnote and behaves like a blocker:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;az login&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. The documentation is specific that you run &lt;code&gt;az login&lt;/code&gt; with &lt;i&gt;the same identity used by the active PAC CLI profile&lt;/i&gt;. Two different accounts across the two CLIs is the classic way to get a confusing permission error much later in the run, long after you have forgotten which account each tool holds. Check the active PAC profile with &lt;code&gt;pac auth list&lt;/code&gt; before you start, and if the two do not match, fix it now rather than at step six.&lt;/p&gt;

&lt;p&gt;3) Install the plugin. There is a one-line installer that sets up all of the Power Platform plugins at once, in PowerShell:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;iwr https://raw.githubusercontent.com/microsoft/power-platform-skills/main/scripts/install.js -OutFile install.js; node install.js; del install.js&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. This downloads a JavaScript file, runs it with Node, and deletes it, which is exactly the kind of command you should read before you run. The installer detects which supported AI code generation tools you have, registers the Power Platform Skills marketplace, installs the plugins, and turns on automatic updates. The source lives in the public &lt;a href=&quot;https://github.com/microsoft/power-platform-skills&quot;&gt;microsoft/power-platform-skills&lt;/a&gt; repository, so you can read &lt;code&gt;scripts/install.js&lt;/code&gt; first, and I would encourage you to do exactly that. There is an equivalent for a Windows command window:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://raw.githubusercontent.com/microsoft/power-platform-skills/main/scripts/install.js | node&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;4) Or install just the one plugin, which is what I would do the first time. These are slash commands typed into the AI coding tool&#39;s own chat, not shell commands:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/plugin marketplace add microsoft/power-platform-skills
/plugin install model-apps@power-platform-skills&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. The first command registers the marketplace, the second installs the Power Apps plugin from it, and the part after the &lt;code&gt;@&lt;/code&gt; is the marketplace name. Restart the tool afterward if it does not pick up the plugin on its own. For Claude Code specifically, the documentation points out that plugins can be installed at global, local or user scope, and that depending on the scope you must be in the correct directory for the plugin to load. If the skill appears to be missing, check where you are standing before you reinstall anything.&lt;/p&gt;

&lt;p&gt;5) Build something. Start a conversation with the tool and invoke the skill:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/app-builder

Build an equipment inspection app for field technicians and dispatchers.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. That example prompt is quoted from the documentation, along with &quot;Build an app to manage supplier onboarding and contract approvals&quot; and &quot;Create a model-driven app for tracking service requests across multiple teams.&quot; Notice the shape they all share: a business process and the people in it, not a list of tables. That is deliberate, because personas are a first-class part of the app spec and they are what the security roles get derived from. You can also skip &lt;code&gt;/app-builder&lt;/code&gt; and simply describe the app you want.&lt;/p&gt;

&lt;p&gt;6) Then work the review, which is the part that earns the whole exercise. Answer the skill&#39;s questions about the process, users, data and workflows. Read the app spec and check that the personas, jobs to be done, tables, relationships, forms, views, charts, validation rules, generative pages, navigation and sitemap icons actually support the business scenario, and ask for revisions until they do. Read the dry-run build plan and approve it only when its proposed changes match your intent. Let it build. Take the optional verification step. Then open the app at &lt;a href=&quot;https://make.powerapps.com&quot;&gt;make.powerapps.com&lt;/a&gt;, run its primary scenarios, and inspect the generated artifacts with your own eyes.&lt;/p&gt;

&lt;p&gt;7) To edit an app you already have, invoke &lt;code&gt;/app-builder&lt;/code&gt; again and name it:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/app-builder

Add an Invoices table and related forms and views to the Supplier Management app.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note. The skill asks you to confirm which existing app it should retrieve, then presents an editable app spec built from the deployed app, and then the same plan-and-approve cycle applies. This is the path where the caution about manual customizations bites, so read that plan properly rather than skimming for the word &quot;success.&quot;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;One more thing that landed in the same set of changes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The generative pages article picked up a genuinely useful capability that has nothing to do with AI coding tools, and it would be a shame to let it get buried: you can now &lt;b&gt;embed a generative page inside a model-driven app form&lt;/b&gt;, in a section or a tab. Create and publish a generative page set up to accept the &lt;code&gt;recordId&lt;/code&gt; input parameter, then:&lt;/p&gt;

&lt;p&gt;1) Open the form in the form designer.&lt;br/&gt;
2) In the left pane, select &lt;b&gt;Components&lt;/b&gt;, and then expand &lt;b&gt;Display&lt;/b&gt;.&lt;br/&gt;
3) Select &lt;b&gt;Generative page&lt;/b&gt;.&lt;br/&gt;
4) Select the generative page you want to embed.&lt;br/&gt;
5) Optionally, provide values for any additional static inputs the page accepts.&lt;br/&gt;
6) Save and publish the form.&lt;/p&gt;

&lt;p&gt;When a user opens a record, the form passes the current record ID to the page as &lt;code&gt;recordId&lt;/code&gt; automatically. You do not configure a static value for it. That is a small paragraph in the docs and a fairly large door, because it turns generative pages from a separate destination into something that lives on the record the user is already looking at.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Companion repo sketch&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;A lab repository built around the round trip rather than the generation. A &lt;code&gt;prompts/&lt;/code&gt; folder with the exact prompts in order, so somebody else can reproduce the run instead of taking your word for it. A &lt;code&gt;specs/&lt;/code&gt; folder holding the approved app spec from the first build and the regenerated spec from after the manual edit, because the diff between those two files &lt;i&gt;is&lt;/i&gt; the finding. A &lt;code&gt;plans/&lt;/code&gt; folder with the dry-run build plans exactly as they appeared, including the one you rejected, since a lab that records only the approved plan is hiding the interesting half. A &lt;code&gt;solution/&lt;/code&gt; folder with the exported and unpacked solution so the generated artifacts show up as reviewable files. And a short &lt;code&gt;FINDINGS.md&lt;/code&gt; that says which artifacts the skill got right, which it missed, and whether the manual customization survived, in plain language.&lt;/p&gt;

&lt;p&gt;Point the README at a developer environment, say so loudly, and repeat it. This is preview software that writes schema. Nobody should be pointing it at production to find out what it does.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Final Notes&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;What I keep coming back to is that the headline feature of this skill is a document. Not the generation, which is impressive but is the part everybody expected. The app spec is the thing: a reviewable description of a model-driven app, produced from a conversation, editable by a human, capable of building the app and capable of being rebuilt from it. We have wanted that artifact for a very long time, and it is a little funny that it arrives as a side effect of teaching a coding agent to click the designers for us.&lt;/p&gt;

&lt;p&gt;It is preview, so treat it accordingly. Read the installer before you run it, keep your two CLI logins on the same identity, use a developer environment, and read every plan before you approve it. And if you take one habit from the whole exercise, make it the verification step, because the gap between &quot;it built&quot; and &quot;it matches what I approved&quot; is where every automation story eventually gets interesting.&lt;/p&gt;

&lt;p&gt;What I learned from this exercise: when a tool asks you to approve a plan, the plan is not a formality it added to be polite. It is the tool telling you exactly where the responsibility moves from the machine back to you.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br/&gt;
Mariano Gomez Bent&lt;br/&gt;
Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/8604335111466068845/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/8604335111466068845' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8604335111466068845'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8604335111466068845'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/09/power-apps-describe-app-approve-plan.html' title='Power Apps | Describe the App, Approve the Plan'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-5958214116179228133</id><published>2026-08-31T07:00:00.000-04:00</published><updated>2026-08-31T07:00:00.121-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ALM"/><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="MCP"/><category scheme="http://www.blogger.com/atom/ns#" term="Open Source"/><category scheme="http://www.blogger.com/atom/ns#" term="pac CLI"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Platform"/><title type='text'>Copilot Studio | Turning War Stories into Guardrails: Introducing pac-copilot-kit</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Turning war stories into guardrails: building pac-copilot-kit&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-10-pac-copilot-kit.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Let me start with the announcement, because some of you came for the tool and not the story, and that is perfectly fine.&lt;/p&gt;

&lt;p&gt;Today I am releasing &lt;strong&gt;pac-copilot-kit&lt;/strong&gt;, open source under MIT at &lt;a href=&quot;https://github.com/dgpblogster/pac-copilot-kit&quot;&gt;github.com/dgpblogster/pac-copilot-kit&lt;/a&gt;. It is a paved-road toolkit for the Copilot Studio agent lifecycle, and it does exactly two things. First, capability: it creates Dataverse knowledge sources from code, inside your solution, repeatably, which is the one thing &lt;code&gt;pac&lt;/code&gt; cannot do at all. Second, the paved road: it makes Microsoft&#39;s own ALM prescription executable, one guarded command from YAML on disk to a published agent, in a shell or in CI. The first is why you install it. The second is why you keep it.&lt;/p&gt;

&lt;p&gt;The PowerShell module is on the PowerShell Gallery as &lt;code&gt;PacCopilotKit&lt;/code&gt;, and an MCP server that exposes the same engine to Claude Code, GitHub Copilot, or Codex is on npm as &lt;code&gt;pac-copilot-kit-mcp&lt;/code&gt;. Two commands and you are running:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Install-Module PacCopilotKit
npx -y pac-copilot-kit-mcp   # optional: the MCP door, for your AI agent&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Issues and pull requests are welcome, and the war-stories document in the repository is the living inventory of every gotcha the tool guards against. If that is all you needed, go build something and let me know how it goes!&lt;/p&gt;

&lt;p&gt;Now, if you brought popcorn, stay for the rest, because the story of &lt;em&gt;why&lt;/em&gt; this tool exists is the more interesting half, and it includes one moment where the build caught its own author in an overclaim.&lt;/p&gt;

&lt;h2&gt;Where it came from&lt;/h2&gt;

&lt;p&gt;My last two Copilot Studio articles, one on &lt;a href=&quot;https://www.theworkbench.blog/2026/08/copilot-studio-adding-dataverse.html&quot;&gt;wiring Dataverse knowledge sources with the Web API&lt;/a&gt; and one on &lt;a href=&quot;https://www.theworkbench.blog/2026/08/copilot-studio-real-alm-for-agents-and.html&quot;&gt;the ALM loop that keeps an agent rebuildable&lt;/a&gt;, both ended the same way: with a list of war stories and their workarounds. The runtime that silently ignores your knowledge source. The &lt;code&gt;pac&lt;/code&gt; auth profile that another workspace quietly re-points. The Quick Find view update that fails with an opaque error code. Each one cost me real hours, each one got written down, and I felt rather good about that.&lt;/p&gt;

&lt;p&gt;Then a thought started nagging at me. A war story you have to remember is a war story you will eventually forget. Six months from now, in a different tenant, I will hit one of these again and I will not remember the article, and neither will you. Writing it down was necessary. It was nowhere near sufficient. So, this time around, I turned the whole inventory into a tool.&lt;/p&gt;

&lt;h2&gt;From paragraphs to guardrails&lt;/h2&gt;

&lt;p&gt;The idea fits in one sentence: every war story becomes something that executes. Not documentation you consult, but a check that runs, refuses, and tells you why. I call them guardrails, and as it turns out, they come in exactly two flavors. &lt;strong&gt;Preflight checks&lt;/strong&gt; refuse before anything is touched: the runtime trap and the authentication requirement from the knowledge article, the profile drift from the ALM article, each now a refusal with the fix named in the message. &lt;strong&gt;Error translators&lt;/strong&gt; let a call happen and catch a known failure on the way back: when the infamous &lt;code&gt;0x80040216&lt;/code&gt; shows up on a savedquery update, you get the war story and the working alternatives, not the opaque code.&lt;/p&gt;

&lt;p&gt;Every guardrail lives under a contract from day one: one file, one test proving the failure it prevents is real, and one plain-language entry in a war-stories document that travels with the code. No entry, no guardrail. The document I would have written anyway now cannot drift from the code, because the code refuses to exist without it. And every refusal carries a typed exit code whose range means something: 10 through 19 says your environment is misconfigured, 20 says you asked for a route the platform is known to break. You may be asking why anyone should care about an exit code taxonomy. Very simple: CI cares. A pipeline that can tell &quot;the environment is wrong&quot; from &quot;the operation failed&quot; is one a human can diagnose from the log line alone.&lt;/p&gt;

&lt;p&gt;The natural home for all of this was a PowerShell module. But I work with an AI coding agent driving a lot of the platform interaction now, and an agent cannot benefit from a guardrail it cannot see, so the same engine got a second door: the MCP server, seven task-shaped verbs over the same guardrails. My favorite is the one with no cmdlet behind it, &lt;code&gt;explain-failure&lt;/code&gt;: when any operation is refused, the agent can ask what just happened and gets the war story, the fix, and a warning against working around the refusal, straight from the exit code registry, without another round trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I did not build this alone, and pretending otherwise would be strange in 2026. I paired with an AI coding agent (Claude Code, for the record) for the entire build. Keep in mind the division of labor, though: every platform fact in the tool was verified against a live environment before it was allowed to ship, because an AI pair is exactly like every other tool in my shop. Trust, but verify.&lt;/p&gt;

&lt;h2&gt;How my own tool tried to lie to me&lt;/h2&gt;

&lt;p&gt;Once the first guardrails were in place, we ran a red team pass against our own code: thirteen tests written to attack the guardrails the way a careless caller would, written before any fixes, expected to fail. Seven landed. Seven! The worst were bypasses of the flagship guardrail, which watched for &lt;code&gt;savedqueries(&lt;/code&gt; at the start of a request path. The very same forbidden request sailed right past it spelled as an absolute URL, or with a leading slash, or with the payload as a raw JSON string. Four spellings of one request, and the guardrail caught exactly one. Now, you may be thinking the fix is three more string checks. It is not! The fix was structural: normalize every request into one canonical shape before any check sees it, so the whole class of bypass dies at once. Suffice to say, every guardrail now gets a bypass hunt, red first, before it ships.&lt;/p&gt;

&lt;h2&gt;What live verification taught me, including about my own articles&lt;/h2&gt;

&lt;p&gt;The unit suite proves the code does what we intended. The live runs proved something better: that one of my published claims was too broad, and that the platform had two more surprises waiting. Three findings, in ascending order of embarrassment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the drift guardrail caught me. Me!&lt;/strong&gt; The very first live pipeline run refused to deploy, because the active &lt;code&gt;pac&lt;/code&gt; profile on my machine pointed at a completely different environment than the one I had pinned. The founding war story of the whole effort, firing on its author on day one. I could not have scripted a better validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, my savedquery claim did not survive a third environment.&lt;/strong&gt; I reported that the &lt;code&gt;fetchxml&lt;/code&gt; update fails on every route, reproduced in two orgs. The integration test then found views that accept the very same call. We spent an afternoon hunting the discriminator: custom versus system table? No. The search index flag? No. Standard versus virtual versus elastic? Also no. The honest current claim is that the failure reproduces on most Quick Find views while a minority accept the call, and the discriminator is not yet identified. The guardrail was redesigned that same day, from refuse-up-front to attempt-and-translate, because refusing a route that sometimes works is its own kind of bug. If you figure out what separates the two, the comments are open and I will credit you properly!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, and this one should worry every CI pipeline you own: &lt;code&gt;pac&lt;/code&gt; can print an error and hand your shell a success code.&lt;/strong&gt; Three times in one session, the CLI wrote a clear &lt;code&gt;Error:&lt;/code&gt; line and returned 0: an argument error twice, and an environment resolution failure. Any pipeline gating on the exit code treats all three as success and keeps right on going. Before publishing that sentence I wanted to know &lt;em&gt;which&lt;/em&gt; part of &lt;code&gt;pac&lt;/code&gt; was doing it, so I ran the same failing command two ways. Through the MSI-installed &lt;code&gt;pac&lt;/code&gt; on my &lt;code&gt;PATH&lt;/code&gt;, a two-line wrapper that calls &lt;code&gt;pac.launcher.exe&lt;/code&gt;: error printed, exit code 0. Through the versioned &lt;code&gt;pac.exe&lt;/code&gt; underneath it: same error, exit code 1. As it turns out, &lt;code&gt;pac&lt;/code&gt; itself tells the truth. It is the launcher that discards the exit code, on every command I tried, and that launcher is version 1.0.7 with a file date of September 2019, still shipping in the current installer. A seven-year-old shim, silently defeating a command Microsoft documents as safe to run in a build pipeline!&lt;/p&gt;

&lt;p&gt;This is not new. It sits on Microsoft&#39;s tracker as &lt;a href=&quot;https://github.com/microsoft/powerplatform-build-tools/issues/912&quot;&gt;issue #912&lt;/a&gt; (the launcher) and &lt;a href=&quot;https://github.com/microsoft/powerplatform-build-tools/issues/1027&quot;&gt;issue #1027&lt;/a&gt; (the symptom, with other users chiming in), both still open as I write this, and I have &lt;a href=&quot;https://github.com/microsoft/powerplatform-build-tools/issues/912#issuecomment-5309857001&quot;&gt;added the isolation above to the thread&lt;/a&gt;. In the meantime the tool refuses to believe a zero exit code when the output carries an &lt;code&gt;Error:&lt;/code&gt; line, and I would gently suggest your scripts do the same, or install &lt;code&gt;pac&lt;/code&gt; as a &lt;code&gt;dotnet tool&lt;/code&gt;, which skips the launcher entirely. The lesson generalizes: verify the exit codes of every CLI you automate, through the exact path your pipeline will call, because a wrapper on the &lt;code&gt;PATH&lt;/code&gt; can defeat a perfectly honest executable underneath it.&lt;/p&gt;

&lt;h2&gt;The bottom line&lt;/h2&gt;

&lt;p&gt;A war story is a liability until it is executable. The moment the savedquery story became a guardrail with a test, the test caught my own overclaim, and documentation cannot do that. That is the whole reason pac-copilot-kit exists, and the discriminator hunt from finding number two is genuinely open, so if you run it against your own environments, I would love to hear what you find.&lt;/p&gt;

&lt;p&gt;My next installment will be the one I promised last time: the walk from proof of concept to production, with the actual promotion done rather than theorized. Stay tuned!&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/5958214116179228133/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/5958214116179228133' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5958214116179228133'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5958214116179228133'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/copilot-studio-turning-war-stories-into.html' title='Copilot Studio | Turning War Stories into Guardrails: Introducing pac-copilot-kit'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-8320439968606332011</id><published>2026-08-28T07:00:00.000-04:00</published><updated>2026-08-28T07:00:00.120-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ALM"/><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="Dataverse"/><category scheme="http://www.blogger.com/atom/ns#" term="DevOps"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Platform"/><title type='text'>Copilot Studio | Real ALM for Agents (and the One Click That Breaks Your Pipeline)</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Real ALM for Copilot Studio agents: the repo is the agent&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-09-agent-alm.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;
&lt;p&gt;In my &lt;a href=&quot;https://www.theworkbench.blog/2026/08/copilot-studio-adding-dataverse.html&quot;&gt;previous Copilot Studio article&lt;/a&gt;, I was working on a support agent grounded in Dataverse knowledge articles and a custom table of curated case resolutions, and I showed how to wire those knowledge sources up with the Web API rather than clicking them together in the maker portal. What I did not explain there is why I cared so much about scripting them in the first place.&lt;/p&gt;

&lt;p&gt;The reason is a question every consultant eventually asks about a sandbox: what happens to all of this when somebody refreshes the environment from production? The answer, of course, is that it all goes away. The agent, its topics, its knowledge sources, the custom table behind them, all of it. And the honest follow-up is the question that actually matters: how long would it take me to build it back?&lt;/p&gt;

&lt;p&gt;If the answer is &quot;about a week, and I would probably forget something,&quot; then what you have is not a project. It is a sandcastle! So, in this article I will walk through the lifecycle loop I settled on for a Copilot Studio agent, score it honestly against what Microsoft prescribes today, and show you the three places where the prescribed path quietly runs out of road. All three are documented by Microsoft, in public, and not one of them is where you would expect.&lt;/p&gt;

&lt;h2&gt;What Microsoft actually prescribes&lt;/h2&gt;

&lt;p&gt;Let me start by being fair to the guidance, because it is good guidance and I follow most of it. Microsoft&#39;s ALM story for Copilot Studio agents is &lt;strong&gt;solutions&lt;/strong&gt;, and it has not wavered: &lt;a href=&quot;https://learn.microsoft.com/microsoft-copilot-studio/authoring-solutions-overview&quot;&gt;the documentation&lt;/a&gt; is explicit that to move an agent between environments you need a custom solution rather than the default one. On top of that sits a set of &lt;a href=&quot;https://learn.microsoft.com/microsoft-copilot-studio/guidance/alm&quot;&gt;ALM golden rules&lt;/a&gt; familiar to anyone who has shipped a model-driven app: work in solutions, use a custom publisher and prefix, use environment variables for anything that changes between environments, deploy as managed downstream, and automate it all with source control. Microsoft also states a floor: at least three environments, development, test, and production.&lt;/p&gt;

&lt;p&gt;Alongside that, there is now a genuine authoring story for code-first people. The Visual Studio Code extension is generally available, the &lt;code&gt;pac copilot&lt;/code&gt; command group gives you a local workspace of YAML files, and the pitch includes Git and pull requests. Suffice to say, this is a real improvement over clicking everything in a portal and hoping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; read the banner at the top of those pages. Every ALM and code-first article I could find says the features described are powered by the &lt;em&gt;standard harness&lt;/em&gt;. If you are building in the newer agent experience, hold that thought.&lt;/p&gt;

&lt;h2&gt;The loop I actually ran&lt;/h2&gt;

&lt;p&gt;Here is the cycle, start to finish. Nothing exotic, and that is rather the point:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pac copilot init                          # scaffold a local agent workspace
# author agent.mcs.yml, settings.mcs.yml, topics/*.mcs.yml
# validate every file offline, before anything touches the tenant
pac copilot pack --publisher-prefix wrk --solution-name WorkbenchSupportAssistant
pac solution import --path .\WorkbenchSupportAssistant.zip --publish-changes
git add . &amp;amp;&amp;amp; git commit&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note here. The offline validation step is mine, not Microsoft&#39;s, and it is the single highest-value habit in the loop: a Power Fx string containing a colon followed by a space parses wrong and surfaces as strange runtime behavior rather than an error, and catching that locally costs a second where catching it after deployment costs an afternoon. &lt;code&gt;pac copilot pack&lt;/code&gt; is the interesting one: &lt;a href=&quot;https://learn.microsoft.com/power-platform/developer/cli/reference/copilot&quot;&gt;Microsoft documents it&lt;/a&gt; as a purely local operation that needs no authentication and no environment, which makes it, in their own words, safe to run in a build pipeline. Hold that thought too. And then, always, a commit. That repository is not a nicety. It &lt;em&gt;is&lt;/em&gt; the recovery plan.&lt;/p&gt;

&lt;h2&gt;Scoring myself against the golden rules&lt;/h2&gt;

&lt;table&gt;
&lt;tr&gt;&lt;th&gt;Golden rule&lt;/th&gt;&lt;th&gt;What I did&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Always work in the context of solutions&lt;/td&gt;&lt;td&gt;Followed. Everything packs and imports as one solution.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Use a custom publisher and prefix&lt;/td&gt;&lt;td&gt;Followed, from day one.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Use environment variables for what changes between environments&lt;/td&gt;&lt;td&gt;Followed to the letter. The escalation address lives in an environment variable, never in a topic node.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Do not customize outside a development environment&lt;/td&gt;&lt;td&gt;Followed, though see below, because I only had one.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Create separate solutions only when you need independent deployment&lt;/td&gt;&lt;td&gt;Followed. One solution.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Automate ALM with source control&lt;/td&gt;&lt;td&gt;Followed, and then some.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Deploy as managed downstream&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Not followed.&lt;/strong&gt; Unmanaged only.&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;Six out of seven, and the miss is not laziness. It follows from a platform constraint that will hit you too: &lt;strong&gt;Copilot Studio&#39;s Dataverse knowledge sources only read the agent&#39;s own environment.&lt;/strong&gt; My agent had to be grounded on data that lived in one specific environment, so the agent had to live there as well. That collapses the three-environment model into one, and with nothing downstream to deploy to, managed solutions buy you nothing but ceremony. That is a limitation of my situation, not a flaw in the guidance. Keep in mind, though, that &quot;the data only exists in one place&quot; is an extremely common shape for a grounded agent, and the standard ALM advice does not really account for it.&lt;/p&gt;

&lt;h2&gt;Where the prescribed path runs out&lt;/h2&gt;

&lt;p&gt;Three gaps, and I found all three the hard way before finding them in the documentation afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: the command built for pipelines has no pipeline.&lt;/strong&gt; Microsoft explicitly documents &lt;code&gt;pac copilot pack&lt;/code&gt; as safe to run in a build pipeline, so I went looking for the pipeline that accepts it. Let me be precise, because this claim deserves precision: agents are solution components, so the generic solution tasks deploy one just fine, and Microsoft&#39;s Build Tools FAQ says as much. What the &lt;a href=&quot;https://learn.microsoft.com/power-platform/alm/devops-build-tool-tasks&quot;&gt;Power Platform Build Tools task list&lt;/a&gt; does not contain is anything that knows an agent from a canvas app: no task wraps &lt;code&gt;pac copilot&lt;/code&gt;, and the only &quot;agent&quot; in that reference is the build agent running the job. &lt;a href=&quot;https://learn.microsoft.com/power-platform/alm/devops-github-available-actions&quot;&gt;GitHub Actions for Power Platform&lt;/a&gt; is the same story. I could not find a single Microsoft sample pipeline that runs &lt;code&gt;pac copilot pack&lt;/code&gt; followed by &lt;code&gt;pac solution import&lt;/code&gt;. You can build it yourself, and it is not hard, but the CLI was designed for CI, documented for CI, and the CI tooling has not caught up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second: the newer agent experience has no documented lifecycle at all.&lt;/strong&gt; Copilot Studio now ships more than one runtime, and I wrote last time about how that split silently breaks knowledge grounding. It splits the ALM story more sharply. Every solutions, import/export, and code-first article carries the standard-harness banner, and the newer experience&#39;s documented lifecycle is create, build, test, publish, monitor, all inside a single environment. No promotion, no solutions, no source control story. There is a related asymmetry worth knowing: in the newer experience each publish creates a new version, which &lt;a href=&quot;https://learn.microsoft.com/microsoft-copilot-studio/agents-experience/publication-fundamentals-publish-channels&quot;&gt;the documentation&lt;/a&gt; frames as a way to tell the live release from newer drafts. That is a counter, and I found no documented way to browse history or restore a previous version; rollback lives at the solution layer instead. So an agent can carry two unrelated version numbers, and only one can take you backward. I will say only what I can defend: I found no documentation of a restore capability, which is not the same as proving there is none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, and this is the one that changes how you should build: there is a manual click in the middle of the automated path.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;The one click that breaks your pipeline&lt;/h2&gt;

&lt;p&gt;Microsoft publishes a &lt;a href=&quot;https://learn.microsoft.com/troubleshoot/power-platform/copilot-studio/lifecycle-management/agents-solution-mapping&quot;&gt;troubleshooting article&lt;/a&gt; for agents whose components go missing after a solution import. The symptoms it lists are topics, environment variables, tools, child agents, and, yes, knowledge sources, all present in the source environment and absent in the target. The cause it gives is that the imported solution does not automatically include components added to the agent&#39;s source solution after the fact. The prescribed fix is to open the agent&#39;s commands menu, choose &lt;strong&gt;Advanced&lt;/strong&gt;, select &lt;strong&gt;Add required objects&lt;/strong&gt;, and export again, and the instruction is explicit that you repeat this before every export.&lt;/p&gt;

&lt;p&gt;Read that as an operations person rather than a maker. The fidelity of your deployment depends on a human remembering to click something in a portal &lt;em&gt;before every export&lt;/em&gt;. There is no CLI equivalent I could find, and no pipeline step, so your automated deployment has a manual precondition that fails silently when skipped. Nothing errors. The solution imports cleanly. The agent simply answers as though a knowledge source it needs was never there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; the documentation is candid about the general shape of this. The &lt;a href=&quot;https://learn.microsoft.com/microsoft-copilot-studio/authoring-solutions-import-export&quot;&gt;import and export guidance&lt;/a&gt; says outright that some components, custom topics and knowledge sources among them, might not be included depending on how they were created or linked; there is a published list of things that are simply not solution-aware (Application Insights settings, manual authentication, Direct Line and web channel security, deployed channels, sharing); and after any import you must reconfigure authentication and publish before sharing. None of it is hidden. It is just easy not to go looking until something breaks.&lt;/p&gt;

&lt;h2&gt;Why scripting the components sidesteps all of it&lt;/h2&gt;

&lt;p&gt;In the previous article I showed that a Dataverse knowledge source is really four ordinary Dataverse records, and that one header on each create call, &lt;code&gt;MSCRM.SolutionUniqueName&lt;/code&gt;, lands them inside your solution. That was written as a way around a tooling limitation. It turns out to be the whole answer to the click.&lt;/p&gt;

&lt;p&gt;The knowledge wiring in this project is a PowerShell script. It takes a token from the signed-in Azure CLI, talks to the Dataverse Web API directly, and creates the custom table, its columns, the table search configurations, the knowledge-source components, and the associations between them, every call carrying that header. The shape is the part worth stealing:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;$org   = &quot;https://yourorg.crm.dynamics.com&quot;
$token = (az account get-access-token --resource $org | ConvertFrom-Json).accessToken

$headers = @{
  Authorization             = &quot;Bearer $token&quot;
  &quot;Content-Type&quot;            = &quot;application/json&quot;
  &quot;MSCRM.SolutionUniqueName&quot; = &quot;WorkbenchSupportAssistant&quot;   # every call. no exceptions.
}

$search = Invoke-RestMethod -Method Post -Headers $headers `
  -Uri &quot;$org/api/data/v9.2/dvtablesearchs&quot; `
  -Body (@{ name = &quot;wrk_case_resolutions_search&quot; } | ConvertTo-Json)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note here. The token comes from the Azure CLI, so there is no credential in the script and nothing to rotate in source control. The solution header sits in the shared &lt;code&gt;$headers&lt;/code&gt; block rather than being passed per call, which is the whole discipline in one line: you cannot forget it on the fourth create if it was never optional to begin with. And it deliberately depends on no &lt;code&gt;pac&lt;/code&gt; state whatsoever, because &lt;code&gt;pac&lt;/code&gt; authentication profiles are machine-global rather than per-project, and I have had a different workspace on the same laptop quietly re-point the profile my project was using. A recovery script that assumes the CLI is still aimed where you left it is a recovery script that fails on the one day you need it.&lt;/p&gt;

&lt;p&gt;Because the knowledge sources come from a script I can run again, I never depend on a solution remembering them. The failure mode Microsoft documents cannot happen to a component I rebuild from source in ninety seconds. The same goes for the custom table, its columns, and the flow behind the escalation topic, which had to be created with a direct call to the Dataverse workflow endpoint carrying that same header, because the tooling I first reached for created a flow that never appeared in the solution at all.&lt;/p&gt;

&lt;p&gt;And when something resists the API entirely, there is one more door. Last time I noted that updating a Quick Find view&#39;s find columns through the Web API fails with an opaque error, and that the maker portal was the fastest way through. Here is the part I promised to come back to: the same change can be made at the solution level, without clicking anything.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pac solution export --name WorkbenchSupportAssistant --path .\out
# unzip, edit the savedquery block in customizations.xml, re-zip
pac solution import --path .\WorkbenchSupportAssistant.zip --publish-changes&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I have come to think of this as solution surgery, and the safety rule is the whole trick: a Quick Find view is an ordinary Dataverse view, not an agent component. Microsoft warns that changing an agent&#39;s components directly inside the solution breaks export and import, and that warning is correct and worth obeying. The rest of the solution is ordinary Power Platform metadata and behaves accordingly. Keep in mind the import round-trips everything else in the solution, so if you have a flow in there, verify afterward that it came back activated. Mine did, but I checked.&lt;/p&gt;

&lt;h2&gt;What this approach costs, honestly&lt;/h2&gt;

&lt;p&gt;The scripts are real code, and real code needs maintenance. They encode undocumented record shapes that can change in any release, so my recovery plan has a shelf life and needs re-verification rather than blind trust. I gave up managed solutions, and with them the layering protections that stop someone editing production by hand. Parts of what I built sit outside the supported path, which is a fine place for a proof of concept and a rather different proposition for production. And I gave up the three-environment model, so I have no test environment standing between an idea and the only environment that exists, and I want to be blunt that source control does nothing whatsoever about that.&lt;/p&gt;

&lt;p&gt;That last point deserves precision, because I have seen it oversold. What all of this bought me is that &lt;strong&gt;the environment became replaceable, not unnecessary.&lt;/strong&gt; Rebuildability protects you from losing work; a test environment protects you from breaking something in front of people, and those are two different problems with two different answers. Anyone who tells you a tidy repository removes the need for proper environments is selling you something. Here is what reconciles the two, though, and it is why I would do it all again: &lt;strong&gt;being able to rebuild the whole agent from source is precisely what makes a second environment cheap.&lt;/strong&gt; When everything comes back by running a script, standing up a test environment stops being a project and becomes an afternoon pointed at a different URL. The discipline that looks like a substitute for the three-environment model is actually what makes it affordable.&lt;/p&gt;

&lt;h2&gt;So what does production actually look like?&lt;/h2&gt;

&lt;p&gt;Which is the question you are probably asking from inside your own proof of concept, and it has three answers worth knowing now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Placement is not a choice.&lt;/strong&gt; Dataverse knowledge reads only the agent&#39;s own environment, so the production agent lives in production, right alongside live customer data. Promoting a grounded agent is not like promoting a self-contained app into a tidy empty environment; you are placing an agent inside your live business system, and every decision about what it can see and say gets more serious the moment you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution carries less than you would hope.&lt;/strong&gt; The agent, its topics, your table&#39;s schema, the environment variable definitions, and your flows travel. Authentication, deployed channels, sharing, and Application Insights settings do not, so a production deployment ends with an afternoon of hand configuration, plus pointing your environment variables at real values instead of the test address you have used since day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your curated knowledge is data, not schema.&lt;/strong&gt; The table definition rides along in the solution; the reviewed content in it does not. So you choose between regenerating that content against production data and migrating the rows you already reviewed, and as it turns out, regeneration is the tempting answer and probably the wrong one, because synthesis is not deterministic and re-running it discards the editorial judgement that was the expensive part. Migrate the reviewed rows, including the ones you deliberately rejected, so the audit trail stays intact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; one sequencing trap. You cannot convert an unmanaged solution into a managed one in place. A development environment holding your work unmanaged is exactly right, and you export managed from it into production. But if you ever decide that environment should instead &lt;em&gt;receive&lt;/em&gt; managed solutions, the unmanaged solution has to come out first, and it takes your agent with it. Decide what each environment is for before the first managed export, not after.&lt;/p&gt;

&lt;p&gt;And then there is the part that is not really an ALM question at all: the guardrail work a sandbox lets you write down as caveats and a production rollout does not. That deserves its own article rather than a paragraph, so consider this the honest state of play. I know the shape production takes, I know which parts of my proof of concept were a loan rather than a discount, and the walk from one to the other is something I will be exploring in a future article, with the actual promotion done rather than theorized. Stay tuned!&lt;/p&gt;

&lt;h2&gt;The bottom line&lt;/h2&gt;

&lt;p&gt;Microsoft&#39;s ALM prescription for Copilot Studio agents is sound, and I followed six of its seven golden rules without argument. But the guidance assumes a shape of project that a grounded agent frequently cannot have, the tooling stops one step short of the automation it openly advertises, and the step that guarantees your components actually travel is a click somebody has to remember before every single export.&lt;/p&gt;

&lt;p&gt;So my rule ended up simple enough to fit on one line: &lt;strong&gt;the repository defines the agent, and an environment is a place I can put it.&lt;/strong&gt; One environment today, because the data gave me no choice. Two on the way to production, because promotion demands it. And the reason the second one will cost an afternoon rather than a fortnight is the same discipline that made the first one survivable.&lt;/p&gt;

&lt;p&gt;If you have automated a Copilot Studio agent deployment end to end, especially if you have found a way to handle the add-required-objects step without a human in the loop, please drop a note in the comments describing how. That is the piece I would most like to stop doing by hand.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/8320439968606332011/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/8320439968606332011' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8320439968606332011'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8320439968606332011'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/copilot-studio-real-alm-for-agents-and.html' title='Copilot Studio | Real ALM for Agents (and the One Click That Breaks Your Pipeline)'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-3602864419858771009</id><published>2026-08-26T07:00:00.000-04:00</published><updated>2026-08-26T08:28:11.938-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ALM"/><category scheme="http://www.blogger.com/atom/ns#" term="Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="Dataverse"/><category scheme="http://www.blogger.com/atom/ns#" term="Web API"/><title type='text'>Copilot Studio | Adding Dataverse Knowledge Sources with the Web API (the &quot;UI-Only&quot; Rule That Isn&#39;t)</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Adding Copilot Studio Dataverse knowledge sources with the Web API&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-08-dataverse-knowledge.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;
&lt;p&gt;Not long ago, I set out to build a support agent in Microsoft Copilot Studio for a product in the Dynamics ecosystem: the kind of agent that answers customer questions grounded in &lt;em&gt;your&lt;/em&gt; content, meaning Dataverse knowledge articles plus a custom table of curated case resolutions. No web browsing, no model knowledge; the knowledge sources are the agent&#39;s entire world. And since I treat agents as code (YAML in source control, &lt;code&gt;pac copilot pack&lt;/code&gt;, solution import, repeatable deployments), I ran head-first into a wall you may have hit yourself:&lt;/p&gt;
&lt;!-- On publish: link &quot;treat agents as code&quot; to the git-vs-pac source control article --&gt;

&lt;p&gt;&lt;strong&gt;Dataverse knowledge sources can only be added through the Copilot Studio UI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is what the documentation implies, that is the line the community tooling holds, and &lt;code&gt;pac copilot pack&lt;/code&gt; enforces it: put a Dataverse source definition in your workspace&#39;s &lt;code&gt;knowledge/&lt;/code&gt; folder and pack rejects it outright. Public website sources? Fine in YAML. SharePoint? Fine. Dataverse tables, the one source type that lives &lt;em&gt;in the same platform as the agent itself&lt;/em&gt;, are clicks only.&lt;/p&gt;

&lt;p&gt;Except, as it turns out, that is not quite true. Copilot Studio is built &lt;em&gt;on&lt;/em&gt; Dataverse. Every knowledge source you click together in the maker portal materializes as plain Dataverse rows, and anything that is plain Dataverse rows can be created with the Web API: inside a solution, from a pipeline, repeatably. So, in this article I will walk through the recipe, battle-tested twice now: once in a production-grade Dynamics 365 CE org, and once in a freshly provisioned vanilla environment, where it surfaced several extra lessons you will want in hand before trying this yourself.&lt;/p&gt;

&lt;div style=&quot;background: rgba(245, 158, 11, 0.12); border-left: 4px solid #f59e0b; border-radius: 6px; padding: 0.9rem 1.2rem; margin: 1.6rem 0;&quot;&gt;&lt;strong&gt;&amp;#9888;&amp;#65039; NOT OFFICIALLY SUPPORTED:&lt;/strong&gt; none of this is documented or supported by Microsoft. These are the shapes Copilot Studio happens to write today, discovered by inspecting what the maker UI creates, and they can change in any release. Use this for dev and ALM automation with your eyes open, and always verify the result in the maker portal afterward.&lt;/div&gt;

&lt;h2&gt;First, which kind of agent? (This one matters)&lt;/h2&gt;

&lt;p&gt;Copilot Studio now ships &lt;strong&gt;two agent runtimes&lt;/strong&gt;, which Microsoft calls &lt;em&gt;harnesses&lt;/em&gt;. The &lt;strong&gt;standard harness&lt;/strong&gt; is the classic experience: topics, the full Knowledge pane, solution-first ALM. The &lt;strong&gt;GitHub Copilot harness&lt;/strong&gt; is the new streamlined agent builder, with its Build, Preview, and Evaluate tabs and a model picker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dataverse knowledge is a standard-harness feature.&lt;/strong&gt; The new experience does not offer Dataverse in its Add knowledge dialog at all, and here is the trap that cost me an evening: if you create your knowledge component via the API against a new-experience agent, &lt;strong&gt;the Knowledge panel will happily display it&lt;/strong&gt; (the panel reads the same component table) &lt;strong&gt;while the runtime silently never queries it&lt;/strong&gt;. The agent will cheerfully answer from its own general knowledge and tell you it has no documents. No error anywhere!&lt;/p&gt;

&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;The new-experience agent builder showing the API-created Curated Case Resolutions source listed in the Knowledge panel, displayed but never used&quot; height=&quot;573&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/images/post-08/fig1-new-experience-knowledge-panel.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1837&quot; /&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; font-size: 0.9em; margin: 0.4em 0 1.4em;&quot;&gt;&lt;em&gt;The trap in action: the new experience displays the API-created Dataverse source in its Knowledge panel...&lt;/em&gt;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;The new-experience agent answering a scanner question from general model knowledge, stating it has no device docs in the workspace&quot; height=&quot;893&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/images/post-08/fig2-new-experience-ungrounded.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1832&quot; /&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; font-size: 0.9em; margin: 0.4em 0 1.4em;&quot;&gt;&lt;em&gt;...but its runtime never queries it. Note the answer opens with &amp;quot;I don&amp;#39;t have any device docs or network data in this workspace&amp;quot; while the source sits right there in the panel.&lt;/em&gt;&lt;/div&gt;

&lt;p&gt;So check your agent&#39;s runtime before wiring anything, as follows:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /api/data/v9.2/bots?$select=name,schemaname,template&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A &lt;code&gt;template&lt;/code&gt; of &lt;code&gt;default-2.1.0&lt;/code&gt; (or a similar &lt;code&gt;default-*&lt;/code&gt;) means the standard harness; proceed. A &lt;code&gt;template&lt;/code&gt; of &lt;code&gt;cliagent-1.0.0&lt;/code&gt; means the new experience, and this recipe will not ground there. To build standard-harness agents, flip the &lt;strong&gt;&quot;New experience&quot; toggle off&lt;/strong&gt; on the Copilot Studio homepage, or pick &lt;strong&gt;&quot;Other ways to build.&quot;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; a fun fact discovered along the way. In the new experience, the default &quot;Search all websites&quot; knowledge chip is not a knowledge source row at all; it is an &lt;code&gt;enableWebSearch&lt;/code&gt; flag inside the bot&#39;s configuration JSON. Different harness, different bookkeeping.&lt;/p&gt;

&lt;h2&gt;Why bother?&lt;/h2&gt;

&lt;p&gt;If you are happy clicking the UI once, close this tab with my blessing. But if any of the following apply, read on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your dev environment is a &lt;strong&gt;sandbox that gets refreshed&lt;/strong&gt; from production periodically, wiping everything you built. Mine was! With the agent rebuildable from a git repository, knowledge sources included, a refresh becomes a non-event instead of a lost week.&lt;/li&gt;
&lt;li&gt;You deploy the &lt;strong&gt;same agent to multiple environments&lt;/strong&gt; and want identical knowledge wiring in each without a click-checklist taped to the monitor.&lt;/li&gt;
&lt;li&gt;You want knowledge sources &lt;strong&gt;inside your solution&lt;/strong&gt;, versioned and transported like everything else you ship.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Finding the shapes: mirror a working example&lt;/h2&gt;

&lt;p&gt;The trick that unlocked everything is almost embarrassingly simple: find a working reference implementation &lt;em&gt;in the same environment&lt;/em&gt; and mirror its record shapes. If your org runs Dynamics 365 Customer Service, Microsoft&#39;s own first-party copilots ship as solution components, and their Dataverse knowledge sources are sitting right there in the very same tables yours will use; that is what I mirrored. In a clean environment with no first-party copilots, make your own reference: click one knowledge source together in the UI, then query what it wrote. Either way, the shapes reveal themselves:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /api/data/v9.2/botcomponents?$filter=componenttype eq 16
  &amp;amp;$select=name,data,schemaname&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Four moving parts emerge from that inspection:&lt;/p&gt;

&lt;table&gt;
&lt;tr&gt;&lt;th&gt;Piece&lt;/th&gt;&lt;th&gt;What it is&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;dvtablesearch&lt;/code&gt;&lt;/td&gt;&lt;td&gt;A named &quot;table search&quot; configuration: the knowledge source&#39;s identity&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;dvtablesearchentity&lt;/code&gt;&lt;/td&gt;&lt;td&gt;A child row per table being searched, pointing at its parent via the &lt;code&gt;DVTableSearch&lt;/code&gt; navigation property and naming the table in &lt;code&gt;entitylogicalname&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;botcomponent&lt;/code&gt; (componenttype 16)&lt;/td&gt;&lt;td&gt;The agent-side knowledge component; its &lt;code&gt;data&lt;/code&gt; column holds KnowledgeSourceConfiguration YAML whose &lt;code&gt;skillConfiguration&lt;/code&gt; names the &lt;code&gt;dvtablesearch&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;botcomponent_dvtablesearch&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The N:N association tying the component to the search config&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;A few things to note before we build. First, the component attaches to your agent through its &lt;strong&gt;&lt;code&gt;parentbotid&lt;/code&gt; lookup&lt;/strong&gt;, and not, as you might reasonably guess, through the &lt;code&gt;bot_botcomponent&lt;/code&gt; N:N relationship. That relationship exists in the schema, but for knowledge components it stays empty; the parent lookup is what the platform actually uses. Ask me how long that one took! Second, on naming, because it bit me: &lt;code&gt;dvtablesearch.name&lt;/code&gt; is a &lt;strong&gt;machine-style identifier&lt;/strong&gt; (the component YAML references it verbatim), while the friendly display name your makers see in the Knowledge tab lives on the &lt;code&gt;botcomponent.name&lt;/code&gt;. Keep them distinct on purpose.&lt;/p&gt;

&lt;h2&gt;Step 0: the custom table (if you are grounding on one)&lt;/h2&gt;

&lt;p&gt;My scenario grounds the agent on a custom curated-resolutions table. If you only need standard tables like &lt;code&gt;knowledgearticle&lt;/code&gt; (which, by the way, is a base Dataverse table, present even in environments without any Dynamics 365 apps), feel free to skip ahead to Step 1. Otherwise, three gotchas await, and the third one is the difference between an agent that grounds and one that shrugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 1: Memo columns want an explicit format.&lt;/strong&gt; Declare &lt;code&gt;Format: TextArea&lt;/code&gt; on each &lt;code&gt;MemoAttributeMetadata&lt;/code&gt;. That is plain multi-line text, which is what the knowledge indexer expects, and a working column reads back with both &lt;code&gt;Format&lt;/code&gt; and &lt;code&gt;FormatName&lt;/code&gt; as &lt;code&gt;TextArea&lt;/code&gt;. Here is the whole table, one create:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;POST /api/data/v9.2/EntityDefinitions
MSCRM.SolutionUniqueName: WorkbenchSupportAssistant

{
  &quot;@odata.type&quot;: &quot;Microsoft.Dynamics.CRM.EntityMetadata&quot;,
  &quot;SchemaName&quot;: &quot;wrk_caseresolution&quot;,
  &quot;DisplayName&quot;: { &quot;LocalizedLabels&quot;: [ { &quot;Label&quot;: &quot;Case Resolution&quot;, &quot;LanguageCode&quot;: 1033 } ] },
  &quot;DisplayCollectionName&quot;: { &quot;LocalizedLabels&quot;: [ { &quot;Label&quot;: &quot;Case Resolutions&quot;, &quot;LanguageCode&quot;: 1033 } ] },
  &quot;OwnershipType&quot;: &quot;UserOwned&quot;,
  &quot;HasActivities&quot;: false,
  &quot;HasNotes&quot;: false,
  &quot;SyncToExternalSearchIndex&quot;: true,
  &quot;Attributes&quot;: [
    { &quot;@odata.type&quot;: &quot;Microsoft.Dynamics.CRM.StringAttributeMetadata&quot;,
      &quot;SchemaName&quot;: &quot;wrk_title&quot;, &quot;IsPrimaryName&quot;: true, &quot;MaxLength&quot;: 300,
      &quot;RequiredLevel&quot;: { &quot;Value&quot;: &quot;ApplicationRequired&quot; },
      &quot;DisplayName&quot;: { &quot;LocalizedLabels&quot;: [ { &quot;Label&quot;: &quot;Title&quot;, &quot;LanguageCode&quot;: 1033 } ] } },
    { &quot;@odata.type&quot;: &quot;Microsoft.Dynamics.CRM.MemoAttributeMetadata&quot;,
      &quot;SchemaName&quot;: &quot;wrk_symptom&quot;, &quot;Format&quot;: &quot;TextArea&quot;, &quot;MaxLength&quot;: 4000,
      &quot;DisplayName&quot;: { &quot;LocalizedLabels&quot;: [ { &quot;Label&quot;: &quot;Symptom&quot;, &quot;LanguageCode&quot;: 1033 } ] } },
    { &quot;@odata.type&quot;: &quot;Microsoft.Dynamics.CRM.MemoAttributeMetadata&quot;,
      &quot;SchemaName&quot;: &quot;wrk_resolution&quot;, &quot;Format&quot;: &quot;TextArea&quot;, &quot;MaxLength&quot;: 10000,
      &quot;DisplayName&quot;: { &quot;LocalizedLabels&quot;: [ { &quot;Label&quot;: &quot;Resolution&quot;, &quot;LanguageCode&quot;: 1033 } ] } }
  ]
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One POST, one 204, done! And notice &lt;code&gt;SyncToExternalSearchIndex: true&lt;/code&gt; riding along in the create payload. Which brings us to...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 2: Dataverse search must be on, and the org-level flag is just a column.&lt;/strong&gt; Copilot Studio&#39;s Dataverse knowledge rides Dataverse search. The admin-center setting (now &lt;em&gt;two&lt;/em&gt; checkboxes in the modern admin UI, one for &quot;AI and agent experiences&quot; and one for the global search bar) ultimately reflects a boolean on the organization record, and yes, you can flip it via API:&lt;/p&gt;

&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;The Power Platform admin center Dataverse search settings showing the two indexing checkboxes plus the Search behavior options&quot; height=&quot;718&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/images/post-08/fig3-admin-dataverse-search.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;788&quot; /&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; font-size: 0.9em; margin: 0.4em 0 1.4em;&quot;&gt;&lt;em&gt;The modern admin center splits search indexing into two scopes; the first checkbox (&amp;quot;AI and agent experiences&amp;quot;) is the one Copilot Studio knowledge rides on.&lt;/em&gt;&lt;/div&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;PATCH /api/data/v9.2/organizations(&amp;lt;orgid&amp;gt;)
{ &quot;isexternalsearchindexenabled&quot;: true }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But budget real time for what happens next. On a freshly provisioned environment, &lt;strong&gt;initial index provisioning took about two hours&lt;/strong&gt; before my rows became searchable. And do not trust &lt;code&gt;GET /api/search/v1.0/status&lt;/code&gt; to tell you how it is going; it reported &quot;0 tables indexed&quot; &lt;em&gt;even while queries were returning hits&lt;/em&gt;. The only reliable readiness probe is an actual query:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;POST /api/search/v1.0/query
{ &quot;search&quot;: &quot;scanner&quot;, &quot;entities&quot;: [&quot;wrk_caseresolution&quot;], &quot;top&quot;: 5 }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For a table that already exists and needs the sync flag flipped, resist the urge to PATCH the entity definition, because &lt;strong&gt;metadata entities cannot be updated with PATCH&lt;/strong&gt;. Updates replace the whole definition: GET it, modify, PUT the &lt;em&gt;entire&lt;/em&gt; document back with an &lt;code&gt;MSCRM.MergeLabels: true&lt;/code&gt; header (or kiss your localized labels goodbye, since omitted properties reset to their defaults), then call the &lt;code&gt;PublishXml&lt;/code&gt; action. You may be asking whether all that ceremony is really necessary for one boolean. It is! Which is exactly why you set it at create time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha 3: find columns ARE the search index.&lt;/strong&gt; This is the one that separates &quot;source attached&quot; from &quot;agent grounded.&quot; Dataverse search indexes the columns configured as &lt;strong&gt;find columns on the table&#39;s Quick Find view&lt;/strong&gt;, and a fresh custom table&#39;s Quick Find view has exactly one: the primary name. Your beautiful Symptom and Resolution text? Invisible to search, and therefore to your agent, until they are added.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; and here is the kicker. Updating the view&#39;s &lt;code&gt;fetchxml&lt;/code&gt; via the Web API fails with an opaque &lt;code&gt;0x80040216&lt;/code&gt; on every route I tried (record PATCH, single-property PUT), &lt;strong&gt;in two different orgs&lt;/strong&gt;, while &lt;code&gt;layoutxml&lt;/code&gt; on the very same record updates fine. Whatever message the maker UI sends, the Web API&#39;s &lt;code&gt;savedquery&lt;/code&gt; update path is not it. Budget one manual click-step: go to Power Apps, then &lt;strong&gt;Tables&lt;/strong&gt;, then your table, then &lt;strong&gt;Views&lt;/strong&gt;, open the Quick Find view, click &lt;strong&gt;Edit find table columns&lt;/strong&gt;, add your text columns, and &lt;strong&gt;Save and publish&lt;/strong&gt;. That is the fastest route, and it is the one I used here. Now, it is not the only route: the same change can be made at the solution level, by exporting the solution, editing the view definition inside it, and importing it back, which automates cleanly. That belongs to a bigger story about treating agents as source-controlled artifacts, and it is where I am headed next!&lt;/p&gt;

&lt;p&gt;Seed a few rows while you are here. An empty table grounds nothing!&lt;/p&gt;

&lt;h2&gt;Step 1: create the &lt;code&gt;dvtablesearch&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;First surprise: the entity-set name is &lt;code&gt;dvtablesearchs&lt;/code&gt;. Yes, really, that plural! Ask &lt;code&gt;$metadata&lt;/code&gt; if you do not believe me; I did, twice:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;POST /api/data/v9.2/dvtablesearchs
MSCRM.SolutionUniqueName: WorkbenchSupportAssistant

{ &quot;name&quot;: &quot;wrk_case_resolutions_search&quot; }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That single property is genuinely the whole payload; verified. Machine-style name, remember: this exact string gets referenced by the component YAML in Step 3. And keep an eye on that &lt;code&gt;MSCRM.SolutionUniqueName&lt;/code&gt; header, because it is doing quiet but important work in every call in this article. It lands each row in &lt;strong&gt;your solution&lt;/strong&gt; instead of the default one, which is the whole ALM point of the exercise.&lt;/p&gt;

&lt;h2&gt;Step 2: add the table(s) to it&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;dvtablesearchentity&lt;/code&gt; per table the source searches, bound to its parent via the single-valued navigation property, as follows:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;POST /api/data/v9.2/dvtablesearchentities
MSCRM.SolutionUniqueName: WorkbenchSupportAssistant

{
  &quot;entitylogicalname&quot;: &quot;wrk_caseresolution&quot;,
  &quot;DVTableSearch@odata.bind&quot;: &quot;/dvtablesearchs(&amp;lt;dvtablesearch-guid&amp;gt;)&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Step 3: the &lt;code&gt;botcomponent&lt;/code&gt; that makes it a knowledge source&lt;/h2&gt;

&lt;p&gt;This is the piece that surprises people: the knowledge source your agent sees is a &lt;code&gt;botcomponent&lt;/code&gt; row of componenttype 16 whose &lt;code&gt;data&lt;/code&gt; column is... YAML. A &lt;code&gt;KnowledgeSourceConfiguration&lt;/code&gt; document whose &lt;code&gt;skillConfiguration&lt;/code&gt; property names the &lt;code&gt;dvtablesearch&lt;/code&gt; from Step 1, and whose &lt;code&gt;parentbotid&lt;/code&gt; lookup attaches it to your agent, all in one create:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;POST /api/data/v9.2/botcomponents
MSCRM.SolutionUniqueName: WorkbenchSupportAssistant

{
  &quot;name&quot;: &quot;Curated Case Resolutions&quot;,
  &quot;componenttype&quot;: 16,
  &quot;schemaname&quot;: &quot;wrk_WorkbenchSupportAssistant20.knowledge.wrk_case_resolutions&quot;,
  &quot;data&quot;: &quot;kind: KnowledgeSourceConfiguration\nsource:\n  kind: DataverseStructuredSearchSource\n  skillConfiguration: wrk_case_resolutions_search\n&quot;,
  &quot;parentbotid@odata.bind&quot;: &quot;/bots(&amp;lt;bot-guid&amp;gt;)&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That &lt;code&gt;data&lt;/code&gt; YAML, unfolded so you can actually read it:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;kind: KnowledgeSourceConfiguration
source:
  kind: DataverseStructuredSearchSource
  skillConfiguration: wrk_case_resolutions_search&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note here. The &lt;code&gt;schemaname&lt;/code&gt; follows the convention &lt;code&gt;&amp;lt;botSchemaName&amp;gt;.knowledge.&amp;lt;componentName&amp;gt;&lt;/code&gt;, and that is the &lt;strong&gt;bot&#39;s&lt;/strong&gt; schemaname, which you should read rather than guess: the same &lt;code&gt;GET /bots&lt;/code&gt; from the harness check gives it to you, and agents created in the new experience even carry a random suffix in theirs. And the friendly &lt;code&gt;name&lt;/code&gt; is what your makers will see in the Knowledge tab, so make it read like a knowledge source, not like a database object.&lt;/p&gt;

&lt;h2&gt;Step 4: tie the component to the search config&lt;/h2&gt;

&lt;p&gt;The N:N association that closes the loop:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;POST /api/data/v9.2/botcomponents(&amp;lt;component-guid&amp;gt;)/botcomponent_dvtablesearch/$ref

{ &quot;@odata.id&quot;: &quot;https://yourorg.crm.dynamics.com/api/data/v9.2/dvtablesearchs(&amp;lt;dvtablesearch-guid&amp;gt;)&quot; }&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Verify like the maker UI is watching&lt;/h2&gt;

&lt;p&gt;Open the agent in Copilot Studio and go to the &lt;strong&gt;Knowledge&lt;/strong&gt; page. Your source should be listed exactly as if you had clicked it together: name, table, status. Then comes the real test, and no publish is needed. Go straight into the test canvas and ask a question only your table can answer.&lt;/p&gt;

&lt;p&gt;When it works, it &lt;em&gt;really&lt;/em&gt; works! My test agent answered a scanner-connectivity question with the exact resolution from the seeded row, three citations pointing at &lt;code&gt;wrk_caseresolution&lt;/code&gt; records, and the canvas&#39;s activity map showing the knowledge searches with &quot;Curated Case Resolutions&quot; as the referenced source. Grounded, cited, and not one of those records touched by a mouse.&lt;/p&gt;

&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;The standard-harness test canvas showing the activity map with completed knowledge searches, the Referenced sources panel quoting the Curated Case Resolutions row, and the grounded answer with three wrk_caseresolution citations&quot; height=&quot;1069&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/images/post-08/fig4-grounded-answer.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1846&quot; /&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; font-size: 0.9em; margin: 0.4em 0 1.4em;&quot;&gt;&lt;em&gt;The payoff: the activity map traces three knowledge searches, the inspection panel quotes the seeded resolution verbatim, and the answer carries three citations into wrk_caseresolution; every one of those records created by the Web API.&lt;/em&gt;&lt;/div&gt;

&lt;p&gt;Two behavioral notes for Dataverse sources generally, both documented but easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Same environment only.&lt;/strong&gt; Copilot Studio&#39;s Dataverse knowledge reads the agent&#39;s own environment. If your data lives in environment A, the agent lives in environment A. Plan placement before you scaffold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-user authentication must be &quot;Authenticate with Microsoft.&quot;&lt;/strong&gt; Dataverse sources ride the end user&#39;s identity; &quot;No authentication&quot; and &quot;Authenticate manually&quot; agents will not search them, and the docs are explicit about this one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;The bigger picture&lt;/h2&gt;

&lt;p&gt;Everything Copilot Studio&#39;s maker portal does lands in Dataverse tables, and a surprising amount of the &quot;UI-only&quot; surface is only UI-only until you look at what the UI writes. The same technique has bailed me out more than once: find where the platform stores it, mirror the shape, create it in your solution. In turn, it converts &quot;click this checklist in every environment&quot; into &quot;run the deployment,&quot; and that is the difference between an ALM story with an asterisk and one without.&lt;/p&gt;

&lt;p&gt;One more time, because it matters: these are undocumented shapes, subject to change, so verify in the portal after every deployment. But with that caveat honored, a source-controlled agent is rebuildable end to end, knowledge sources included, and that is always a good thing.&lt;/p&gt;

&lt;p&gt;If you have automated your own way around a &quot;UI-only&quot; corner of Copilot Studio, or if these shapes have already drifted by the time you read this, please drop a note in the comments describing what you found; undocumented territory is exactly where comparing notes pays the most.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/3602864419858771009/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/3602864419858771009' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/3602864419858771009'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/3602864419858771009'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/copilot-studio-adding-dataverse.html' title='Copilot Studio | Adding Dataverse Knowledge Sources with the Web API (the &quot;UI-Only&quot; Rule That Isn&#39;t)'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-5531898008819768436</id><published>2026-08-24T07:00:00.000-04:00</published><updated>2026-08-24T10:55:37.069-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Application Insights"/><category scheme="http://www.blogger.com/atom/ns#" term="Azure"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><category scheme="http://www.blogger.com/atom/ns#" term="Telemetry"/><title type='text'>Power Automate | Cloud Flow Telemetry with Azure Application Insights, Down to the Action Level</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Power Automate cloud flow telemetry in Azure Application Insights, down to the action level&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-06-appinsights.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;
&lt;p&gt;Longtime readers may remember that the very last article I published before this blog went quiet, back in August of 2021, covered monitoring &lt;a href=&quot;https://www.theworkbench.blog/2021/08/power-apps-application-monitoring-with.html&quot; target=&quot;_blank&quot;&gt;Power Apps with Azure Application Insights&lt;/a&gt;. So it is only fitting that I return to the subject from the other side of the platform: getting cloud flow telemetry out of Power Automate and into Application Insights, down to the action level.&lt;/p&gt;

&lt;p&gt;I will be honest about why I am writing this one. When I went looking for how this integration works, the picture I pieced together from searching around was incomplete, and once I configured it and read the actual tables, I understood why: the feature is admin-gated where makers cannot see it, and the telemetry schema is not what instinct suggests. I made the wrong assumption myself before the data corrected me! So, in this article I will walk through how the integration actually works, the schema surprise that changes how you query it, and the &lt;a href=&quot;https://learn.microsoft.com/en-us/kusto/?view=microsoft-fabric&quot; target=&quot;_blank&quot;&gt;Kusto queries&lt;/a&gt; that turn &quot;the flow failed&quot; into &quot;the flow failed at this action, with this error.&quot;&lt;/p&gt;

&lt;h2&gt;First things first: yes, the integration exists&lt;/h2&gt;

&lt;p&gt;Power Automate cloud flow telemetry can be exported to Azure Application Insights, and keep in mind, this is not a maker feature -- which is the source of a lot of the confusion. Canvas apps let a maker paste an instrumentation key into the app and start calling &lt;i&gt;&lt;b&gt;Trace()&lt;/b&gt;&lt;/i&gt;. Cloud flows offer nothing of the sort, and for a sound reason: flows have no client runtime to instrument. They execute server-side on shared infrastructure, so the integration was built as a platform-level export instead.&lt;/p&gt;

&lt;p&gt;The setup lives in the &lt;strong&gt;Power Platform admin center&lt;/strong&gt;, under &lt;strong&gt;Data export&lt;/strong&gt;. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEii9WQxdgAykdSYgUhfp2ZM_5SH0AR4ddRhmq3AdVEnuxRxRlbp9oJVxUvKTVM1T4JTtfV_G2Zz6hjs0Xvki3N9MBzXBRDhWO0hF-h6UHy2mwad_USZ4lOHy5GxSmjzdEnAr6EpxjAs44_gt7Q_GcU-hBpmnLZ1qiAr5K4c6AG8UQJxu4UVqnKaaHawNPaE&quot;&gt;&lt;img style=&quot;width: 100%; height: auto;&quot; alt=&quot;&quot; data-original-height=&quot;818&quot; data-original-width=&quot;1622&quot; height=&quot;818&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEii9WQxdgAykdSYgUhfp2ZM_5SH0AR4ddRhmq3AdVEnuxRxRlbp9oJVxUvKTVM1T4JTtfV_G2Zz6hjs0Xvki3N9MBzXBRDhWO0hF-h6UHy2mwad_USZ4lOHy5GxSmjzdEnAr6EpxjAs44_gt7Q_GcU-hBpmnLZ1qiAr5K4c6AG8UQJxu4UVqnKaaHawNPaE=s1600&quot; width=&quot;1622&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; font-size: 0.9em; margin: 0.4em 0 1.4em;&quot;&gt;&lt;em&gt;Admin Center &amp;gt; Manage &amp;gt; Data Export&lt;/em&gt;&lt;/div&gt;&lt;br /&gt;An administrator creates an export package, selects Power Automate as the data type, chooses the environment, and points it at an Application Insights resource. Three requirements to check before you start, because each one can stop you cold:&lt;p&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The environment must be a Managed Environment.&lt;/strong&gt; The feature is gated behind this governance tier, which is exactly why a maker who goes looking for it at the flow level concludes it does not exist. It does; it just lives upstairs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need admin rights, not maker rights.&lt;/strong&gt; Power Platform administrator or Dynamics 365 administrator at the tenant level, plus environment or system administrator in the Dataverse environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need Contributor rights or better on the Application Insights resource&lt;/strong&gt; itself, over on the Azure side.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; once configured, expect up to 24 hours before telemetry starts appearing. Initial patience is required; ongoing latency is much lower. Do not spend that first afternoon rewriting your export package because the tables look empty!&lt;/p&gt;

&lt;h2&gt;The schema surprise: two tables, not one&lt;/h2&gt;

&lt;p&gt;Here is the assumption I walked in with: every action a flow executes, including the trigger, lands as an entry in the Application Insights &lt;code&gt;requests&lt;/code&gt; table. It seems reasonable. It is also not how the schema works, and the difference matters more than any other detail in this article. As it turns out, the export splits telemetry across two tables:&lt;/p&gt;

&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Table&lt;/th&gt;&lt;th&gt;Contains&lt;/th&gt;&lt;th&gt;signalCategory value&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;requests&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Cloud flow runs&lt;/td&gt;&lt;td&gt;&quot;Cloud flow runs&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;dependencies&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Cloud flow triggers and actions&lt;/td&gt;&lt;td&gt;&quot;Cloud flow triggers&quot; / &quot;Cloud flow actions&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;In turn, if your monitoring queries only touch &lt;code&gt;requests&lt;/code&gt;, you are monitoring at the run level. You will know a flow failed. You will not know which action failed, how long each action took, or whether a specific connector call is degrading over time. All of that lives in &lt;code&gt;dependencies&lt;/code&gt;, waiting to be queried.&lt;/p&gt;

&lt;p&gt;There is a second, related detail that took me a moment to appreciate. When you configure the export package, you explicitly choose whether to export cloud flow runs, triggers, or actions. These are separate checkboxes! If you only selected runs during setup, action telemetry never leaves the platform, and no Kusto query will conjure it afterward. Granularity is decided at configuration time, not query time.&lt;/p&gt;

&lt;h2&gt;Run-level monitoring: the baseline&lt;/h2&gt;

&lt;p&gt;Run-level failure alerting is the right starting point, and the query looks as follows:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;requests
| where customDimensions.signalCategory == &quot;Cloud flow runs&quot;
| where success == false
| extend flowId = tostring(customDimensions[&quot;resourceId&quot;]),
         environmentId = tostring(customDimensions[&quot;environmentId&quot;])
| project timestamp, name, resultCode, duration, flowId, environmentId, operation_Id&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note here. Filtering on &lt;code&gt;signalCategory&lt;/code&gt; matters because your Application Insights instance may be collecting telemetry from many sources, and this keeps the flow signals separated from everything else. Filtering on &lt;code&gt;environmentId&lt;/code&gt; matters when multiple environments export to the same resource. Wire this query to an Azure Monitor alert rule, and you have real-time failure notifications by email, SMS, or webhook.&lt;/p&gt;

&lt;h2&gt;Action-level monitoring: where the real value is&lt;/h2&gt;

&lt;p&gt;Now, with the actions checkbox enabled in your export package, the &lt;code&gt;dependencies&lt;/code&gt; table gives you per-action telemetry:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;dependencies
| where customDimensions.signalCategory == &quot;Cloud flow actions&quot;
| where success == false
| extend flowId = tostring(customDimensions[&quot;resourceId&quot;])
| project timestamp, name, resultCode, duration, flowId, operation_Id&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;name&lt;/code&gt; field carries the action name exactly as it appears in the flow designer. This is the query that turns &quot;the invoice approval flow failed&quot; into &quot;the invoice approval flow failed at the vendor validation HTTP call with a 429.&quot;&lt;/p&gt;

&lt;p&gt;Correlation is what makes the two tables work together. The &lt;code&gt;operation_Id&lt;/code&gt; on an action&#39;s dependency row matches the &lt;code&gt;operation_Id&lt;/code&gt; on the parent run&#39;s request row, so you can join them:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;requests
| where customDimensions.signalCategory == &quot;Cloud flow runs&quot;
| where success == false
| join kind=inner (
    dependencies
    | where customDimensions.signalCategory == &quot;Cloud flow actions&quot;
    | where success == false
  ) on operation_Id
| project runTime = timestamp, flowRun = name,
          failedAction = name1, actionResult = resultCode1, actionDuration = duration1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What this join produces is a failure report at the granularity your support team actually needs: which run, which action, what error, and how long it ran before dying. The &lt;code&gt;name1&lt;/code&gt;, &lt;code&gt;resultCode1&lt;/code&gt;, and &lt;code&gt;duration1&lt;/code&gt; columns are Kusto&#39;s automatic renames for the joined table&#39;s fields; the projection gives them names a human can read.&lt;/p&gt;

&lt;p&gt;Beyond failures, the same table supports performance work. Percentile queries over &lt;code&gt;duration&lt;/code&gt; by action &lt;code&gt;name&lt;/code&gt; will surface connector calls that are slowly degrading long before they start timing out, which is exactly the kind of early signal run-level monitoring can never provide.&lt;/p&gt;

&lt;h2&gt;Three caveats before you turn everything on&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NOTE 1:&lt;/strong&gt; ingestion cost scales with granularity. Application Insights bills per GB ingested, and action-level export on a busy flow with Apply to each loops generates a row per action, per iteration, per run. A flow processing a few thousand records nightly can produce telemetry volume that surprises you at invoice time. Scope the export deliberately: enable action-level export for the flows that justify it rather than blanketing the environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE 2:&lt;/strong&gt; validate scope nesting behavior empirically. If your flows use the Try/Catch pattern built on Scope containers, test how nested actions surface before you write production alert queries. Build a flow with a deliberately failing action inside a scope, run it, and inspect what lands in &lt;code&gt;dependencies&lt;/code&gt;. Ten minutes with a test flow will teach you more than any amount of reading, and your failure queries should be written against observed behavior rather than assumptions. I know so as I&#39;ve done so myself!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE 3:&lt;/strong&gt; treat the export as telemetry, not as a system of record. Microsoft is explicit that small data losses can occur due to transient service issues. The flow run history inside the Power Automate portal remains the transactional, authoritative record. Application Insights is for alerting, dashboards, and trend analysis, not for audit-grade completeness.&lt;/p&gt;

&lt;h2&gt;What about custom telemetry from inside a flow?&lt;/h2&gt;

&lt;p&gt;The export covers runs, triggers, and actions, but you may be asking, &quot;where is the Trace() function for flows?&quot; Very simple: there is no native equivalent. If you need business-level events -- &quot;vendor matched,&quot; &quot;payment batch released&quot; -- the pattern is an HTTP action posting directly to the Application Insights ingestion endpoint at &lt;code&gt;https://dc.services.visualstudio.com/v2/track&lt;/code&gt;, or preferably the regional endpoint from your resource&#39;s connection string. No authentication header is required; the instrumentation key inside the payload identifies the target resource:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;Microsoft.ApplicationInsights.Event&quot;,
  &quot;time&quot;: &quot;@{utcNow()}&quot;,
  &quot;iKey&quot;: &quot;&amp;lt;your-instrumentation-key&amp;gt;&quot;,
  &quot;data&quot;: {
    &quot;baseType&quot;: &quot;EventData&quot;,
    &quot;baseData&quot;: {
      &quot;name&quot;: &quot;InvoiceApprovalFlow.Step3.VendorMatched&quot;,
      &quot;properties&quot;: {
        &quot;flowRunId&quot;: &quot;@{workflow()[&#39;run&#39;][&#39;name&#39;]}&quot;,
        &quot;flowName&quot;: &quot;@{workflow()[&#39;name&#39;]}&quot;,
        &quot;environment&quot;: &quot;@{workflow()[&#39;tags&#39;][&#39;environmentName&#39;]}&quot;,
        &quot;vendorId&quot;: &quot;@{variables(&#39;vendorId&#39;)}&quot;
      },
      &quot;measurements&quot;: {
        &quot;recordsProcessed&quot;: 42,
        &quot;durationMs&quot;: 1830
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note about this payload. Events posted this way land in the &lt;code&gt;customEvents&lt;/code&gt; table. Swap &lt;code&gt;baseType&lt;/code&gt; to &lt;code&gt;TraceData&lt;/code&gt; with a &lt;code&gt;message&lt;/code&gt; and &lt;code&gt;severityLevel&lt;/code&gt; for trace-style logging, or to &lt;code&gt;ExceptionData&lt;/code&gt; for structured errors. And carrying the flow run ID in the properties gives you a correlation key back to the platform-exported tables, so your business events join cleanly against the runs and actions they belong to.&lt;/p&gt;

&lt;h3&gt;Centralize it in a child flow&lt;/h3&gt;

&lt;p&gt;Do not scatter this HTTP action across every flow you own! Build it once as a child flow -- call it something like &quot;LogTelemetry&quot; -- and have every parent flow call it as a single action. The child flow takes three inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event name&lt;/strong&gt; (text): the dotted event identifier, such as &lt;code&gt;InvoiceApprovalFlow.Step3.VendorMatched&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Properties&lt;/strong&gt; (text): a JSON string of custom dimensions, parsed and merged into the payload inside the child&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Severity&lt;/strong&gt; (text or number): drives whether the child posts EventData, TraceData, or ExceptionData&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In turn, the child flow owns the instrumentation key, the endpoint URL, and the payload envelope. When the ingestion endpoint changes, when you rotate the key, or when you decide to enrich every event with the environment name, you change one flow instead of forty. Parent flows stay clean: one &quot;Run a child flow&quot; action per telemetry point, and no HTTP plumbing in sight.&lt;/p&gt;

&lt;p&gt;The same child flow pairs naturally with Scope-based error handling. Wrap your business logic in a Try scope, follow it with a Catch scope configured to run after &quot;has failed&quot; and &quot;has timed out,&quot; and inside the Catch use &lt;code&gt;result(&#39;Try&#39;)&lt;/code&gt; to extract the failed action details and pass them to LogTelemetry as an ExceptionData payload. That gives you structured failure telemetry carrying the actual action error, not just a run that shows red in the portal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; two constraints to know before you commit to this pattern. The HTTP connector requires premium licensing, and the raw post does not work if your Application Insights resource enforces Entra ID-only ingestion. In that case, a small Azure Function relay running the real SDK is the better shape, since a flow cannot easily perform the token acquisition against Azure Monitor scopes.&lt;/p&gt;

&lt;p&gt;And to save you the scaffolding: I built LogTelemetry exactly as described and published it as an importable solution on GitHub, over at &lt;a href=&quot;https://github.com/dgpblogster/power-automate-logtelemetry&quot;&gt;github.com/dgpblogster/power-automate-logtelemetry&lt;/a&gt;. It uses no connectors that require authentication, so the import prompts for nothing; you replace two clearly-marked CONFIG values, turn it on, and start logging. The README walks the import click by click.&lt;/p&gt;

&lt;h2&gt;The bottom line&lt;/h2&gt;

&lt;p&gt;Power Automate telemetry in Application Insights is real, admin-configured, and gated behind Managed Environments. Runs live in &lt;code&gt;requests&lt;/code&gt;; triggers and actions live in &lt;code&gt;dependencies&lt;/code&gt;. If your monitoring stops at the &lt;code&gt;requests&lt;/code&gt; table, you have built a smoke detector that tells you the building is on fire without telling you which room. The &lt;code&gt;dependencies&lt;/code&gt; table, an export package with the actions checkbox enabled, and one join on &lt;code&gt;operation_Id&lt;/code&gt; are all it takes to do better.&lt;/p&gt;

&lt;p&gt;If you have wired up this export yourself, or found other signals hiding in these tables worth querying, please drop a note in the comments describing your experience; telemetry patterns get better every time somebody shares what they found in the data.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/5531898008819768436/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/5531898008819768436' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5531898008819768436'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/5531898008819768436'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/power-automate-cloud-flow-telemetry.html' title='Power Automate | Cloud Flow Telemetry with Azure Application Insights, Down to the Action Level'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEii9WQxdgAykdSYgUhfp2ZM_5SH0AR4ddRhmq3AdVEnuxRxRlbp9oJVxUvKTVM1T4JTtfV_G2Zz6hjs0Xvki3N9MBzXBRDhWO0hF-h6UHy2mwad_USZ4lOHy5GxSmjzdEnAr6EpxjAs44_gt7Q_GcU-hBpmnLZ1qiAr5K4c6AG8UQJxu4UVqnKaaHawNPaE=s72-c" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-8241138433724031971</id><published>2026-08-21T07:00:00.000-04:00</published><updated>2026-08-21T07:00:00.121-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Business Central"/><category scheme="http://www.blogger.com/atom/ns#" term="Integration"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Platform"/><title type='text'>Power Automate | Design Every Flow So a Resubmit Is Always Safe</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Design every Power Automate flow so a resubmit is always safe&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-05-resubmit.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;
&lt;p&gt;If there is one question I have learned to ask of every single action in every Power Automate flow I build, it is this one: &lt;b&gt;what happens if this runs twice?&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;Throughout our Business Central to Dynamics 365 CE billing integration, the same project from my previous articles in this series, we made the answer to that question a design requirement. Every flow had to produce the same correct end state whether an event arrived once, twice, or five times, and whether the runs came from the webhook, a retry, or a human clicking resubmit weeks later. So, in this article I will walk through why that requirement pays for itself many times over, the patterns that satisfy it, and the two match-key mistakes we made on the way to getting it right. Yes, it took us three attempts!&lt;/p&gt;
&lt;!--On publish: link &quot;my previous articles&quot; to the FlowAgent post--&gt;

&lt;h2&gt;Why events arrive more than once&lt;/h2&gt;

&lt;p&gt;Keep in mind, if your integration is triggered by webhooks, replays are not an edge case. They are the operating environment!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The source system fires on modification, not creation.&lt;/strong&gt; Our flows trigger on Business Central&#39;s customer ledger, where a single business event can touch several entries, and each touch fires the trigger. Guards filter the noise, but the same document can legitimately come through the front door repeatedly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repairs happen by resubmission.&lt;/strong&gt; When a run fails -- a permissions gap, a defect, an outage -- the natural fix is: repair the cause, resubmit the stored run. The trigger payload replays against the flow as it exists now.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing happens by resubmission.&lt;/strong&gt; The fastest way to verify a flow change against real data is to resubmit a known run and inspect the result. If reprocessing old events corrupts data, you have lost your best testing tool.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Late events reprocess old ground.&lt;/strong&gt; A payment applied to an invoice months after posting triggers a reconcile over records your flow already created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, you may be asking, &quot;&lt;b&gt;&lt;i&gt;can I not just guard against each of these individually?&lt;/i&gt;&lt;/b&gt;&quot; You certainly can, and you will be maintaining those guards forever. Or you can adopt the single stronger property that covers all of them at once: every run converges the target system to the source system&#39;s current truth, no matter how many times it executes.&lt;/p&gt;

&lt;h2&gt;Sync the state, not the event&lt;/h2&gt;

&lt;p&gt;The foundational pattern is a mental shift. An event-applying flow says &quot;an invoice was posted, so create an invoice.&quot; A state-converging flow says &quot;an invoice exists in the source; make the target match it.&quot; The first duplicates on replay. The second cannot, because its writes are defined relative to what is already there.&lt;/p&gt;

&lt;p&gt;Concretely, our invoice sync is a reconciling upsert. It looks up the target invoice by the source system&#39;s document number. If it does not exist, it creates it with its lines. If it does exist, it updates the header and then reconciles the lines: match each source line to a target line, update the matches in place, create the missing, and delete the orphans that no longer exist in the source. The payment sync works exactly the same way, reconciling the payment&#39;s application detail rows against what the source says is currently applied, deleting rows the source no longer has, right down to deleting the payment header itself when zero applications remain.&lt;/p&gt;

&lt;p&gt;In turn, a reconcile has a property an append never has: it is self-healing. When we later fixed a line-mapping defect, the reconcile did not just handle new invoices correctly; rerunning it against previously damaged invoices replaced the malformed lines with correct ones. The repair tool was the flow itself!&lt;/p&gt;

&lt;h2&gt;Let the source system name things&lt;/h2&gt;

&lt;p&gt;Idempotency needs a key, and the right key is the source system&#39;s own identity for the record, carried into the target where the flow can find it again. Our payment headers are named by the Business Central document number, which makes the name double as the idempotency key: any rerun finds the header it created before. Invoices are looked up by the posted invoice number. Nothing is keyed by anything the flow generates, because a generated key is different on every run, which is precisely the property you do not want.&lt;/p&gt;

&lt;p&gt;Getting this right at the line level took us three attempts, and the two failures are worth studying, because they will save you from repeating them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt one: match lines by name.&lt;/strong&gt; As it turns out, this failed immediately, because the target system overwrites the line description with the product name on product-based lines. Our matching field was being silently rewritten by the platform after every create, so matches always missed, and every reconcile deleted and recreated every line. The lesson: never match on a field the target system considers its own to modify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt two: match lines by product ID.&lt;/strong&gt; This one worked for months. Then it failed structurally the day we met an invoice with two lines sharing one product -- a maintenance credit billed as a negative line on the same product code it credits. The reconcile saw the product already present and concluded the credit line was already synced. It was not a bug in the matching code; the key itself could not distinguish the rows. Worse, the failure mode was silent: the reconcile ran, reported success, and structurally could never repair the missing line. Suffice to say, a key that is merely usually unique is a defect with a delayed fuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt three: match lines by the source system&#39;s line number,&lt;/strong&gt; stamped into a dedicated field on every line the flow writes. Business Central&#39;s line numbers are the source&#39;s own identity for the row: unique within the document, stable across time, and meaningless for the target platform to overwrite. Every line create and update stamps it; matching and orphan detection key on it. This is the version that survived every scenario we could throw at it.&lt;/p&gt;

&lt;p&gt;The general rule that falls out: &lt;strong&gt;the match key must be the source system&#39;s identity for the row, persisted in the target, and owned by nobody else.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A quick aside before moving on: both failures were diagnosed in minutes rather than an afternoon of clicking through run history, because we develop these flows with Claude Code reading run actions directly through FlowAgent. The agent walked the failed runs, found the exact action and input responsible each time, and in the second case also exposed that the product-keyed reconcile could never have repaired the damage. That workflow is a story of its own, and I told it in my FlowAgent article; here it is enough to say that resubmit-safe design and run forensics compose beautifully -- one gives you a safe repair tool, and the other tells you precisely when to use it.&lt;/p&gt;
&lt;!--On publish: link &quot;my FlowAgent article&quot; to the Building Power Automate Flows with Claude Code and FlowAgent post--&gt;

&lt;h2&gt;Gate the side effects on change&lt;/h2&gt;

&lt;p&gt;Data writes converge naturally under a reconcile, but side effects -- notifications, timeline notes, cascade triggers -- duplicate cheerfully unless you design them not to. Two patterns handled every case we had:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attach side effects to transitions, not to runs.&lt;/strong&gt; Our settlement timeline notes are written only when a detail row is actually created, only when a synced row is actually deleted, and only on the first sighting of a cancellation, guarded by &quot;the invoice is not already cancelled.&quot; A resubmitted run that changes nothing writes no note, because no transition occurred. We proved this on a live rerun: identical resubmit, zero writes, zero duplicate notes. The note trail reads like a history precisely because it records transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write only when the value differs.&lt;/strong&gt; Our bundle cascade flow updates a child record only if its expiration differs from the parent&#39;s or the row is inactive. That single guard makes the cascade convergent: reprocessing is free, and a chain of cascading updates terminates on its own -- five runs, converged, no runaway, in our live verification. As a bonus we did not initially design for, the flow can safely react to writes made by other flows sharing its connection identity, because reacting to an already-correct value is a no-op.&lt;/p&gt;

&lt;h2&gt;The payoff: resubmit becomes your universal tool&lt;/h2&gt;

&lt;p&gt;Once every flow held the property, resubmission quietly became the answer to almost everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repairs.&lt;/strong&gt; When the credit-line defect left two invoices wrong in the target system, the repair was: deploy the fix, resubmit the two failed runs. Both invoices converged to the source&#39;s truth to the penny, replacing the malformed legacy lines on the way. No repair script, no manual edits!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment fixes.&lt;/strong&gt; When a run failed on a missing privilege in a fresh environment, the fix was the role assignment plus one resubmit, which completed the scenario and re-proved reconcile idempotence in that environment for free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification.&lt;/strong&gt; Every flow change was tested by resubmitting a stored run and reading the results, precisely because reprocessing was guaranteed harmless.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Locked-record handling.&lt;/strong&gt; Even where the target system makes records read-only -- paid invoices, in our staging environment -- the reconcile wraps its work in a capture-state, reopen, sync, restore sequence, so a resubmit against a paid invoice succeeds and leaves the record exactly as it found it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Two caveats from the field&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; a resubmit replays the trigger payload against the &lt;em&gt;current&lt;/em&gt; flow definition. That is exactly what makes fix-then-resubmit work, but it has a sharp edge: if a deployment has left the flow stopped, a resubmit silently replays against the old definition, and the result can masquerade as a test outcome. It happened to us exactly once, which was enough to add &quot;verify the flow is active and read the definition back&quot; to the deployment checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; idempotency does not replace concurrency control. Convergent runs racing each other can still interleave reads and writes. We run our webhook-triggered flows at concurrency one, not because the design needs it to be correct in sequence, but as cheap insurance that runs actually execute in sequence. The two properties compose: idempotency makes replays safe, and serialization makes overlaps impossible.&lt;/p&gt;

&lt;h2&gt;The checklist&lt;/h2&gt;

&lt;p&gt;For every flow, before it ships, walk through the following five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can I resubmit any historical run right now with no damage? If not, why not? Fix that first.&lt;/li&gt;
&lt;li&gt;Is every record the flow writes findable by a key the source system owns?&lt;/li&gt;
&lt;li&gt;Does the flow reconcile to current source truth, including deleting what the source no longer has?&lt;/li&gt;
&lt;li&gt;Is every notification and note attached to a transition, not to a run?&lt;/li&gt;
&lt;li&gt;Does every cascading write fire only when the value actually differs?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When it&#39;s all said and done, the cost is a habit of mind during design, and the return is an integration where the scariest phrase in operations, &quot;just run it again,&quot; is not scary at all. It is the standard procedure, and it is always safe.&lt;/p&gt;

&lt;p&gt;If you have your own resubmit war story, or a match key that failed you in a way I did not list here, please drop a note in the comments describing your experience; idempotency lessons are expensive to learn firsthand and free to learn from each other.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/8241138433724031971/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/8241138433724031971' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8241138433724031971'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8241138433724031971'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/power-automate-design-every-flow-so.html' title='Power Automate | Design Every Flow So a Resubmit Is Always Safe'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-7677422031119209399</id><published>2026-08-19T07:00:00.000-04:00</published><updated>2026-08-19T07:00:00.121-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Azure Key Vault"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Platform"/><category scheme="http://www.blogger.com/atom/ns#" term="Security"/><title type='text'>Power Automate | Getting Secrets Out of Your Flows with Azure Key Vault</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Getting secrets out of your Power Automate flows with Azure Key Vault&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-04-keyvault.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;
&lt;p&gt;The flow we inherited at the start of our Business Central to Dynamics 365 CE billing integration, the same project from my FlowAgent article, had a problem so common it barely registers as a problem anymore: the OAuth client secret it used to call Business Central&#39;s OData services was sitting in plaintext, inside a String environment variable, referenced by the flow as an ordinary parameter. Everything worked. Nothing was on fire. And that secret was quietly everywhere!&lt;/p&gt;
&lt;!--On publish: link &quot;my FlowAgent article&quot; to the Building Power Automate Flows with Claude Code and FlowAgent post--&gt;

&lt;p&gt;So, in this article I will walk through where &quot;everywhere&quot; turns out to be, and the migration we ran to fix it: a secret-type environment variable backed by Azure Key Vault, runtime retrieval inside the flow, secured inputs and outputs, and a cleanup pass that chased the old secret out of every place it had settled. I will also cover the one gotcha that failed our first live run, because keep in mind, it will fail yours too if you do not know about it.&lt;/p&gt;

&lt;h2&gt;Where a plaintext secret actually lives&lt;/h2&gt;

&lt;p&gt;When a secret sits in a flow definition or a plaintext environment variable, it does not live in one place. It lives in every place the definition travels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every solution export.&lt;/strong&gt; The environment variable&#39;s value rides along in the zip. Email that zip to a colleague, attach it to a ticket, or drop it in a shared folder, and the secret goes with it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run history.&lt;/strong&gt; If the secret flows through an ordinary action, every run&#39;s inputs and outputs display it to anyone with permission to read run history -- and that audience is usually much larger than the audience you would knowingly hand a credential to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source control.&lt;/strong&gt; The moment you bring the solution under version control, which you absolutely should, the plaintext secret is in a commit, and commits are forever. Rotating the secret later does not un-commit the old one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups and working files.&lt;/strong&gt; Any tooling that snapshots flow definitions -- designer copies, local backups, agent working files -- multiplies the copies further.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is hypothetical. We found our inherited secret in the solution export, in the flow definition, in run history via the token request&#39;s visible inputs, and, once we started working, in the local backup snapshots our own tooling captured. The secret had more copies than the flow had actions!&lt;/p&gt;

&lt;p&gt;The fix Microsoft provides for exactly this scenario is the &lt;strong&gt;secret-type environment variable&lt;/strong&gt;: instead of storing a value, it stores a pointer to a secret in Azure Key Vault, and the flow retrieves the value at runtime. In turn, the vault becomes the only place the secret exists. Everything that travels -- the solution, the source tree, the definition -- carries only the pointer.&lt;/p&gt;

&lt;h2&gt;The migration, in the order that worked&lt;/h2&gt;

&lt;p&gt;We did this as a single day&#39;s remediation, and as it turns out, the sequencing mattered as much as the steps. Here is the order, with the reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) Set up Key Vault RBAC first.&lt;/strong&gt; Secret-type environment variables only support Azure Key Vault as the store, and two identities need roles on the vault before anything else works: the person doing the setup needs to write the secret and pass the environment variable&#39;s save-time validation (Key Vault Secrets Officer covers both), and the Dataverse service principal needs Key Vault Secrets User so the platform can retrieve the value at runtime. Also confirm the Power Platform resource provider is registered on the subscription that owns the vault; it is a one-time thing, and a confusing failure if it is missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Rotate by appending, and never look at the new secret.&lt;/strong&gt; We did not migrate the old secret; we replaced it. The new client secret was created on the app registration with an append, leaving the old credential intact so the running flow never broke during the migration, and piped directly from the creation command into the vault in a single step. The value was never displayed on screen, never in a file, never in shell history. Keep in mind: if your new secret is ever visible, you have created another copy to worry about. The pipe means there is nothing to clean up afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Create the secret-type environment variable, and get the reference format right.&lt;/strong&gt; The variable lives in the solution like any other environment variable, but its value record contains only a pointer to the secret, and the form of that pointer is the part that stopped us cold, because it is not what instinct suggests. It is not the secret&#39;s URL from the Azure portal, and it is not a vault name and secret name pair. It is the secret&#39;s full Azure &lt;strong&gt;resource ID path&lt;/strong&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.KeyVault/vaults/{vault-name}/secrets/{secret-name}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note here. Every segment is required, including the &lt;code&gt;providers/Microsoft.KeyVault&lt;/code&gt; in the middle that nobody types from memory. If you create the variable through the maker portal, the form collects the pieces separately and assembles the path for you; if you create it through the Web API, or find yourself editing the value record directly, you supply the whole path yourself, and anything else fails validation. The save is also more than a format check: Dataverse resolves the path against Azure with your permissions at save time, so a typo in any segment, or a missing role on the vault, surfaces right there rather than at the first flow run. That is a kindness once you understand it, and a mystifying save error until you do!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; export the solution afterward and inspect it; you should see the resource path and nothing else. We made this check part of the verification, not an assumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) Prove retrieval with a throwaway flow before touching the real one.&lt;/strong&gt; You may be asking, &quot;can I just test the retrieval from the Web API?&quot; Categorically no! Secrets in secret-type environment variables are only resolvable from inside flows and custom connectors, and that is by design. So we built a disposable test flow whose only job was to call &lt;code&gt;RetrieveEnvironmentVariableSecretValue&lt;/code&gt; and succeed, ran it, confirmed the retrieval, and deleted it. Two minutes of scaffolding, and the real flow&#39;s rewire started from a known-good vault path instead of debugging two things at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Rewire the flow, and secure both ends.&lt;/strong&gt; The real flow gained one action: a Dataverse unbound action call to &lt;code&gt;RetrieveEnvironmentVariableSecretValue&lt;/code&gt; for the new variable, with &lt;strong&gt;secured inputs and outputs&lt;/strong&gt; turned on. The token request was repointed to consume that action&#39;s output, and it got secured inputs and outputs as well. That second part is easy to forget and matters just as much: the client secret passes through the token request, and the bearer token that comes back is itself a credential. With both actions secured, neither the secret nor the token appears in run history anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) Add a graceful failure path.&lt;/strong&gt; A vault outage or a permissions change now has a single, well-defined failure point, so we gave it a dedicated handler: an alert email carrying the run ID and the likely causes, then a Terminate with a named error code. Without this, a retrieval failure dies mid-run with secured -- and therefore blank -- diagnostics, which brings us to the gotcha in a moment.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhiwa9cvh8TajkcxBTT-T_F0b_6Udz46RdQ9RkijSVxKcSKn7-xFORQwlqMLJ1v08Jzx63RmHuqx3ATjZpj81pHV3r4774fxZ2_CRflcx6XqOCPcjUG5UT9JhirT9g92hDVvJHZRz1zft3GpmGkr_FuPjyGfFwMvYo4RrWGH93b5djY8CAdmnYLLJiyMmyb&quot;&gt;&lt;img alt=&quot;&quot; data-original-height=&quot;1048&quot; data-original-width=&quot;762&quot; height=&quot;419&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhiwa9cvh8TajkcxBTT-T_F0b_6Udz46RdQ9RkijSVxKcSKn7-xFORQwlqMLJ1v08Jzx63RmHuqx3ATjZpj81pHV3r4774fxZ2_CRflcx6XqOCPcjUG5UT9JhirT9g92hDVvJHZRz1zft3GpmGkr_FuPjyGfFwMvYo4RrWGH93b5djY8CAdmnYLLJiyMmyb=w306-h419&quot; width=&quot;306&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: center; font-size: 0.9em; margin: 0.4em 0 1.4em;&quot;&gt;&lt;em&gt;Graceful failure path&lt;/em&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;div style=&quot;text-align: center;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;strong&gt;7) Clean up until a search proves you are done.&lt;/strong&gt; The old plaintext environment variable and the flow parameter that referenced it were deleted; a forgotten draft copy of the flow held the last dependency and had to go first, which the dependency checker told us. Then we searched the entire synced source tree for any fragment of the old secret and did not stop until the search came back empty. Local backup snapshots that predated the fix still contained it, so they went into &lt;code&gt;.gitignore&lt;/code&gt;. Only after the tree was provably secret-free did the repository get its first commit. That ordering is worth stating plainly: &lt;strong&gt;git init comes after the secret hunt, not before.&lt;/strong&gt;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8) Hand the old secret off for rotation.&lt;/strong&gt; The old credential was left working throughout the migration and then handed to the system administrator to rotate, after checking whether anything else used it. In our case, something else probably did, which is its own small lesson: plaintext secrets get shared around precisely because they are visible, and you cannot assume you are the only consumer.&lt;/p&gt;

&lt;h2&gt;The gotcha: runtime RBAC follows the connection owner&lt;/h2&gt;

&lt;p&gt;Our first live-fire test failed at the secret retrieval with a BadRequest and no details, because secured outputs hide details by design. The error handling worked beautifully -- alert sent, run terminated cleanly, zero partial writes -- but the diagnosis took some actual thought, so here it is for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Dataverse validates the vault RBAC of the calling user, and the calling user is the owner of the Dataverse &lt;em&gt;connection&lt;/em&gt; the flow uses, not the person who built the flow. As it turns out, our earlier test flow had succeeded because it ran on the author&#39;s own connection, and the author had vault roles. The production flow ran on a shared connection owned by a service account that had none. Granting Key Vault Secrets User to that service account fixed it, and the resubmitted run passed end to end.&lt;/p&gt;

&lt;p&gt;The practical rule: enumerate every identity that will ever own a connection your flow runs on, in every environment, and grant each one Secrets User on the vault. And when the retrieval fails with hidden diagnostics, check connection ownership before anything else. It is the most likely cause and the least visible one.&lt;/p&gt;

&lt;h2&gt;The payoff shows up at deployment time&lt;/h2&gt;

&lt;p&gt;Weeks later, when we deployed the solution to a staging environment through a Power Platform pipeline, the secret story was the part that just worked. The secret-type environment variable&#39;s vault reference traveled with the solution; no secret value needed to be supplied at import, because there is no secret value in the solution. The only per-environment chore is the RBAC grant for that environment&#39;s connection-owning identity, and the first live run tells you immediately whether you missed it, through the graceful failure path built in step 6.&lt;/p&gt;

&lt;p&gt;Now, compare that with the plaintext alternative, where every deployment either carries the secret in the zip or prompts someone to paste it in. Both of those mint fresh copies. It just doesn&#39;t work!&lt;/p&gt;

&lt;h2&gt;The other half: everything else into environment variables too&lt;/h2&gt;

&lt;p&gt;The secret was the dangerous case, but the same remediation pass moved every per-environment value out of the flow definition and into ordinary environment variables: tenant ID, company ID, environment name, target system record IDs, alert recipients, and a set of sandbox-only test toggles. Two decisions there proved right in hindsight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ship no values in the solution for anything environment-specific.&lt;/strong&gt; An import that prompts is a feature; a value that silently travels to the wrong environment is a defect waiting to be discovered in production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make the flow&#39;s behavior mode explicit.&lt;/strong&gt; A Sandbox/Production mode variable, with test toggles honored only in Sandbox mode, means production can never accidentally run with a test configuration. The gate rule lives in the flow once, not scattered across conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it&#39;s all said and done, you end up with a flow definition that has no secrets, no environment names, no GUIDs, and no email addresses in it. Everything that varies is a variable, and the one thing that must not leak is a pointer to a vault. That is the whole trick, and a day of careful sequencing is all it costs.&lt;/p&gt;

&lt;p&gt;If you run this migration yourself, or if you have chased a plaintext secret through more hiding places than the ones I listed here, please drop a note in the comments describing your experience; these war stories are how the community keeps each other&#39;s credentials out of run history.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/7677422031119209399/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/7677422031119209399' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/7677422031119209399'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/7677422031119209399'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/power-automate-getting-secrets-out-of.html' title='Power Automate | Getting Secrets Out of Your Flows with Azure Key Vault'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEhiwa9cvh8TajkcxBTT-T_F0b_6Udz46RdQ9RkijSVxKcSKn7-xFORQwlqMLJ1v08Jzx63RmHuqx3ATjZpj81pHV3r4774fxZ2_CRflcx6XqOCPcjUG5UT9JhirT9g92hDVvJHZRz1zft3GpmGkr_FuPjyGfFwMvYo4RrWGH93b5djY8CAdmnYLLJiyMmyb=s72-w306-h419-c" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-8965672142025405958</id><published>2026-08-17T07:00:00.000-04:00</published><updated>2026-08-17T07:00:00.177-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ALM"/><category scheme="http://www.blogger.com/atom/ns#" term="DevOps"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Platform"/><title type='text'>One Solution, Two Source Trees: The Maker Portal&#39;s Git Integration vs pac</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;One Solution, Two Source Trees: maker portal Git integration versus pac&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-03-git-pac.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;
&lt;p&gt;Early in our Business Central to Dynamics 365 CE billing integration, the same project I wrote about in my &lt;a href=&quot;https://www.theworkbench.blog/2026/08/building-power-automate-flows-with.html&quot; target=&quot;_blank&quot;&gt;FlowAgent article&lt;/a&gt;, we ran into a lesson that Microsoft&amp;#39;s own documentation only caught up to this spring, and that I have not seen written up from the trenches: there are now two official ways to put a Power Platform solution under source control, and they do not mix. At all! We learned this the day we put the solution source pac had cloned next to the solution source the maker portal&#39;s Git integration had committed, and found two completely different structures staring back at us.&lt;/p&gt;


&lt;p&gt;So, in this article I will walk through the two formats, how the collision showed up in our project, and why we picked the CLI side, with a couple of smaller git-versus-pac frictions at the end that you will want to know about before you start.&lt;/p&gt;

&lt;h2&gt;Two formats, one solution&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The classic XML format&lt;/strong&gt; is what &lt;code&gt;pac solution clone&lt;/code&gt;, &lt;code&gt;pac solution sync&lt;/code&gt;, and the traditional &lt;a href=&quot;https://learn.microsoft.com/power-platform/alm/solution-packager-tool&quot;&gt;SolutionPackager&lt;/a&gt; workflow produce. The solution&#39;s identity lives in &lt;code&gt;Other\Solution.xml&lt;/code&gt; and &lt;code&gt;Other\Customizations.xml&lt;/code&gt;, and the component files (workflow JSON, environment variable definitions, connection references) sit in a folder hierarchy alongside them. Keep in mind this is the format that pairs with the round-trip commands: &lt;code&gt;pac solution pack&lt;/code&gt; builds an importable zip from the folder, &lt;code&gt;pac solution import&lt;/code&gt; pushes it to the environment, and &lt;code&gt;pac solution sync&lt;/code&gt; pulls the environment&#39;s current state back down.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjVJtSD3ss43jgqAPw8AeYT3fO0BYpiChiqAqLmYQ18yTOGKgiwmbu2q4drMnjWHunrOUZsLZ-svgOsW_mzy-tvQPG5F9UsUKhjkNCZaBitiaio2EIawsk5nPznaBIYVGOpXStoM7OVsKwOHYecT1tkGbijuftmW81Kzhn8jxCfNOxIi4VbV0_2KF0agVKb&quot;&gt;&lt;img alt=&quot;&quot; data-original-height=&quot;693&quot; data-original-width=&quot;633&quot; height=&quot;570&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjVJtSD3ss43jgqAPw8AeYT3fO0BYpiChiqAqLmYQ18yTOGKgiwmbu2q4drMnjWHunrOUZsLZ-svgOsW_mzy-tvQPG5F9UsUKhjkNCZaBitiaio2EIawsk5nPznaBIYVGOpXStoM7OVsKwOHYecT1tkGbijuftmW81Kzhn8jxCfNOxIi4VbV0_2KF0agVKb=w521-h570&quot; width=&quot;521&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;font-size: 0.9em; margin: 0.4em 0px 1.4em; text-align: center;&quot;&gt;&lt;em&gt;Classic XML Format&lt;/em&gt;&lt;/div&gt;&lt;br /&gt;&lt;strong&gt;The &lt;a href=&quot;https://learn.microsoft.com/power-platform/alm/solution-source-control-yaml-format&quot;&gt;YAML source control format&lt;/a&gt;&lt;/strong&gt; is what the maker portal&#39;s native &lt;a href=&quot;https://learn.microsoft.com/power-platform/alm/git-integration/overview&quot;&gt;Git integration&lt;/a&gt; writes. When you connect an environment or a solution to Azure DevOps from within Power Apps, every commit lands as a &lt;code&gt;solutions\&amp;lt;name&amp;gt;\solution.yml&lt;/code&gt; manifest with component folders at the repository root. I will be the first to tell you it is a genuinely nicer format to read: compact YAML, small focused diffs, and support for canvas apps and modern flows that the XML layout never handled. It is also, and this is the entire point of this article, a different representation that the classic tooling chain does not interoperate with.&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjsbZTOjRDQMljVlwCsdza-yWxMUBTwiRt0v4V9beS7O6Z6SUwbpMRDoyFnIq5eGw6b-OVk7kRmtdbguUAIg1kdMpyEN3IbsXKiCYDoqwntqOcF2p1B9AnnWiHiQYZ3_Bblg1naYGx4R1vnFAazygpMwXkq7_oz86honTi7x8gqGlKFDwBuEsYGKHoKIv-F&quot;&gt;&lt;img alt=&quot;&quot; data-original-height=&quot;859&quot; data-original-width=&quot;671&quot; height=&quot;674&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjsbZTOjRDQMljVlwCsdza-yWxMUBTwiRt0v4V9beS7O6Z6SUwbpMRDoyFnIq5eGw6b-OVk7kRmtdbguUAIg1kdMpyEN3IbsXKiCYDoqwntqOcF2p1B9AnnWiHiQYZ3_Bblg1naYGx4R1vnFAazygpMwXkq7_oz86honTi7x8gqGlKFDwBuEsYGKHoKIv-F=w527-h674&quot; width=&quot;527&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;font-size: 0.9em; margin: 0.4em 0px 1.4em; text-align: center;&quot;&gt;&lt;em&gt;YAML Format&lt;/em&gt;&lt;/div&gt;&lt;br /&gt;Now, here is the part that will save you an afternoon of head scratching. The packager tools decide which format they are looking at by folder shape. A &lt;code&gt;solutions\&lt;/code&gt; subdirectory containing &lt;code&gt;solution.yml&lt;/code&gt; files means YAML; no &lt;code&gt;solutions\&lt;/code&gt; subdirectory means classic XML, in which case the tool goes looking for &lt;code&gt;Other\Solution.xml&lt;/code&gt;. There is no flag that says &quot;this repo contains both, figure it out.&quot;&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; a folder that mixes the two layouts, or that has YAML files in the wrong place, produces the format war&#39;s signature error: a complaint about a missing &lt;code&gt;Customizations.xml&lt;/code&gt; from a tool that has silently fallen back to the XML code path while staring at YAML files. If you have ever seen that error and thought &quot;but this is not an XML solution,&quot; congratulations, you have met the auto-detection rules! And as of this spring, Microsoft&amp;#39;s SolutionPackager documentation carries a troubleshooting entry for exactly this case, describing the silent fallback and calling the error misleading in so many words. It is good to see it written down; it would have been better to read it before we found it.&lt;/p&gt;

&lt;h2&gt;What happened to us&lt;/h2&gt;

&lt;p&gt;Our source control story started deliberately. On the first day, we ran &lt;code&gt;pac solution clone&lt;/code&gt; against the inherited invoice flow&#39;s solution, dropped the XML source tree into a folder, and began a discipline we have kept ever since: the cloud flow is the working copy, the local tree is the versioned mirror, and after every applied change we run &lt;code&gt;pac solution sync&lt;/code&gt;, write a changelog entry, and commit. That loop was not an accident. Our whole development approach, an AI coding agent editing flow definitions, packing them, and importing them, depends on having the real definition JSON locally and being able to round-trip it through &lt;code&gt;pac solution pack&lt;/code&gt; and &lt;code&gt;pac solution import&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then the maker portal&#39;s Git integration entered the picture, the way it enters most pictures: it is right there in the portal, connecting a solution to a repo takes a few clicks, and it feels like the officially blessed path. And it is a blessed path! It is just a different one, and it wants to own the repository in its own format.&lt;/p&gt;

&lt;p&gt;Now, the moment the incompatibility became real for us was not an error message; it was a side-by-side comparison. We put the tree pac had cloned next to the repository the portal&#39;s Git integration had committed, expecting two views of the same solution, and found two structures with almost nothing in common: different file names, different nesting, different serialization, and no file on one side corresponding one-to-one with a file on the other. This was not two dialects that a diff tool could reconcile. There was nothing to diff. Nothing! Keep in mind, that comparison is worth doing deliberately if you are facing the same choice, because it settles the question faster than any documentation will.&lt;/p&gt;

&lt;p&gt;And once you have seen it, the operational hazard is obvious: had both stayed connected, every portal commit and every &lt;code&gt;pac solution sync&lt;/code&gt; would have written its own divergent truth into the same repo, with the packager&#39;s auto-detection deciding which one it believed based on which folders it noticed first. Nothing about that fails loudly at the moment you set it up. It fails later, confusingly, in tooling errors that point at the wrong cause. A repository is going to hold one format or the other, and somebody has to choose.&lt;/p&gt;

&lt;p&gt;So we chose, and wrote it down: this solution is versioned in the classic XML format maintained by pac, the maker portal&#39;s Git integration was deliberately disconnected, and the README carries a standing instruction, in bold, that says do not reconnect it. That last part matters more than it looks. The portal integration is a checkbox any admin can re-enable in a moment of tidiness, and the person who re-enables it will genuinely believe they are improving your source control posture. Suffice to say, that one bolded line in the README is the cheapest insurance we bought all week.&lt;/p&gt;
&lt;h2&gt;Why we picked the pac side&lt;/h2&gt;

&lt;p&gt;You may be asking, &quot;so is the YAML format bad?&quot; Not at all. On a fresh project with canvas apps in the mix, the YAML format and the portal integration are very likely the right choice, and Microsoft&#39;s direction of travel is clearly toward it. We picked the XML side for three reasons specific to how we work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) The round-trip commands are our development loop, not just our backup.&lt;/strong&gt; Large flow edits in our project go through the solution route: edit the definition JSON locally, then &lt;code&gt;pac solution pack&lt;/code&gt;, &lt;code&gt;pac solution import&lt;/code&gt;, and publish. The clone and sync commands our loop depends on produce the classic XML format, and as I write this they still do not emit the YAML layout, whatever the documentation implies (there is &lt;a href=&quot;https://github.com/microsoft/powerplatform-build-tools/issues/1385&quot;&gt;an open issue&lt;/a&gt; on exactly that). Choosing the portal&#39;s format would have meant choosing away the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Commit cadence and commit meaning.&lt;/strong&gt; The portal integration commits when a maker clicks commit, at whatever granularity the portal batches. Our discipline is one commit per applied change, pushed immediately, with a changelog entry stating what changed, why, where it was applied, and how it was verified. In turn, that kind of history only happens when the same actor making the change also makes the commit, which in our case is the agent completing its edit-sync-log-commit ritual. The repository is not a backup of the solution; it is the audit trail of the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) One writer per repository.&lt;/strong&gt; Even if the formats had been compatible, two independent processes committing to the same branch on their own schedules is a merge conflict generator. A source control setup where you cannot predict who writes next is worse than either setup alone. It just doesn&#39;t work!&lt;/p&gt;

&lt;h2&gt;Two frictions that come with the pac side&lt;/h2&gt;

&lt;p&gt;Choosing pac did not end the git-versus-pac story. Two smaller frictions followed, both consequences of letting a CLI own the source tree, and both are worth knowing before you start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Line endings.&lt;/strong&gt; Git on Windows helpfully converts line endings unless told otherwise, and pac-generated source trees do not want the help. Every &lt;code&gt;pac solution sync&lt;/code&gt; after a fresh clone produced walls of CRLF warnings and threatened noisy whitespace-only diffs. The fix was repo-local: set &lt;code&gt;core.autocrlf=input&lt;/code&gt; in the repository config, and add a &lt;code&gt;.gitattributes&lt;/code&gt; marking the pac-generated source trees as &lt;code&gt;-text&lt;/code&gt; so git stops converting them entirely. The solution source is machine-generated and machine-consumed; git should store it byte for byte and stay out of the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Byte order marks.&lt;/strong&gt; A later sync quietly restored UTF-8 byte order marks on files where our editing had stripped them, which showed up as mysterious whole-file diffs on files nobody had meaningfully changed. We widened the &lt;code&gt;.gitattributes&lt;/code&gt; coverage and learned the larger lesson: treat the pac tree as pac&#39;s territory. Our edits happen there when the loop requires it, but the formatting conventions are the tool&#39;s, and fighting them costs more than accepting them. Fence the pac tree off from git&#39;s text handling with &lt;code&gt;.gitattributes&lt;/code&gt; the day you create it, not the day the warnings annoy you.&lt;/p&gt;

&lt;h2&gt;What I would tell you to do&lt;/h2&gt;

&lt;p&gt;Whichever side you land on, two practices carry over to any project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pick one format before the first commit, and never let both write to the same repository.&lt;/strong&gt; If your workflow lives in the CLI round-trip commands, take the classic XML format. If you are starting fresh, especially with canvas apps or a team of makers committing from the portal, take the YAML format and let the portal integration own the repo. Either way, if someone has already connected the other one, disconnect it before it writes a single commit. Divergence is silent, and cleanup is manual.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the decision down where the next admin will trip over it.&lt;/strong&gt; A bolded &quot;do not reconnect the Git integration for this solution&quot; in the README, with one sentence of why, will save a future colleague from an afternoon of &lt;code&gt;Customizations.xml&lt;/code&gt; errors on a repository full of YAML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The two-format situation is a snapshot of a platform mid-transition, and it should resolve as the newer format&#39;s tooling support fills in. Until it does, keep in mind the failure mode is not that either path is broken. Both paths work, both look official, and nothing warns you that they cannot coexist in the same repository. Consider yourself warned!&lt;/p&gt;
&lt;p&gt;If you have hit the two-format collision yourself, or found a cleaner way through it, please drop a note in the comments describing your experience; this corner of Power Platform ALM has very little written about it, and the community learns fastest when we compare scars.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/8965672142025405958/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/8965672142025405958' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8965672142025405958'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8965672142025405958'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/one-solution-two-source-trees-maker.html' title='One Solution, Two Source Trees: The Maker Portal&#39;s Git Integration vs pac'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEjVJtSD3ss43jgqAPw8AeYT3fO0BYpiChiqAqLmYQ18yTOGKgiwmbu2q4drMnjWHunrOUZsLZ-svgOsW_mzy-tvQPG5F9UsUKhjkNCZaBitiaio2EIawsk5nPznaBIYVGOpXStoM7OVsKwOHYecT1tkGbijuftmW81Kzhn8jxCfNOxIi4VbV0_2KF0agVKb=s72-w521-h570-c" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-7409906273057442616</id><published>2026-08-13T07:00:00.000-04:00</published><updated>2026-08-13T07:00:00.121-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI"/><category scheme="http://www.blogger.com/atom/ns#" term="Claude Code"/><category scheme="http://www.blogger.com/atom/ns#" term="MCP"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><title type='text'>Building Power Automate Flows with Claude Code and FlowAgent</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Building Power Automate Flows with Claude Code and FlowAgent&quot; height=&quot;630&quot; src=&quot;https://raw.githubusercontent.com/dgpblogster/theworkbench-assets/main/covers/cover-02-flowagent.png&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;
&lt;!--Cover hosted on GitHub; optionally re-upload via the Blogger editor for Google-hosted images--&gt;
&lt;p&gt;For the past week, my team and I have been heads-down building a Business Central to Dynamics 365 CE billing integration: posted sales invoices, applied cash receipts, subscription cancellations, and the product registry automation that ties them all together. Every one of those integrations is a Power Automate cloud flow, and every one of them was developed, debugged, and verified with &lt;b&gt;Claude Code&lt;/b&gt; driving Power Automate directly, through a tool called &lt;b&gt;FlowAgent&lt;/b&gt;. Suffice to say, this changed how I think about building flows, so I figured I would write up what we learned while it is still fresh.&lt;/p&gt;

&lt;p&gt;Now, keep in mind this is not a review written after a demo. This is written after shipping three production-bound flows through dozens of iterations, a full test campaign, and a healthy number of defects found and fixed along the way. The scars are real!&lt;/p&gt;

&lt;h2&gt;What FlowAgent is&lt;/h2&gt;

&lt;p&gt;FlowAgent is an MCP server that ships inside the &lt;strong&gt;power-automate&lt;/strong&gt; plugin from the good folks at Microsoft, published in the open source &lt;a href=&quot;https://github.com/microsoft/power-platform-skills&quot;&gt;power-platform-skills&lt;/a&gt; repository. MCP, the Model Context Protocol, is the standard that lets an AI coding assistant call real tools instead of just talking about them. Once the plugin is installed, Claude Code gains a set of Power Automate operations it can invoke on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List environments and flows, and set a working environment and current flow&lt;/li&gt;
&lt;li&gt;Read a flow&#39;s full definition -- this is, the JSON under the designer&lt;/li&gt;
&lt;li&gt;Edit, validate, and publish flows&lt;/li&gt;
&lt;li&gt;Read run history, run details, and per-action inputs and outputs from any run&lt;/li&gt;
&lt;li&gt;Inspect connections, connectors, and operation schemas&lt;/li&gt;
&lt;li&gt;Search connector operations and get expression help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin also bundles a Microsoft Learn MCP server, so the assistant can pull official documentation while it works, plus a set of skills -- guided workflows -- for creating, debugging, and diagnosing flows.&lt;/p&gt;

&lt;p&gt;You may be asking, &quot;why does this matter when the assistant can already tell me what to build?&quot; Very simple: without FlowAgent, the AI describes a flow and you go click it together in the designer. With FlowAgent, the assistant reads the actual flow, makes the actual edit, publishes it, watches the actual run, and reads the actual error. The feedback loop that used to run through your eyes and your mouse now runs at tool speed.&lt;/p&gt;

&lt;h2&gt;Setting it up&lt;/h2&gt;

&lt;p&gt;The prerequisites are modest: Claude Code, Node.js, the Azure CLI logged into your tenant -- FlowAgent uses it for authentication -- and ideally the Power Platform CLI (&lt;code&gt;pac&lt;/code&gt;) for solution work. Installation is two commands from within Claude Code:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude plugin marketplace add microsoft/power-platform-skills
claude plugin install power-automate@power-platform-skills&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note before moving on. The first command registers Microsoft&#39;s repository as a plugin marketplace; the second installs the power-automate plugin from it. After both commands complete, restart your Claude Code session, because plugins attach at session start. Then run the plugin&#39;s setup skill once: it verifies Node, the Azure CLI login, and the Power Automate token, and lets you pin a working environment so every later operation lands where you expect.&lt;/p&gt;

&lt;p&gt;Two lessons from our own installation that may save you an afternoon:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; check which Claude Code binary you are actually running. We had an old standalone CLI on the PATH that predated the plugin system entirely, and it silently swallowed the &lt;code&gt;plugin&lt;/code&gt; subcommands as if they were prompts. The VS Code extension ships a current CLI in its &lt;code&gt;resources\native-binary&lt;/code&gt; folder; that one worked immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; do not hand-edit the plugin registration files. Our first attempt at manual registration produced a &quot;marketplace configuration corrupted&quot; state that even blocked removal. Resetting the marketplace file and using the real CLI commands fixed it in minutes.&lt;/p&gt;

&lt;h2&gt;Debugging: the single biggest win&lt;/h2&gt;

&lt;p&gt;When a flow fails, the assistant pulls the run, walks the failed action, reads its exact inputs and outputs, and correlates all of it with the flow definition in one pass. Early in the project, this alone paid for the whole setup. As it turns out, our invoice flow had been silently broken for about two weeks, because the web services it called no longer existed, and the failure was masked by trigger noise. Two weeks! Nobody noticed, and frankly, nobody was going to: reading run histories systematically across hundreds of runs is exactly the kind of work a human never quite gets around to. Claude Code found it, remapped both HTTP calls to the replacement services, and verified the fix against a live run.&lt;/p&gt;

&lt;p&gt;The same loop caught subtler things. A negative credit line on an invoice was failing because the CRM connector rejects negative discount amounts. The failed runs told the whole story: header created, positive line created, credit line refused. Reading the actual action outputs turned a vague &quot;totals are off in CRM&quot; report into a precise defect, a designed fix, and a verified repair, all inside one working session.&lt;/p&gt;

&lt;h2&gt;Edits go where edits belong&lt;/h2&gt;

&lt;p&gt;Our standing rule became: the cloud flow is the working copy, and the local solution source is the versioned mirror. Small, surgical changes go through FlowAgent&#39;s edit and publish tools. After every applied change, &lt;code&gt;pac solution sync&lt;/code&gt; pulls the new definition into a git repository and the change gets a changelog entry. The assistant does all of this in one motion, so the discipline actually holds. Every flow change in this project, and there have been many dozens, is in version control with a written rationale.&lt;/p&gt;

&lt;p&gt;For large edits, there is a practical workaround worth knowing: tool payloads truncate somewhere around 5 KB, so a big definition rewrite goes through the solution route instead. Edit the definition JSON locally, then &lt;code&gt;pac solution pack&lt;/code&gt;, &lt;code&gt;pac solution import&lt;/code&gt;, and publish. Same destination, different door. The assistant learned to choose the route based on the size of the change.&lt;/p&gt;

&lt;h2&gt;Verification stopped being optional&lt;/h2&gt;

&lt;p&gt;Because the assistant can resubmit a stored run and then read the new run&#39;s results, &quot;deploy and hope&quot; turned into &quot;deploy and prove.&quot; Our habit became: make the change, resubmit a known trigger event, read every action&#39;s output, compare the CRM records against Business Central truth, and only then call it done.&lt;/p&gt;

&lt;p&gt;During the test campaign, this cadence caught a deployment race where a managed export picked up a stale version and the target environment silently skipped the import as already installed. In turn, a resubmit against what turned out to be the old definition briefly looked like a test result. The lesson is now procedure: after every deployment, read the definition back from the target before trusting any run.&lt;/p&gt;

&lt;h2&gt;The rough edges, honestly&lt;/h2&gt;

&lt;p&gt;FlowAgent is genuinely useful and genuinely version one. Things we hit, and how we worked around them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Solution-aware flows do not appear in the flow list&lt;/strong&gt;, which only shows personal-scope flows. The workaround is to address your flow by its ID directly. Once pinned as the current flow, everything else works.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The resubmit and diagnose tools had bugs&lt;/strong&gt; in the version we used; both threw type errors. We resubmitted runs through the underlying Power Automate REST API instead, which the assistant handles just fine, and filed the issues upstream with the plugin&#39;s built-in report-issue skill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Importing a solution leaves flows stopped&lt;/strong&gt;, and validation errors surface at activation time, not import time. Publishing or activating after import is a required step, not a nicety.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection authorization is per connection, not per flow.&lt;/strong&gt; A brand new flow referencing a shared connection needs one activation by the connection&#39;s owner; after that, other makers can deactivate, edit, and reactivate freely. Knowing this pattern saved us from repeatedly misdiagnosing authorization failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these were blockers. All of them are the kind of thing you want written down before you start, which is exactly why they are written down here.&lt;/p&gt;

&lt;h2&gt;What it added up to&lt;/h2&gt;

&lt;p&gt;In a bit under a week of working sessions, this setup carried us through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remediation of six pre-existing defects in an inherited flow, including moving a plaintext secret into Azure Key Vault and parameterizing every environment-specific value into environment variables&lt;/li&gt;
&lt;li&gt;A ground-up rebuild of the invoice flow onto a customer ledger trigger, eliminating an entire class of draft-noise and race-condition bugs&lt;/li&gt;
&lt;li&gt;A cash receipts flow built from a feasibility stub to feature complete in a single day, across four live-verified iterations&lt;/li&gt;
&lt;li&gt;A registry automation flow that replaced two hardcoded legacy flows with one table-driven design&lt;/li&gt;
&lt;li&gt;A full staging test campaign in which every planned scenario across three flows fired live, with every defect found along the way fixed and re-verified&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Could a person do all of that in the portal designer? Eventually. But the honest answer is that the verification depth is what a person would not do. Reading every action output of every test run, reconciling every synced record against the source system to the penny, and writing it all down as you go is precisely the tedious, high-value work that an agent with real tool access does without fatigue.&lt;/p&gt;

&lt;h2&gt;If you try it&lt;/h2&gt;

&lt;p&gt;Three practices made the difference for us, and none of them are about the tool itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep a changelog and a git mirror from day one.&lt;/strong&gt; The agent moves fast; the paper trail is what lets you trust the speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decide your edit routing up front.&lt;/strong&gt; Cloud as working copy, local as mirror, one commit per applied change. Ambiguity here is how definitions drift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Insist on live verification.&lt;/strong&gt; The agent can prove its work against real runs. Make it do so every time, and &quot;it should work&quot; disappears from your vocabulary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One last thing, because it matters: FlowAgent will not design your integration for you, and it should not. The design conversations, the business rulings, and the judgment calls stayed firmly human in our project. What it collapses is the distance between a decision and a verified, versioned, running flow -- a distance that used to be measured in hours of clicking and is now measured in minutes of reading the agent&#39;s evidence.&lt;/p&gt;

&lt;p&gt;If you take FlowAgent for a spin, please drop a note in the comments describing your experience, including the rough edges you find; that feedback is exactly what makes open source tooling like this get better.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/7409906273057442616/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/7409906273057442616' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/7409906273057442616'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/7409906273057442616'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/building-power-automate-flows-with.html' title='Building Power Automate Flows with Claude Code and FlowAgent'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-1641915574014671123</id><published>2026-08-11T09:35:27.912-04:00</published><updated>2026-08-11T09:35:27.912-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Agents"/><category scheme="http://www.blogger.com/atom/ns#" term="Copilot Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="GitHub"/><category scheme="http://www.blogger.com/atom/ns#" term="MCP"/><title type='text'>Same Agent, Two Architectures: When MCP Wins (and When It Doesn&#39;t)</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;text-align: center;&quot;&gt;&lt;img alt=&quot;Same Agent, Two Architectures: connector agent versus MCP agent in Copilot Studio&quot; data-original-height=&quot;630&quot; data-original-width=&quot;1200&quot; height=&quot;630&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhkrl_litjnFdt-K1B-RXfDbnqeOFbPKUavvAqZ_s8W6v2QSwg2rdSYO-u3tGq2fkly3hmI53LiaSHhAR0bP54Ge2JZkylSDT82dIO7QbcTWSuuoy5o2aZMLBPViSW7-O38QTAfPS920iYhLkpxmKWlEgLNqXq_kT3QY-sg2HqZogzoEt-z_Mz0kAG5BP9j&quot; style=&quot;height: auto; max-width: 100%;&quot; width=&quot;1200&quot; /&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhkrl_litjnFdt-K1B-RXfDbnqeOFbPKUavvAqZ_s8W6v2QSwg2rdSYO-u3tGq2fkly3hmI53LiaSHhAR0bP54Ge2JZkylSDT82dIO7QbcTWSuuoy5o2aZMLBPViSW7-O38QTAfPS920iYhLkpxmKWlEgLNqXq_kT3QY-sg2HqZogzoEt-z_Mz0kAG5BP9j&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;I know, I know. It has been almost five years since my last post, and in that time the masthead changed, the name changed, and frankly, the entire technology landscape changed with them. But I did not want to relaunch this blog with a ceremonial hello-world post, so instead, this first post from &lt;b&gt;The Workbench&lt;/b&gt; starts out the way many of my posts have over the years: with a question.&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The question comes up in just about every architecture review I sit in these days, whether in my day job as CTO or in conversations with folks in the community, and it goes something like this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&quot;Should we be using MCP for our Copilot Studio agents?&quot;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I will be the first to tell you this is the wrong question. And rather than argue the point in the abstract, I built the same agent twice to prove it. Let&#39;s see how it&#39;s done!&lt;/p&gt;

&lt;h2&gt;Background&lt;/h2&gt;

&lt;p&gt;Model Context Protocol -- MCP for short, introduced by the folks at Anthropic -- has gone from curiosity to checklist item in record time, much like microservices did a few years back, and just like then, everyone wants to know where it fits before anyone has agreed on when it should. So, I put together a lab around a fictional but very realistic engineering program called &lt;strong&gt;Project Orion&lt;/strong&gt;: a customer-facing web application mid-way through a major release cycle, with its operational truth split across two systems, the way it always is in real life:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Issues&lt;/strong&gt; holds the code-level activity: 35 issues covering bugs, feature work, blockers, and performance problems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A SQL Server database&lt;/strong&gt; holds the delivery truth: five sprints of history, 70 work items, and 30 days of health metrics, including a release readiness score, critical bug counts, blocker counts, and velocity trend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, the seeded data tells a story on purpose. Sprint 3 completed 38 of 45 points. Sprint 4 dropped to 33 of 44 with 11 points of rollover. The latest health snapshot shows a release readiness score of 52, three critical bugs, three blockers, and a velocity trend of Declining. Suffice to say, this project is quietly going sideways, and the signals are scattered across two systems that do not talk to each other.&lt;/p&gt;

&lt;p&gt;With the stage set, I built the same agent twice in Microsoft Copilot Studio and asked both versions the same questions.&lt;/p&gt;

&lt;h2&gt;Version 1: the connector agent&lt;/h2&gt;

&lt;p&gt;The first agent uses nothing but the GitHub connector. Ten minutes of work, most of which is authentication, and here is the part that surprises people every time I demo it: this agent is &lt;strong&gt;good&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ask it &quot;What critical bugs are open in Project Orion?&quot; and it answers cleanly, with issue numbers, titles, and labels. Ask it &quot;What issues are labeled as blocked?&quot; and it nails that one too. For questions whose answer lives entirely inside GitHub, the connector agent is fast, correct, and required exactly zero infrastructure from me -- no server to provision, nothing to patch, and it sits comfortably inside the tenant&#39;s existing governance and data loss prevention story, which your IT department will certainly appreciate.&lt;/p&gt;

&lt;p&gt;Now, that&#39;s all cool, but then you ask it the ONE question your VP actually cares about: &lt;em&gt;&quot;What is blocking the Project Orion release?&quot;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And there it is -- the ceiling! The agent can see three critical bugs in GitHub, so it reports three critical bugs. It cannot see that the release readiness score sits at 52 and falling, it cannot see that velocity has declined two sprints in a row, and it cannot see that two of the blocked work items have been stalled for over a week. Keep in mind, its answer is not wrong. It is something worse than wrong: it is &lt;strong&gt;confidently incomplete&lt;/strong&gt;. A stakeholder reading that answer would walk away thinking the release has three bugs to fix, when the data says the release is structurally at risk.&lt;/p&gt;

&lt;p&gt;In the lab version of this agent I made the limitation explicit, and I recommend you do the same for any single-source agent you ship. Its instructions include the following line:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;&quot;I can only see GitHub Issues data. For full project health including sprint metrics and release readiness, you would need access to the project health database.&quot;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;An agent that states its blind spots is an agent you can trust. An agent that answers everything with whatever fraction of the truth it can reach is a liability wearing a chat interface. It just doesn&#39;t work!&lt;/p&gt;

&lt;h2&gt;Version 2: the MCP agent&lt;/h2&gt;

&lt;p&gt;The second agent is the same Copilot Studio agent rebuilt on two MCP servers: the GitHub MCP Server for issue data, and a custom TypeScript MCP server I wrote that fronts the Project Orion SQL database.&lt;/p&gt;

&lt;p&gt;The custom server exposes seven tools, and I would encourage you to study the tool list closely, because this is where the real design work lives. These are not generic &quot;run a query&quot; endpoints; each one answers a question a human being would actually ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get_current_sprint&lt;/code&gt;: the active sprint with planned versus completed points&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_sprint_history&lt;/code&gt;: velocity and completion history across completed sprints&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_critical_work_items&lt;/code&gt;: open Critical and High priority items, including blocked ones&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_work_items_by_sprint&lt;/code&gt;: work items for a sprint, optionally filtered by status&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_latest_health_metrics&lt;/code&gt;: the current health snapshot, readiness score included&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_health_metrics_trend&lt;/code&gt;: readiness score over the last N days&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_stalled_work_items&lt;/code&gt;: items sitting in Active or New with no movement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two implementation details will save you a considerable amount of head scratching, so pay close attention. First, as it turns out, &lt;strong&gt;Copilot Studio speaks HTTP, not stdio&lt;/strong&gt;. Most MCP tutorials out there show the stdio transport, because that is what desktop clients use, but Copilot Studio requires the streamable HTTP transport running in stateless mode, with a fresh transport and server instance created for each incoming request. For brevity sake, I am only showing the shape of it:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Copilot Studio requires HTTP transport in stateless mode
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined, // stateless: critical for Copilot Studio
});
// a fresh server + transport pair is created per incoming request
app.post(&#39;/mcp&#39;, async (req, res) =&amp;gt; {
  const server = buildProjectOrionServer();
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Second, validate your tool inputs. The model -- not you, not your code -- is choosing the parameter values at runtime, so every tool takes a schema, not a prayer:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;server.tool(
  &#39;get_health_metrics_trend&#39;,
  &#39;Returns the release readiness score trend over the last N days &#39; +
  &#39;to show whether project health is improving or declining.&#39;,
  { days: z.number().optional().default(14) },
  async ({ days }) =&amp;gt; {
    const rows = await getHealthMetricsTrend(days);
    return { content: [{ type: &#39;text&#39;, text: JSON.stringify(rows) }] };
  }
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few things to note before moving on. That tool description string is not documentation for humans; it is the interface the model reasons over when deciding which tools to call and in what combination. Write your tool descriptions the way you would write requirements for a sharp new team member, because for all practical purposes, that is exactly what they are.&lt;/p&gt;

&lt;h2&gt;The moment it earns its keep&lt;/h2&gt;

&lt;p&gt;Now ask the rebuilt agent the same question: &lt;em&gt;&quot;What is blocking the Project Orion release?&quot;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This time the agent calls &lt;code&gt;get_critical_work_items&lt;/code&gt; and &lt;code&gt;get_latest_health_metrics&lt;/code&gt; on the SQL side, pulls the open critical issues from the GitHub side, and then does the thing no single-source agent can do: it correlates. The three critical GitHub bugs map to blocked work items in the sprint; the blockers explain the 11 points of rollover in Sprint 4; the rollover explains the declining velocity trend; and all of it together explains a readiness score of 52. And voila! The answer reads like something a good release manager would write, complete with a one-line risk summary at the end, because the agent finally has the same field of vision the release manager has.&lt;/p&gt;

&lt;p&gt;Ask it &quot;Are there any patterns between our open GitHub issues and our sprint health?&quot; and it produces an answer that did not have a data path to exist in Version 1. That is the difference, and it is worth being precise about it: not better answers to the old questions, but answers to questions that were previously UNANSWERABLE.&lt;/p&gt;

&lt;h2&gt;What MCP costs you&lt;/h2&gt;

&lt;p&gt;Here is the part the hype conveniently skips, and the part I lean on hardest whenever I present this material: everything above has a bill attached.&lt;/p&gt;

&lt;p&gt;With the connector agent, Microsoft runs the integration. With the MCP agent, I run the integration. In the lab, the server is a TypeScript process on my laptop reached through an Azure Dev Tunnel, which is perfectly fine for a demo and perfectly irresponsible for production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; the moment this architecture goes to production, you own hosting, authentication, TLS, availability, logging, patching the SDK, and the security review for a brand-new surface area that reaches directly into a database. The connector came pre-governed; your MCP server is governed by whatever you remembered to build.&lt;/p&gt;

&lt;p&gt;So the honest comparison is not &quot;MCP versus connector.&quot; It is &quot;cross-source reasoning versus operational simplicity.&quot; Sometimes that trade is spectacular, and sometimes it amounts to buying yourself a server bill to answer questions a connector already answered. It is your job as an architect to know which of the two situations you are standing in.&lt;/p&gt;

&lt;h2&gt;The decision signal&lt;/h2&gt;

&lt;p&gt;After building both versions side by side, the rule I trust fits in one sentence:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;When your agent needs to reason across sources it did not know about at design time, that is your MCP signal.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;In practice, I walk through the following four questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Does the answer live in one system?&lt;/strong&gt; Use the connector. You are done, and you inherited Microsoft&#39;s operations team for free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does the answer require correlating two or more systems in a single reasoning pass?&lt;/strong&gt; That is your MCP signal. No amount of prompt engineering will give a single-source agent data it cannot reach.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Will the source list grow?&lt;/strong&gt; MCP servers compose. Adding a third source to the Orion agent is another server registration, not a rebuild.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can you operate a service?&lt;/strong&gt; If the answer is no, that is not a character flaw, but keep in mind the connector ceiling is your ceiling until that answer changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Build it yourself&lt;/h2&gt;

&lt;p&gt;Everything in this post is reproducible from the lab I published for my Community Summit NA 2026 session in Nashville, &lt;em&gt;&quot;Same Agent, Two Architectures: When MCP Wins (And When It Doesn&#39;t)&quot;&lt;/em&gt;. The repository contains the SQL schema and seed data, a script that creates all 35 GitHub issues, scaffold prompts for the MCP server and a Next.js health dashboard, and the Copilot Studio prompts for both agents -- including the intentionally limited one:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/dgpblogster/project-orion-lab&quot;&gt;github.com/dgpblogster/project-orion-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code&lt;/a&gt;, &lt;a href=&quot;https://nodejs.org/&quot;&gt;Node 18+&lt;/a&gt;, &lt;a href=&quot;https://www.microsoft.com/en-us/sql-server/sql-server-downloads&quot;&gt;SQL Server Express&lt;/a&gt;, the &lt;a href=&quot;https://cli.github.com/&quot;&gt;GitHub CLI&lt;/a&gt;, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started&quot;&gt;Azure Dev Tunnels&lt;/a&gt;, and a &lt;a href=&quot;https://copilotstudio.microsoft.com&quot;&gt;Copilot Studio license&lt;/a&gt; -- a trial works just fine. Budget an afternoon, and make sure you build toward the moment where you ask both agents the release-blocker question back to back, because watching one agent hit the ceiling and the other reason straight through it will teach you more about agent architecture than any slide deck, including mine. The effort is well worth it!&lt;/p&gt;

&lt;p&gt;If there is interest, my next installment will walk through hardening this MCP server for real production use -- authentication, hosting, and telemetry -- so please drop a note in the comments if you would like to see that. It is good to be back at the bench.&lt;/p&gt;

&lt;p&gt;Until next post!&lt;/p&gt;

&lt;p&gt;MG.-&lt;br /&gt;Mariano Gomez Bent&lt;br /&gt;Former Microsoft BizApps MVP&amp;nbsp;&amp;nbsp;&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/1641915574014671123/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/1641915574014671123' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1641915574014671123'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1641915574014671123'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2026/08/same-agent-two-architectures-when-mcp.html' title='Same Agent, Two Architectures: When MCP Wins (and When It Doesn&#39;t)'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/a/AVvXsEhkrl_litjnFdt-K1B-RXfDbnqeOFbPKUavvAqZ_s8W6v2QSwg2rdSYO-u3tGq2fkly3hmI53LiaSHhAR0bP54Ge2JZkylSDT82dIO7QbcTWSuuoy5o2aZMLBPViSW7-O38QTAfPS920iYhLkpxmKWlEgLNqXq_kT3QY-sg2HqZogzoEt-z_Mz0kAG5BP9j=s72-c" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-8900068900384765487</id><published>2021-08-12T15:09:00.000-04:00</published><updated>2026-08-19T10:59:34.591-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ALM"/><category scheme="http://www.blogger.com/atom/ns#" term="Azure Application Insights"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><category scheme="http://www.blogger.com/atom/ns#" term="Telemetry"/><title type='text'>Power Apps - Application Monitoring with Azure Application Insights</title><content type='html'>In this video, learn to enable Power Apps Application Monitoring with Azure Application Insights.&amp;nbsp;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style=&quot;text-align: center;&quot;&gt;&lt;iframe allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;480&quot; src=&quot;https://www.youtube.com/embed/nISv1JHNRQY&quot; title=&quot;YouTube video player&quot; width=&quot;640&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;The following topics are covered in this video:&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=0s&quot;&gt;0:00&lt;/a&gt;) - Intro&amp;nbsp;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=36s&quot;&gt;0:36&lt;/a&gt;) - What is Azure Application Insights&amp;nbsp;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=92s&quot;&gt;1:32&lt;/a&gt;) - The timesheet app&amp;nbsp;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=132s&quot;&gt;2:12&lt;/a&gt;) - Creating an Application Insight for the timesheet application&amp;nbsp;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=235s&quot;&gt;3:55&lt;/a&gt;) - Identify and copy the application instrumentation key&amp;nbsp;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=249s&quot;&gt;4:09&lt;/a&gt;) - Enabling the timesheet application with the instrumentation key&amp;nbsp;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=332s&quot;&gt;5:32&lt;/a&gt;) - Working with Application Insights&amp;nbsp;&lt;/div&gt;&lt;div&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=nISv1JHNRQY&amp;amp;t=486s&quot;&gt;8:06&lt;/a&gt;) - Outro&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;For additional information on Azure Application Insights, please follow these resources:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Microsoft Docs - What is Azure Application Insights? &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview&quot;&gt;here&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;Adam Marczak - Azure for Everyone: Azure Application Insights Tutorial, &lt;a href=&quot;https://youtu.be/A0jAeGf2zUQ&quot;&gt;here&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;To check out my YouTube channel visit:
&lt;a href=&quot;https://www.youtube.com/@workbench-blog&quot;&gt;https://www.youtube.com/@workbench-blog&lt;/a&gt;
&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;LIKE AND SUBSCRIBE!!!&lt;br /&gt;&lt;/b&gt;
&lt;br /&gt;
Until next post!&lt;br /&gt;
&lt;br /&gt;
MG.-&lt;br /&gt;
Mariano Gomez, MVP
&lt;br /&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/8900068900384765487/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/8900068900384765487' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8900068900384765487'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/8900068900384765487'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2021/08/power-apps-application-monitoring-with.html' title='Power Apps - Application Monitoring with Azure Application Insights'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/nISv1JHNRQY/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-1316020312829870925</id><published>2021-08-03T15:46:00.000-04:00</published><updated>2026-08-19T10:59:35.642-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ALM"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><category scheme="http://www.blogger.com/atom/ns#" term="Visual Studio"/><title type='text'>Power Platform ALM - Visual Studio Code Extensions</title><content type='html'>&lt;p&gt;Learn about Power Platform extension for Visual Studio Code and how this toolset is improving both the code first developer experience and strengthening the ALM story.&amp;nbsp;&lt;/p&gt;&lt;br /&gt;&lt;div style=&quot;text-align: center;&quot;&gt;&lt;iframe allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;480&quot; src=&quot;https://www.youtube.com/embed/vfwYY6Aexeg&quot; title=&quot;YouTube video player&quot; width=&quot;640&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;The following topics are covered:&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=0s&quot;&gt;0:00&lt;/a&gt;) - Intro&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=76s&quot;&gt;1:16&lt;/a&gt;) - Installing the Power Platform extensions for Visual Studio Code&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=133s&quot;&gt;2:13&lt;/a&gt;) - Working with the Power Platform CLI inside of Visual Studio Code&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=222s&quot;&gt;3:42&lt;/a&gt;) - Authenticating to your environment from Power Platform CLI&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=438s&quot;&gt;7:18&lt;/a&gt;) - Saving your canvas app to your local machine&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=535s&quot;&gt;8:55&lt;/a&gt;) - Unpacking your canvas app&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=619s&quot;&gt;10:19&lt;/a&gt;) - Using Visual Studio Code to make changes to your application&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=710s&quot;&gt;11:50&lt;/a&gt;) - Packing and uploading your application changes&amp;nbsp;&lt;/li&gt;&lt;li&gt;(&lt;a href=&quot;https://www.youtube.com/watch?v=vfwYY6Aexeg&amp;amp;t=835s&quot;&gt;13:55&lt;/a&gt;) - Outro&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;
To check out my YouTube channel visit:
&lt;a href=&quot;https://www.youtube.com/@workbench-blog&quot;&gt;https://www.youtube.com/@workbench-blog&lt;/a&gt;
&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;LIKE AND SUBSCRIBE!!!&lt;br /&gt;&lt;/b&gt;
&lt;br /&gt;
Until next post!&lt;br /&gt;
&lt;br /&gt;
MG.-&lt;br /&gt;
Mariano Gomez, MVP
&lt;br /&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/1316020312829870925/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/1316020312829870925' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1316020312829870925'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/1316020312829870925'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2021/08/power-platform-alm-visual-studio-code.html' title='Power Platform ALM - Visual Studio Code Extensions'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/vfwYY6Aexeg/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5285970135510371565.post-2012731729105666213</id><published>2021-08-02T17:31:00.000-04:00</published><updated>2026-08-19T10:59:36.588-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Power Apps"/><category scheme="http://www.blogger.com/atom/ns#" term="Power Automate"/><title type='text'>Power Automate | Passing Collections from Power Automate to Power Apps (Revisited)</title><content type='html'>I revisit an old video I did couple years ago showing how to pass collections from Power Automate to Power Apps.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;div style=&quot;text-align: center;&quot;&gt;&lt;iframe allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;480&quot; src=&quot;https://www.youtube.com/embed/qUmi3JwjLfI&quot; title=&quot;YouTube video player&quot; width=&quot;640&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;This time around, I show how to leverage arrays to accomplish the same. This time around you will learn about:&lt;div&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/maker/canvas-apps/using-logic-flows&quot; target=&quot;_blank&quot;&gt;Power Apps trigger in Power Automate&lt;/a&gt;&amp;nbsp;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/connectors/sql/&quot; target=&quot;_blank&quot;&gt;Get Rows SQL action&lt;/a&gt;&amp;nbsp;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/power-automate/create-variable-store-values&quot; target=&quot;_blank&quot;&gt;Variables action&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/power-automate/data-operations&quot; target=&quot;_blank&quot;&gt;Select action and Arrays&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Response action&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/maker/canvas-apps/create-update-collection&quot; target=&quot;_blank&quot;&gt;Power Apps Collections&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;To check out my YouTube channel visit:
&lt;a href=&quot;https://www.youtube.com/@workbench-blog&quot;&gt;https://www.youtube.com/@workbench-blog&lt;/a&gt;
&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;div&gt;&lt;b&gt;LIKE AND SUBSCRIBE!!!&lt;br /&gt;&lt;/b&gt;
&lt;br /&gt;
Until next post!&lt;br /&gt;
&lt;br /&gt;
MG.-&lt;br /&gt;
Mariano Gomez, MVP</content><link rel='replies' type='application/atom+xml' href='http://www.theworkbench.blog/feeds/2012731729105666213/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5285970135510371565/2012731729105666213' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/2012731729105666213'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5285970135510371565/posts/default/2012731729105666213'/><link rel='alternate' type='text/html' href='http://www.theworkbench.blog/2021/08/power-automate-passing-collections-from.html' title='Power Automate | Passing Collections from Power Automate to Power Apps (Revisited)'/><author><name>Mariano Gomez</name><uri>http://www.blogger.com/profile/13267738662239812289</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='//blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiE6oIsiofbLlE_uzUGLv64DYdGPmN9xurCqrbDNWs_axq3WbN9RRlG9IYNI50LMAK96HtSeFIVKWTjqw_URFhfBlHzutaAfUXMkUvW4RK9sO0z6Rt8YyNaETF0D4MdiQ/s113/Mariano+Gomez+-+Square+Headshot+%28website%29.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/qUmi3JwjLfI/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry></feed>