<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.kelvinbytes.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.kelvinbytes.com/" rel="alternate" type="text/html" /><updated>2026-06-08T00:06:08+00:00</updated><id>https://blog.kelvinbytes.com/feed.xml</id><title type="html">Kelvin Bytes</title><subtitle>Six one east and half dozen west</subtitle><author><name>Kelvin Shen</name></author><entry><title type="html">Custom D365 Agent</title><link href="https://blog.kelvinbytes.com/2026-04-27-custom-365-agent.html" rel="alternate" type="text/html" title="Custom D365 Agent" /><published>2026-04-27T20:00:00+00:00</published><updated>2026-04-27T20:00:00+00:00</updated><id>https://blog.kelvinbytes.com/custom-365-agent</id><content type="html" xml:base="https://blog.kelvinbytes.com/2026-04-27-custom-365-agent.html"><![CDATA[<h2 id="the-problem">The Problem</h2>

<p>The out-of-the-box D365 Copilot is pretty limited because its data context is restricted to the current form. For a patient management system, clinicians need an AI assistant that can:</p>

<ul>
  <li><strong>Summarise communication history</strong> across emails, notes, phone calls, and appointments</li>
  <li><strong>Fetch related patient information</strong> from multiple tables (conditions, medications, care plans, referrals)</li>
  <li><strong>Recommend next steps</strong> based on clinical context and protocols</li>
</ul>

<p>This requires richer context than the built-in copilot provides.</p>

<h2 id="scenario-patient-communication-agent">Scenario: Patient Communication Agent</h2>

<p>A clinician opens a Patient record in a Model-Driven App. In a side pane, a custom copilot agent:</p>
<ol>
  <li>Automatically receives the current patient’s record ID</li>
  <li>Can query Dataverse for all communication activities (emails, phone calls, appointments, notes)</li>
  <li>Summarises the communication timeline</li>
  <li>Fetches related clinical data (conditions, medications, care team)</li>
  <li>Recommends next actions (e.g., “Patient hasn’t been contacted in 30 days - consider a follow-up call”)</li>
</ol>

<hr />

<h2 id="architecture-options">Architecture Options</h2>

<p>There are <strong>two viable approaches</strong> to build this. I recommend <strong>Approach B</strong> for production solutions today, while <strong>Approach A</strong> is worth watching for the future.</p>

<hr />

<h2 id="approach-a-native-agent-apis-future---preview-only">Approach A: Native Agent APIs (Future - Preview Only)</h2>

<p>Since July 2025, Power Apps provides native <code class="language-plaintext highlighter-rouge">Xrm.Copilot</code> and PCF <code class="language-plaintext highlighter-rouge">context.Copilot</code> APIs that directly invoke Copilot Studio topics from within MDA - no separate authentication, no Direct Line token, no external SDK needed.</p>

<blockquote>
  <p><strong>⚠️ Note:</strong> As of June 2026, this feature is still in <strong>Public Preview</strong> and is <strong>not suitable for production solutions</strong>. Preview features may change, have limited SLA, and are not covered by Microsoft support. Monitor the <a href="https://learn.microsoft.com/en-us/power-platform/release-plans/">release planner</a> for GA announcements. Once GA, this becomes the recommended approach.</p>
</blockquote>

<p><strong>Reference:</strong> <a href="https://dianabirkelbach.wordpress.com/2025/07/02/pcf-%f0%9f%a9%b7-copilot-studio-first-look-at-agent-apis/">PCF + Copilot Studio: First Look at Agent APIs (Diana Birkelbach, July 2025)</a></p>

<h3 id="prerequisites">Prerequisites</h3>

<ul>
  <li>Use <a href="https://make.preview.powerapps.com/">https://make.preview.powerapps.com/</a></li>
  <li>Early release cycle environment</li>
  <li>Configure an <strong>App assistant agent</strong> (formerly “Interactive Agent”) in your MDA (App designer &gt; Agents tab &gt; App assistant agent &gt; Configure in Copilot Studio)</li>
  <li>Docs: <a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/add-agents-to-app">Add agents to your app</a></li>
  <li>Docs: <a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/add-app-assistant-agent">Add app assistant agent</a></li>
</ul>

<h3 id="frontend-architecture">Frontend Architecture</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────┐
│  Model-Driven App - Patient Form                    │
│                                                     │
│  ┌─────────────────────┐  ┌──────────────────────┐ │
│  │  Patient Form        │  │  Side Pane           │ │
│  │  (Main Content)      │  │  ┌────────────────┐  │ │
│  │                      │  │  │ Custom Page     │  │ │
│  │  Name: John Smith    │  │  │ ┌────────────┐ │  │ │
│  │  DOB: 1985-03-15     │  │  │ │ PCF Chat   │ │  │ │
│  │  Conditions: ...     │  │  │ │ Component  │ │  │ │
│  │                      │  │  │ │            │ │  │ │
│  │  [Open Agent] btn    │  │  │ │ Uses       │ │  │ │
│  │                      │  │  │ │ context.   │ │  │ │
│  │                      │  │  │ │ Copilot    │ │  │ │
│  │                      │  │  │ └────────────┘ │  │ │
│  │                      │  │  └────────────────┘  │ │
│  └─────────────────────┘  └──────────────────────┘ │
└─────────────────────────────────────────────────────┘
</code></pre></div></div>

<h3 id="the-api">The API</h3>

<p>Two methods are available:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Execute a specific topic by registered Event Name</span>
<span class="nx">Xrm</span><span class="p">.</span><span class="nx">Copilot</span><span class="p">.</span><span class="nx">executeEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">PatientAgent.SummariseCommunication</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">patientId</span><span class="p">:</span> <span class="dl">"</span><span class="s2">00000000-0000-0000-0000-000000000001</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">scope</span><span class="p">:</span> <span class="dl">"</span><span class="s2">last30days</span><span class="dl">"</span>
<span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// response contains text + attachments (Adaptive Cards, etc.)</span>
<span class="p">});</span>

<span class="c1">// Execute based on natural language trigger queries</span>
<span class="nx">Xrm</span><span class="p">.</span><span class="nx">Copilot</span><span class="p">.</span><span class="nx">executePrompt</span><span class="p">(</span><span class="dl">"</span><span class="s2">Summarise recent communications for this patient</span><span class="dl">"</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="cm">/* render response */</span> <span class="p">});</span>
</code></pre></div></div>

<p>Within a PCF, use <code class="language-plaintext highlighter-rouge">context.Copilot</code> instead of <code class="language-plaintext highlighter-rouge">Xrm.Copilot</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Inside PCF updateView or a button handler</span>
<span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">Copilot</span><span class="p">.</span><span class="nx">executeEvent</span><span class="p">(</span><span class="dl">"</span><span class="s2">PatientAgent.SummariseCommunication</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">patientId</span><span class="p">:</span> <span class="nx">recordId</span>
<span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">renderAgentResponse</span><span class="p">(</span><span class="nx">response</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="passing-parameters">Passing Parameters</h3>

<p>The Agent API automatically provides the current form’s <code class="language-plaintext highlighter-rouge">entityName</code> and <code class="language-plaintext highlighter-rouge">recordId</code> via global variables:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Global.PA__Copilot_Model_PageContext.pageContext.id.guid</code></li>
  <li><code class="language-plaintext highlighter-rouge">Global.PA__Copilot_Model_PageContext.pageContext.entityTypeName</code></li>
</ul>

<p>For custom parameters, pass a JSON object as the second argument to <code class="language-plaintext highlighter-rouge">executeEvent</code>. In Copilot Studio, parse <code class="language-plaintext highlighter-rouge">Activity.Value</code> using the “Parse value” node.</p>

<h3 id="pros--cons-of-approach-a">Pros &amp; Cons of Approach A</h3>

<p><strong>Pros:</strong></p>
<ul>
  <li>No Azure Entra app registration needed</li>
  <li>No token endpoint management</li>
  <li>No external SDK dependencies</li>
  <li>Native integration - the platform handles auth</li>
  <li>Automatic form context (entity name + record ID)</li>
  <li>Works directly in PCF via <code class="language-plaintext highlighter-rouge">context.Copilot</code></li>
</ul>

<p><strong>Cons:</strong></p>
<ul>
  <li>Requires early release cycle environment (as of mid-2025, may be GA by now)</li>
  <li>Must use “App assistant agent” (not a standalone Copilot Studio agent)</li>
  <li>Limited to <code class="language-plaintext highlighter-rouge">executeEvent</code> and <code class="language-plaintext highlighter-rouge">executePrompt</code> - no streaming/conversational chat UI out-of-the-box</li>
  <li>For a full chat experience, you still need to build your own chat UI in the PCF</li>
</ul>

<hr />

<h2 id="approach-b-custom-canvas-via-direct-line-recommended-for-production">Approach B: Custom Canvas via Direct Line (Recommended for Production)</h2>

<p>This approach uses the Bot Framework Web Chat SDK to embed a full conversational chat interface powered by a standalone Copilot Studio agent. All components are GA and production-supported.</p>

<h3 id="frontend">Frontend</h3>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/create-app-side-panes">D365 side panes by using a client API</a> ✅ Link valid</li>
  <li><a href="https://dianabirkelbach.wordpress.com/2021/10/24/side-panes-custom-pages-implement-a-shopping-cart-in-mda/">Custom Pages in Side Panes (Diana Birkelbach)</a> ✅ Link valid</li>
  <li>Inside the Custom Page, build a PCF as the chat interface using <a href="https://github.com/microsoft/BotFramework-WebChat">Bot Framework Web Chat</a></li>
  <li>Use the Copilot Studio <strong>Token Endpoint</strong> + Direct Line to connect</li>
  <li>Register an Azure Entra app for SSO authentication</li>
  <li>Docs: <a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/customize-default-canvas">Customize the default canvas</a></li>
  <li>Docs: <a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/configure-sso">Configure SSO with Microsoft Entra ID</a></li>
  <li>Samples: <a href="https://github.com/microsoft/CopilotStudioSamples">CopilotStudioSamples on GitHub</a></li>
</ul>

<h3 id="passing-record-id-to-the-custom-page">Passing Record ID to the Custom Page</h3>

<p>Yes! The <code class="language-plaintext highlighter-rouge">navigateTo</code> and <code class="language-plaintext highlighter-rouge">pane.navigate</code> APIs support passing <code class="language-plaintext highlighter-rouge">recordId</code> and <code class="language-plaintext highlighter-rouge">entityName</code> to a Custom Page:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// From form scripting - open side pane with patient context</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">openPatientAgent</span><span class="p">(</span><span class="nx">formContext</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">patientId</span> <span class="o">=</span> <span class="nx">formContext</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">entity</span><span class="p">.</span><span class="nx">getId</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">{}</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="dl">""</span><span class="p">);</span>
    
    <span class="kd">const</span> <span class="nx">pane</span> <span class="o">=</span> <span class="nx">Xrm</span><span class="p">.</span><span class="nx">App</span><span class="p">.</span><span class="nx">sidePanes</span><span class="p">.</span><span class="nx">getPane</span><span class="p">(</span><span class="dl">"</span><span class="s2">PatientAgent</span><span class="dl">"</span><span class="p">)</span> 
        <span class="o">??</span> <span class="k">await</span> <span class="nx">Xrm</span><span class="p">.</span><span class="nx">App</span><span class="p">.</span><span class="nx">sidePanes</span><span class="p">.</span><span class="nx">createPane</span><span class="p">({</span>
            <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Patient Agent</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">imageSrc</span><span class="p">:</span> <span class="dl">"</span><span class="s2">WebResources/agent_icon</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">paneId</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PatientAgent</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">canClose</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
            <span class="na">width</span><span class="p">:</span> <span class="mi">450</span>
        <span class="p">});</span>
    
    <span class="nx">pane</span><span class="p">.</span><span class="nx">navigate</span><span class="p">({</span>
        <span class="na">pageType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">custom</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">cr123_patientagentpage</span><span class="dl">"</span><span class="p">,</span>  <span class="c1">// logical name of your custom page</span>
        <span class="na">entityName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">contact</span><span class="dl">"</span><span class="p">,</span>           <span class="c1">// available via Param("entityName")</span>
        <span class="na">recordId</span><span class="p">:</span> <span class="nx">patientId</span>              <span class="c1">// available via Param("recordId")</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the Custom Page (Canvas App), read the parameters:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Power Fx in Custom Page
Set(varPatientId, Param("recordId"));
Set(varEntityName, Param("entityName"));
</code></pre></div></div>

<h3 id="token-endpoint--direct-line-connection-in-pcf">Token Endpoint &amp; Direct Line Connection (in PCF)</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// PCF component - initialize Web Chat</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">initializeChat</span><span class="p">(</span><span class="nx">tokenEndpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 1. Get token from Copilot Studio token endpoint</span>
    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">tokenEndpoint</span><span class="p">);</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">token</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
    
    <span class="c1">// 2. Get regional Direct Line URL</span>
    <span class="kd">const</span> <span class="nx">envEndpoint</span> <span class="o">=</span> <span class="nx">tokenEndpoint</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">tokenEndpoint</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">"</span><span class="s2">/powervirtualagents</span><span class="dl">"</span><span class="p">));</span>
    <span class="kd">const</span> <span class="nx">apiVersion</span> <span class="o">=</span> <span class="nx">tokenEndpoint</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">tokenEndpoint</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">"</span><span class="s2">api-version</span><span class="dl">"</span><span class="p">)).</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">=</span><span class="dl">"</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
    <span class="kd">const</span> <span class="nx">settingsUrl</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">envEndpoint</span><span class="p">}</span><span class="s2">/powervirtualagents/regionalchannelsettings?api-version=</span><span class="p">${</span><span class="nx">apiVersion</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
    
    <span class="kd">const</span> <span class="nx">settingsResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">settingsUrl</span><span class="p">);</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">channelUrlsById</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">settingsResponse</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">directLineUrl</span> <span class="o">=</span> <span class="nx">channelUrlsById</span><span class="p">.</span><span class="nx">directline</span><span class="p">;</span>
    
    <span class="c1">// 3. Create Direct Line connection</span>
    <span class="kd">const</span> <span class="nx">directLine</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">WebChat</span><span class="p">.</span><span class="nx">createDirectLine</span><span class="p">({</span>
        <span class="na">domain</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">directLineUrl</span><span class="p">}</span><span class="s2">v3/directline`</span><span class="p">,</span>
        <span class="na">token</span><span class="p">:</span> <span class="nx">token</span>
    <span class="p">});</span>
    
    <span class="c1">// 4. Render Web Chat</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">WebChat</span><span class="p">.</span><span class="nx">renderWebChat</span><span class="p">(</span>
        <span class="p">{</span> <span class="nx">directLine</span><span class="p">,</span> <span class="nx">styleOptions</span> <span class="p">},</span>
        <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">webchat</span><span class="dl">"</span><span class="p">)</span>
    <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="sso-configuration">SSO Configuration</h3>

<p>To avoid the user being prompted to sign in again inside the chat:</p>
<ol>
  <li>Create an <strong>authentication app registration</strong> for user auth in Copilot Studio</li>
  <li>Create a <strong>canvas app registration</strong> (SPA) for your custom page/PCF</li>
  <li>Configure Token Exchange URL in Copilot Studio security settings</li>
  <li>Use MSAL in your PCF to acquire a token and pass it to Web Chat</li>
</ol>

<p>Reference: <a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/configure-sso">Configure SSO with Entra ID</a></p>

<h3 id="pros--cons-of-approach-b">Pros &amp; Cons of Approach B</h3>

<p><strong>Pros:</strong></p>
<ul>
  <li>Full chat UI with conversation history, typing indicators, Adaptive Cards</li>
  <li>Works with any standalone Copilot Studio agent</li>
  <li>No dependency on preview features</li>
  <li>More flexible - can use any Web Chat customization</li>
</ul>

<p><strong>Cons:</strong></p>
<ul>
  <li>More complex setup (Entra app registration, token management, SSO)</li>
  <li>External SDK dependency (Bot Framework Web Chat)</li>
  <li>Must manually pass context (record ID) via activity payload</li>
  <li>More moving parts to maintain</li>
</ul>

<hr />

<h2 id="backend-copilot-studio-agent-design">Backend: Copilot Studio Agent Design</h2>

<p>Regardless of which frontend approach you choose, the Copilot Studio agent design is the same.</p>

<h3 id="topics-to-build">Topics to Build</h3>

<table>
  <thead>
    <tr>
      <th>Topic</th>
      <th>Trigger</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Summarise Communications</td>
      <td>Event: <code class="language-plaintext highlighter-rouge">PatientAgent.SummariseCommunication</code> or trigger phrase “summarise communications”</td>
      <td>Fetches activities (emails, phone calls, appointments, notes) for the patient and produces a summary</td>
    </tr>
    <tr>
      <td>Patient Overview</td>
      <td>Event: <code class="language-plaintext highlighter-rouge">PatientAgent.PatientOverview</code> or “patient overview”</td>
      <td>Returns demographics, conditions, medications, care team</td>
    </tr>
    <tr>
      <td>Recommend Next Steps</td>
      <td>Event: <code class="language-plaintext highlighter-rouge">PatientAgent.RecommendNextSteps</code> or “what should I do next”</td>
      <td>Analyses gaps in care, overdue follow-ups, pending referrals</td>
    </tr>
    <tr>
      <td>Search Knowledge</td>
      <td>Trigger phrase: free-text clinical questions</td>
      <td>Uses Knowledge sources (SharePoint, Dataverse) to answer clinical protocol questions</td>
    </tr>
  </tbody>
</table>

<h3 id="data-access---custom-connector-or-plugin-actions">Data Access - Custom Connector or Plugin Actions</h3>

<p>Copilot Studio topics can call:</p>

<ol>
  <li><strong>Power Automate Cloud Flows</strong> (simplest)
    <ul>
      <li>Flow receives <code class="language-plaintext highlighter-rouge">patientId</code> as input</li>
      <li>Queries Dataverse for activities: <code class="language-plaintext highlighter-rouge">GET /api/data/v9.2/activitypointers?$filter=_regardingobjectid_value eq '{patientId}'&amp;$orderby=actualend desc&amp;$top=50</code></li>
      <li>Returns structured JSON to Copilot Studio</li>
    </ul>
  </li>
  <li><strong>Custom API / Plugin Actions</strong> (more performant)
    <ul>
      <li>Register a Dataverse Custom API: <code class="language-plaintext highlighter-rouge">new_GetPatientCommunicationSummary</code></li>
      <li>Input: <code class="language-plaintext highlighter-rouge">PatientId</code> (Guid)</li>
      <li>Output: structured JSON with activities, timeline, key dates</li>
      <li>Invoke from Copilot Studio via connector</li>
    </ul>
  </li>
  <li><strong>Dataverse Knowledge Source</strong> (for grounding)
    <ul>
      <li>Add your Dataverse tables as Knowledge in Copilot Studio</li>
      <li>The agent can directly query patient-related tables</li>
    </ul>
  </li>
</ol>

<h3 id="example-flow-summarise-communications">Example Flow: Summarise Communications</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Trigger: Copilot Studio calls flow with patientId
    │
    ├─ List Activities (Dataverse connector)
    │   Filter: regardingobjectid eq patientId
    │   Select: subject, description, activitytypecode, actualstart, actualend
    │   Order: actualend desc, Top: 50
    │
    ├─ Compose: Format activities into structured text
    │   "Email (2026-04-01): Subject - Follow-up on lab results..."
    │   "Phone Call (2026-03-28): Subject - Discussed medication change..."
    │
    └─ Return: Formatted summary text to Copilot Studio
</code></pre></div></div>

<p>Copilot Studio then uses the AI model to:</p>
<ul>
  <li>Synthesise the raw activity list into a natural language summary</li>
  <li>Highlight key themes and recent interactions</li>
  <li>Generate next-step recommendations</li>
</ul>

<h3 id="generative-ai-orchestration-in-copilot-studio">Generative AI Orchestration in Copilot Studio</h3>

<p>Configure the topic to use a <strong>Generative Answers</strong> node:</p>
<ol>
  <li>Feed the structured patient data as context</li>
  <li>Use a system prompt like:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>You are a clinical communication assistant. Given the patient's recent 
communication history, provide:
1. A brief summary (3-5 sentences) of key interactions
2. Any notable patterns or concerns
3. Recommended next steps based on gaps in communication

Patient Data:
{Topic.PatientActivities}
</code></pre></div></div>

<hr />

<h2 id="approach-c-bonus-mcp-apps--custom-ui-widgets-in-m365-copilot">Approach C (Bonus): MCP Apps / Custom UI Widgets in M365 Copilot</h2>

<p>As of April 2026 (Public Preview), there’s a third option: <strong>Custom UI Widgets powered by Power Apps</strong> that render inside M365 Copilot Chat.</p>

<p><strong>Reference:</strong> <a href="https://dianabirkelbach.wordpress.com/2026/05/26/inside-built-in-and-custom-copilot-ui-widgets-powered-by-power-apps/">Inside Built-In and Custom Copilot UI Widgets powered by Power Apps (Diana Birkelbach, May 2026)</a></p>

<p>This approach:</p>
<ul>
  <li>Exports your MDA as an MCP server package</li>
  <li>Deploys to M365 Copilot via Teams sideloading</li>
  <li>Custom tools provide rich HTML UI widgets inside the chat</li>
  <li>Uses the <code class="language-plaintext highlighter-rouge">@modelcontextprotocol/ext-apps</code> npm package for host-view communication</li>
</ul>

<p><strong>When to use this:</strong> If you want the patient agent to be accessible across M365 (Outlook, Teams, Copilot Chat) rather than only inside the MDA form. It’s great for scenarios where clinicians are working in Outlook and want to quickly check a patient summary without opening D365.</p>

<p><strong>Limitation:</strong> This is in preview, requires M365 Copilot Premium license, and deployment requires Teams/M365 admin access.</p>

<hr />

<h2 id="recommended-implementation-plan">Recommended Implementation Plan</h2>

<h3 id="phase-1-foundation-week-1-2">Phase 1: Foundation (Week 1-2)</h3>

<ol>
  <li><strong>Set up environment</strong>: Early release cycle Dataverse environment</li>
  <li><strong>Create data model</strong> (if not existing): Patient (Contact), Communication Activity custom table or use OOB Activity entities</li>
  <li><strong>Create App assistant agent</strong> in MDA App designer (Agents tab &gt; App assistant agent &gt; Configure in Copilot Studio)</li>
  <li><strong>Build first topic</strong> in Copilot Studio: “Summarise Communications”
    <ul>
      <li>Trigger: Custom client event <code class="language-plaintext highlighter-rouge">PatientAgent.SummariseCommunication</code></li>
      <li>Parse <code class="language-plaintext highlighter-rouge">Activity.Value</code> for patientId</li>
      <li>Call Power Automate flow to fetch activities</li>
      <li>Use Generative Answers to produce summary</li>
    </ul>
  </li>
</ol>

<h3 id="phase-2-pcf-chat-component-week-3-4">Phase 2: PCF Chat Component (Week 3-4)</h3>

<ol>
  <li><strong>Build PCF control</strong> (React-based) with:
    <ul>
      <li>Chat message list UI (using Fluent UI v9 components)</li>
      <li>Input box for free-text prompts</li>
      <li>Button bar for predefined actions (Summarise, Overview, Next Steps)</li>
      <li>Calls <code class="language-plaintext highlighter-rouge">context.Copilot.executeEvent()</code> or <code class="language-plaintext highlighter-rouge">context.Copilot.executePrompt()</code></li>
      <li>Renders response text and Adaptive Card attachments</li>
    </ul>
  </li>
  <li><strong>Create Custom Page</strong> containing the PCF</li>
  <li><strong>Wire up Side Pane</strong> via form script on Patient form’s onLoad event</li>
</ol>

<h3 id="phase-3-enriched-agent-week-5-6">Phase 3: Enriched Agent (Week 5-6)</h3>

<ol>
  <li><strong>Add more topics</strong>: Patient Overview, Recommend Next Steps</li>
  <li><strong>Add Knowledge sources</strong>: Clinical protocols on SharePoint, Dataverse tables</li>
  <li><strong>Add Custom APIs</strong> for performant data retrieval</li>
  <li><strong>Test with real patient data</strong> (anonymised)</li>
</ol>

<h3 id="phase-4-polish--productionise-week-7-8">Phase 4: Polish &amp; Productionise (Week 7-8)</h3>

<ol>
  <li><strong>UX polish</strong>: Loading states, error handling, conversation history</li>
  <li><strong>Security review</strong>: Ensure patient data access respects Dataverse security roles</li>
  <li><strong>Performance testing</strong>: Token caching, response times</li>
  <li><strong>Deploy to production environment</strong> (once Agent APIs reach GA)</li>
</ol>

<hr />

<h2 id="key-links--references">Key Links &amp; References</h2>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>URL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Side Panes Client API</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/create-app-side-panes">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>Custom Pages in Side Panes</td>
      <td><a href="https://dianabirkelbach.wordpress.com/2021/10/24/side-panes-custom-pages-implement-a-shopping-cart-in-mda/">dianabirkelbach.wordpress.com</a></td>
    </tr>
    <tr>
      <td>navigateTo Client API (pass recordId)</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-navigation/navigateto">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>PCF + Copilot Studio Agent APIs</td>
      <td><a href="https://dianabirkelbach.wordpress.com/2025/07/02/pcf-%f0%9f%a9%b7-copilot-studio-first-look-at-agent-apis/">dianabirkelbach.wordpress.com</a></td>
    </tr>
    <tr>
      <td>Xrm.Copilot.executeEvent docs</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-copilot/executeevent">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>Xrm.Copilot.executePrompt docs</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-copilot/executeprompt">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>Add Agents to MDA (App assistant agent)</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/add-agents-to-app">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>Agent Response PCF Component</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/form-designer-add-configure-agent-response">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>Bring Intelligence using Agent APIs (PCF)</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/developer/component-framework/bring-intelligence-using-agent-apis">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>Customize Default Canvas (Direct Line)</td>
      <td><a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/customize-default-canvas">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>Configure SSO with Entra ID</td>
      <td><a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/configure-sso">learn.microsoft.com</a></td>
    </tr>
    <tr>
      <td>CopilotStudioSamples (SSO)</td>
      <td><a href="https://github.com/microsoft/CopilotStudioSamples/tree/main/sso/entra-id">github.com</a></td>
    </tr>
    <tr>
      <td>Bot Framework Web Chat</td>
      <td><a href="https://github.com/microsoft/BotFramework-WebChat">github.com</a></td>
    </tr>
    <tr>
      <td>Custom UI Widgets (MCP Apps)</td>
      <td><a href="https://dianabirkelbach.wordpress.com/2026/05/26/inside-built-in-and-custom-copilot-ui-widgets-powered-by-power-apps/">dianabirkelbach.wordpress.com</a></td>
    </tr>
    <tr>
      <td>MCP Apps Announcement</td>
      <td><a href="https://www.microsoft.com/en-us/power-platform/blog/2026/04/22/custom-tools-and-rich-ui-for-app-based-conversations-are-now-in-public-preview/">microsoft.com</a></td>
    </tr>
    <tr>
      <td>Enable App MCP &amp; Custom Widgets</td>
      <td><a href="https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/enable-your-app-copilot">learn.microsoft.com</a></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p><strong>Verdict: This is absolutely feasible.</strong> The recommended path for production is <strong>Approach B</strong> (Direct Line + Web Chat). It uses GA components (Side Panes, Custom Pages, Bot Framework Web Chat, Copilot Studio token endpoint) and gives full control over the chat experience.</p>

<p><strong>Approach A</strong> (native Agent APIs) will simplify the architecture significantly once it reaches GA - keep an eye on it. When it goes GA, you can migrate the PCF from Direct Line to <code class="language-plaintext highlighter-rouge">context.Copilot.executeEvent()</code> and drop the Entra app registration and token management entirely.</p>

<p>For cross-M365 access (Outlook, Teams), consider adding <strong>Approach C</strong> (MCP Apps) later as a complementary channel.</p>

<p><strong>Can I pass the form context record ID to the custom page?</strong> → <strong>Yes!</strong> Use <code class="language-plaintext highlighter-rouge">pane.navigate({ pageType: "custom", name: "...", entityName: "contact", recordId: patientId })</code> and read it in the Custom Page via <code class="language-plaintext highlighter-rouge">Param("recordId")</code>.</p>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="Power Platform" /><category term="AI" /><category term="Integration" /><category term="Copilot Studio" /><category term="PCF" /><category term="MDA" /><summary type="html"><![CDATA[The Problem]]></summary></entry><entry><title type="html">D365 Email Templates Dynamic Text</title><link href="https://blog.kelvinbytes.com/2026-04-13-d365-email-template-dynamic-text.html" rel="alternate" type="text/html" title="D365 Email Templates Dynamic Text" /><published>2026-04-13T20:00:00+00:00</published><updated>2026-04-13T20:00:00+00:00</updated><id>https://blog.kelvinbytes.com/d365-email-template-dynamic-text</id><content type="html" xml:base="https://blog.kelvinbytes.com/2026-04-13-d365-email-template-dynamic-text.html"><![CDATA[<p>Dynamics 365 email templates let you create reusable email content with dynamic text that auto-populates from record data. Here’s a practical guide to working with them effectively.</p>

<h2 id="entity-specific-templates">Entity-Specific Templates</h2>

<p>Entity-specific templates are tied to a particular table (entity) and can resolve fields from that table automatically via the template editor UI. However, there is a significant limitation: <strong>you cannot create entity-specific email templates for custom entities</strong> through the standard UI. This restricts their usefulness in solutions with heavy customisation.</p>

<h2 id="global-templates">Global Templates</h2>

<p>Global templates are not bound to any specific entity, which makes them more flexible. To use dynamic text with custom entities in a global template, you need to manually write the mapping syntax.</p>

<p><strong>Text fields:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{!EntityLogicalName:FieldLogicalName;}
</code></pre></div></div>

<p>Example — pulling a user’s address:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{!User:Address;}
</code></pre></div></div>

<p><strong>Lookup fields</strong> (to resolve the display name):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{!EntityLogicalName:LookupFieldLogicalName/@name;}
</code></pre></div></div>

<p><strong>Tip:</strong> If your syntax is correct, the dynamic value placeholder will turn yellow in the template editor after saving. If it stays plain text, double-check your entity and field logical names. For example, is the column doesn’t exist in the table or spelling errors.</p>

<h2 id="how-to-test-a-template">How to Test a Template</h2>

<p>There are a few approaches to validate that your template resolves correctly:</p>

<ol>
  <li>
    <p><strong>Manual test</strong> — Create an email record, insert the template, and set the regarding record. Confirm the placeholders resolve to actual values.</p>
  </li>
  <li>
    <p><strong>InstantiateTemplate action</strong> — Call the <code class="language-plaintext highlighter-rouge">InstantiateTemplate</code> Dataverse action programmatically. Tools like the <strong>Dataverse REST Builder</strong> XrmToolBox plugin can help. The workflow in REST Builder is: build the request in <em>Configure</em> mode → switch to <em>Fetch</em> mode to generate the request code → move to <em>Editor</em> mode to execute. It’s not the most intuitive flow, but it works.
<img src="../images/2026-04-14-d365-email-template-dynamic-text/rest-builder-config.png" alt="image" /></p>
  </li>
</ol>

<p><img src="../images/2026-04-14-d365-email-template-dynamic-text/rest-builder-execution.png" alt="image" /></p>

<h2 id="implementation-scenarios">Implementation Scenario(s)</h2>
<p><strong>Power Automate</strong> — Use an unbound action step in a cloud flow to call <code class="language-plaintext highlighter-rouge">InstantiateTemplate</code>. This is especially powerful for integrating with the rest of your business process or logic.</p>

<h2 id="complex-data-mapping">Complex Data Mapping</h2>

<p>When out-of-the-box dynamic text isn’t enough (e.g., you need conditional logic, formatted dates, or data from related records beyond a single lookup), use <strong>Power Automate</strong>:</p>

<ul>
  <li>Define your own placeholder syntax in the template body (e.g., ``).</li>
  <li>Build a flow that retrieves the necessary data, performs transformations, and replaces your custom placeholders before sending the email.</li>
</ul>

<p>This pattern gives you full control over the email content while still leveraging templates for the static structure.</p>

<h2 id="reference">Reference</h2>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/power-apps/user/email-dynamic-text">Email dynamic text - Microsoft Learn</a></li>
  <li>https://himbap.com/blog/?p=4541</li>
</ul>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="Power Platform" /><category term="D365" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Dynamics 365 email templates let you create reusable email content with dynamic text that auto-populates from record data. Here’s a practical guide to working with them effectively.]]></summary></entry><entry><title type="html">Managed Identity</title><link href="https://blog.kelvinbytes.com/2026-04-06-managed-identity.html" rel="alternate" type="text/html" title="Managed Identity" /><published>2026-04-06T20:00:00+00:00</published><updated>2026-04-06T20:00:00+00:00</updated><id>https://blog.kelvinbytes.com/managed-identity</id><content type="html" xml:base="https://blog.kelvinbytes.com/2026-04-06-managed-identity.html"><![CDATA[<p>Managed Identities are the natural evolution of service principals because they completely eliminate the need to manage and store secrets.</p>

<p>Integrations can be either inbound or outbound. This article will focus on outbound integration—specifically, using a Managed Identity to allow Power Platform (Dataverse) plugins to securely call external Azure resources.</p>

<h3 id="overall-goal">Overall Goal</h3>
<p>The primary purpose of configuring a Managed Identity (MI) is so that Microsoft Entra ID will trust the calling plugin assembly. This allows your custom Dataverse code to securely authenticate against external Azure services without embedding credentials.</p>

<h3 id="step-by-step-implementation">Step-by-Step Implementation</h3>

<ul>
  <li>Certificate: Generate the <code class="language-plaintext highlighter-rouge">.pfx</code> file containing the key pair.</li>
  <li>Certificate: Import the <code class="language-plaintext highlighter-rouge">.pfx</code> into the Windows certificate store.</li>
  <li>Azure: Create a user-assigned managed identity.</li>
  <li>Dataverse: Create an application user and link it to the managed identity record.</li>
  <li>Dataverse: Assign the required security roles.</li>
  <li>Development: Build the plugin <code class="language-plaintext highlighter-rouge">.dll</code>.</li>
  <li>Security: Sign the assembly using the generated certificate.</li>
  <li>Deployment: Upload the signed assembly to Dataverse.</li>
  <li>Configuration: Link the assembly with the managed identity in Dataverse.</li>
  <li>Azure: Add federated credentials and role assignments with the Azure managed identity.</li>
  <li>Validation: Test the integration.</li>
</ul>

<h3 id="generate-a-self-signed-certificate">Generate a Self-Signed Certificate</h3>
<p>Your certificate must contain a key pair (both private and public keys) to sign the assembly. Below is a PowerShell example to generate one.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$kuCodeSigning</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"1.3.6.1.5.5.7.3.3"</span><span class="p">;</span><span class="w">

</span><span class="nv">$cert</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-SelfSignedCertificate</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Type</span><span class="w"> </span><span class="s2">"CodeSigningCert"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-KeyExportPolicy</span><span class="w"> </span><span class="s2">"Exportable"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Subject</span><span class="w"> </span><span class="s2">"VivoAirManagedIdentityPlugin"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-KeyUsageProperty</span><span class="w"> </span><span class="p">@(</span><span class="s2">"Sign"</span><span class="p">)</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-KeyUsage</span><span class="w"> </span><span class="p">@(</span><span class="s2">"DigitalSignature"</span><span class="p">)</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-TextExtension</span><span class="w"> </span><span class="p">@(</span><span class="s2">"2.5.29.37={text}</span><span class="si">$(</span><span class="nv">$kuCodeSigning</span><span class="si">)</span><span class="s2">"</span><span class="p">,</span><span class="w"> </span><span class="s2">"2.5.29.19={text}false"</span><span class="p">)</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-CertStoreLocation</span><span class="w"> </span><span class="s2">"Cert:\CurrentUser\My"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-KeyLength</span><span class="w"> </span><span class="nx">2048</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-NotAfter</span><span class="w"> </span><span class="p">([</span><span class="n">DateTime</span><span class="p">]::</span><span class="n">Now.AddYears</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span><span class="w"> </span><span class="err">`</span><span class="w">
  </span><span class="nt">-Provider</span><span class="w"> </span><span class="s2">"Microsoft Software Key Storage Provider"</span><span class="p">;</span><span class="w">

</span><span class="nv">$emptyPassword</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">System.Security.SecureString</span><span class="p">]::</span><span class="n">new</span><span class="p">()</span><span class="w">

</span><span class="n">Export-PfxCertificate</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Cert</span><span class="w"> </span><span class="s2">"Cert:\CurrentUser\My\</span><span class="si">$(</span><span class="nv">$cert</span><span class="o">.</span><span class="nf">Thumbprint</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-FilePath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TEMP</span><span class="s2">\VivoAirManagedIdentityPlugin.pfx"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Password</span><span class="w"> </span><span class="nv">$emptyPassword</span><span class="p">;</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">ku</code> in <code class="language-plaintext highlighter-rouge">kuCodeSigning</code> stands for key usage. The example provided in standard <a href="https://learn.microsoft.com/en-us/power-platform/admin/set-up-managed-identity">Microsoft documentation</a> is typically for secure email signage, which is not designed for signing plugin assemblies. The OID <code class="language-plaintext highlighter-rouge">1.3.6.1.5.5.7.3.3</code> explicitly ensures it is valid for code signing.</p>

<p>You need to register the keys of the generated <code class="language-plaintext highlighter-rouge">.pfx</code> file within the Windows certificate storage. Ensure it is placed in both the Personal and Trusted Root Certification Authorities nodes for successful signing.
<img src="../images/2026-04-07-managed-identity/key-storage.png" alt="image" /></p>

<h3 id="signing-tool">Signing Tool</h3>
<p>Use the Windows SDK SignTool to apply the certificate to your assembly.
<img src="../images/2026-04-07-managed-identity/signtool-windows-sdk.png" alt="image" /></p>

<h3 id="create-a-managed-identity-record-in-dataverse">Create a Managed Identity Record in Dataverse</h3>
<p>You can establish this link by creating an application user in the Power Platform admin center.
<img src="../images/2026-04-07-managed-identity/create-managed-identity-record.png" alt="image" /></p>

<h3 id="calling-managed-identity-for-bear-token">Calling Managed Identity for Bear Token</h3>
<pre><code class="language-CSharp">        public string GetToken(List&lt;string&gt; scopes)
        {
            return _managedIdentityService.AcquireToken(scopes);
        }
</code></pre>

<h3 id="add-federated-security-in-azure">Add Federated Security in Azure</h3>
<p>When configuring the federated credentials in Azure, the subject string is used to pinpoint the intended caller plugin assembly. Note that the expected subject format has changed from v1 to v2.</p>

<h3 id="managed-identity-basics">Managed Identity Basics</h3>
<p>There are two types of Managed Identities: System-Assigned and User-Assigned. For Power Platform plugins connecting to Azure, we create and utilize a User-Assigned Managed Identity.</p>

<h2 id="references">References</h2>
<ul>
  <li><a href="https://www.clive-oldridge.com/azure/2024/10/14/set-up-managed-identity-for-power-platform-plugins.html">Set up managed identity for Power Platform Plugins</a></li>
  <li><a href="https://www.clive-oldridge.com/azure/2024/11/22/power-platform-plugin-package-managed-identity.html">Power Platform Plugin Package – Managed identity</a></li>
  <li><a href="https://learn.microsoft.com/en-us/power-platform/admin/set-up-managed-identity">Set up Power Platform managed identity for Dataverse plug-ins or plug-in packages</a></li>
  <li><a href="https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe">SignTool.exe (Sign Tool)</a></li>
  <li><a href="https://learn.microsoft.com/en-us/powershell/module/pki/new-selfsignedcertificate?view=windowsserver2025-ps#example-3">New-SelfSignedCertificate</a></li>
</ul>

<h2 id="future-posts">Future Posts</h2>
<ul>
  <li>Managed Identity with Plugin Packages</li>
  <li>Managed Identity with vNet and apim</li>
</ul>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="Power Platform" /><category term="ALM" /><category term="Azure" /><category term="Integration" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Managed Identities are the natural evolution of service principals because they completely eliminate the need to manage and store secrets.]]></summary></entry><entry><title type="html">Power Platform CICD Evolution</title><link href="https://blog.kelvinbytes.com/2025-07-04-power-platform-cicd-evolution.html" rel="alternate" type="text/html" title="Power Platform CICD Evolution" /><published>2025-07-04T00:00:00+00:00</published><updated>2025-07-04T00:00:00+00:00</updated><id>https://blog.kelvinbytes.com/power-platform-cicd-evolution</id><content type="html" xml:base="https://blog.kelvinbytes.com/2025-07-04-power-platform-cicd-evolution.html"><![CDATA[<h2 id="why-cicd-and-alm">Why CI/CD and ALM?</h2>
<p>We need CI/CD and ALM in Power Platform to move from ad‑hoc manual exports/imports to predictable, governed delivery.</p>

<h2 id="key-pillars">Key Pillars</h2>
<h3 id="development-experience">Development Experience</h3>
<ul>
  <li>Plugin Registration</li>
  <li>Codegen</li>
  <li>Unpack Solutions</li>
  <li>Pack Solutions</li>
</ul>

<h3 id="strategies">Strategies</h3>
<h4 id="solution-segregation">Solution Segregation</h4>
<p>Option 1: Split by Features
But you will still need a core solution for shared components, and if the core solution becomes too big, you need to split it.</p>

<p>Option 2: Split by Components
Customization, Plugin, Workflows and etc.</p>

<h4 id="layering">Layering</h4>
<p>The final product is like a cake and each solution is like a layer of the cakse. The solution importing sequence is important because it dictates if and how solution layer are stacked and which overwrite which.</p>

<h3 id="cicd-experience">CICD Experience</h3>
<ul>
  <li>Deployment targeting - you can target multiple systems, so called fan out. You can keep the deployment mapping configration in the json.</li>
  <li>PR merge gate with Pre-validation and code review.</li>
  <li>Deployment gate - approver before deployment goes ahead</li>
  <li>Deploy - ADO stages and steps and tools.</li>
  <li>Post Deploy - master data import</li>
</ul>

<h2 id="evolution">Evolution</h2>
<h3 id="manually-export-solutions-in-a-folder">Manually export solutions in a folder</h3>
<p>Back in the bad old days, the packaging are handled manually, meaning the dev team will manually export solutions and put them in a folder in the source repository. Later the CICD pipeline will deploy the solutions from the repo. Optionally, you can provide CICD pipeline with powershell scripts to unpack the zip solution and inject the built plugin dlls and re-package.</p>

<h3 id="code-based-package">Code Based Package</h3>
<p>We moved away from committing exported solution .zip files. Instead we use PowerShell with the pac CLI to unpack solutions and commit only the unpacked XML (solution definition) to source control. This gives us:</p>
<ul>
  <li>Meaningful diffs (component level instead of opaque binaries)</li>
  <li>Better traceability (who changed which attribute/artifact)</li>
  <li>Easier observability and code review</li>
</ul>

<p>Discipline is critical. The unpacked solution definition in git must always mirror the originating environment. When drift appears, developers are tempted to hand‑edit XML to “force” a change into the next environment—slow, brittle, and error‑prone.</p>

<p>Repository &amp; branching: all squads contribute to the same repository and share a single release/trunk branch (with short‑lived feature branches). We previously experimented with letting squads keep divergent copies of the same solution for “parallel development” and then hand‑merge XML. Outcome: frequent pipeline failures, sprawling merge conflicts across many XML files, and elongated release cycles.</p>

<p>Lessons learned:</p>
<ol>
  <li>Don’t manually merge unpacked solution XML. Treat the environment as source of truth; always re‑export/unpack/overwrite instead of editing.</li>
  <li>Maintain one authoritative branch lineage per solution; avoid long‑running parallel variants.</li>
  <li>Automate sync validation (pre‑PR or pipeline) to catch drift early.</li>
  <li>Flow changes forward only (dev → test → prod). Avoid reverse edits by hacking XML.</li>
  <li>Make pack/unpack scripts idempotent and run them locally and in CI to eliminate divergence.</li>
</ol>

<p>Core principle: no code‑level merging of solution definitions; solution evolution moves one way toward higher environments.</p>

<h3 id="ppdo">PPDO</h3>
<p><a href="https://github.com/microsoft/powerplatform-build-tools">Power Platform Devops</a> is community initiated toolkit which helps with packing, unpacking, codegen, configuration data export and other packing related tasks.</p>

<h3 id="alm-accelerator">ALM accelerator</h3>
<p><a href="https://learn.microsoft.com/en-us/power-platform/guidance/alm-accelerator/overview">ALM Accelerator for Power Platform</a> is microsoft initiated toolkit which helps with packing, unpacking, deployment mapping, pipeline yaml templates and etc.</p>

<h3 id="power-platform-native-git-integration">Power Platform native git integration</h3>
<p><a href="https://learn.microsoft.com/en-us/power-platform/alm/git-integration/overview">Git integration in Power Platform</a> is the Power Platform native support for solution sync and deployment.</p>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="Power Platform" /><category term="ALM" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Why CI/CD and ALM? We need CI/CD and ALM in Power Platform to move from ad‑hoc manual exports/imports to predictable, governed delivery.]]></summary></entry><entry><title type="html">PowerFx Data Query Grouping</title><link href="https://blog.kelvinbytes.com/2025-06-27-powerfx-data-query-group.html" rel="alternate" type="text/html" title="PowerFx Data Query Grouping" /><published>2025-06-27T00:00:00+00:00</published><updated>2025-06-27T00:00:00+00:00</updated><id>https://blog.kelvinbytes.com/powerfx-data-query-group</id><content type="html" xml:base="https://blog.kelvinbytes.com/2025-06-27-powerfx-data-query-group.html"><![CDATA[<h3 id="grouping-example">Grouping Example</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Does not work
ClearCollect(
    colEmployeesGrouped,
    ForAll(
        Distinct(colEmployees, Department),
        With(
            {
                currentDept: ThisRecord.Value,
                matchingRecords: Filter(colEmployees, Department = ThisRecord.Department)
            },
            {
                currentDeptTest: currentDept,
                matchingRecordsTemp: matchingRecords,
                matchingRecordsCount: CountRows(matchingRecords)
             }
        )
    )
);
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Works
ClearCollect(
    colEmployeesGrouped,
    ForAll(
        Distinct(colEmployees, Department),
        With(
            {
                currentDept: ThisRecord.Value
            },
            {
                Department: First(Filter(colEmployees, Department = currentDept)).Department,
                EmployeeNames: Concat(Filter(colEmployees, Department = currentDept), FullName, "; "),
                JobTitles: Concat(Filter(colEmployees, Department = currentDept), JobTitle, "; ")
             }
        )
    )
);
</code></pre></div></div>

<p>The top code snippet doesn’t work is because ThisRecord.Department doesn’t exist. It will not throw any errors but you will get empty collection rather than expected data.</p>

<p>The problematic code</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Filter(colEmployees, Department = ThisRecord.Department)
</code></pre></div></div>

<p>To fix it, use ThisRecord.Value instead.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Filter(colEmployees, Department = ThisRecord.Value)
</code></pre></div></div>

<p>So, the takeaway from this is when looping with ForAll, the current record context is super important, i.e. This Record.</p>

<h3 id="thisrecord-example">ThisRecord example</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ClearCollect(
    colPartiesGrouped,
    ForAll(
        Distinct(colCaseParties, CasePartyOriginCombined),
        With(
            {
                currentOrigin: ThisRecord.Value,
                matchingRecords: Filter(colCaseParties, CasePartyOriginCombined = ThisRecord.Value)
            },
            {
                CasePartyOriginCombined: First(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin)).CasePartyNameCombined,
                CasePartyRoleNames: Concat(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin), CasePartyRoleName, "; "),
                CasePartyLawyerNames: Concat(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin), CasePartyLawyerName, "; ")
             }
        )
    )
);
</code></pre></div></div>

<p>The value of ThisRecord changes depend on the scope.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>currentOrigin: ThisRecord.Value 
</code></pre></div></div>
<p>The scope of ThisRecord is Distinct(colCaseParties, CasePartyOriginCombined)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>matchingRecords: Filter(colCaseParties, CasePartyOriginCombined = ThisRecord.Value)
</code></pre></div></div>
<p>The scope of ThisRecord is colCaseParties</p>

<h3 id="nested-with">Nested With</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ClearCollect(
    colPartiesGrouped,
    ForAll(
        Distinct(colCaseParties, CasePartyOriginCombined),
        With(
            {
                currentOrigin: ThisRecord.Value,
                matchingRecords: Filter(colCaseParties, CasePartyOriginCombined = currentOrigin)
            },
            {
                CasePartyOriginCombined: First(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin)).CasePartyNameCombined,
                CasePartyRoleNames: Concat(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin), CasePartyRoleName, "; "),
                CasePartyLawyerNames: Concat(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin), CasePartyLawyerName, "; ")
             }
        )
    )
);
</code></pre></div></div>
<p>The above code will not work because of currentOrigin is not recognized inside Filter(colCaseParties, CasePartyOriginCombined = currentOrigin).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ClearCollect(
    colPartiesGrouped,
    ForAll(
        Distinct(colCaseParties, CasePartyOriginCombined),
        With(
            {
                currentOrigin: ThisRecord.Value
            },
            With(
                {
                    matchingRecords: Filter(colCaseParties, CasePartyOriginCombined = currentOrigin)
                },
                {
                    CasePartyOriginCombined: First(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin)).CasePartyNameCombined,
                    CasePartyRoleNames: Concat(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin), CasePartyRoleName, "; "),
                    CasePartyLawyerNames: Concat(Filter(colCaseParties, CasePartyOriginCombined = currentOrigin), CasePartyLawyerName, "; ")
                }
            )
        )
    )
);
</code></pre></div></div>
<p>The above code works</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ClearCollect(
    colPartiesGrouped,
    ForAll(
        Distinct(colCaseParties, CasePartyOriginCombined),
        With(
            {
                currentOrigin: Value,
                matchingRecords: Filter(colCaseParties, Value = ThisRecord.CasePartyOriginCombined)
            },

                {
                    CasePartyOriginCombined: First(matchingRecords).CasePartyNameCombined,
                    CasePartyRoleNames: Concat(matchingRecords, CasePartyRoleName, "; "),
                    CasePartyLawyerNames: Concat(matchingRecords, CasePartyLawyerName, "; ")
                }
            
        )
    )
);
</code></pre></div></div>
<p>Works</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ClearCollect(
    colPartiesGrouped,
    ForAll(
        Distinct(colCaseParties, CasePartyOriginCombined) As DistinctCaseParties,
        With(
            {
                currentOrigin: DistinctCaseParties[@Value],
                matchingRecords: Filter(colCaseParties, ThisRecord.CasePartyOriginCombined = DistinctCaseParties[@Value])
            },

                {
                    CasePartyOriginCombined: First(matchingRecords).CasePartyNameCombined,
                    CasePartyRoleNames: Concat(matchingRecords, CasePartyRoleName, "; "),
                    CasePartyLawyerNames: Concat(matchingRecords, CasePartyLawyerName, "; ")
                }
            
        )
    )
);
</code></pre></div></div>
<p>Works with a disambiguation operator.</p>

<p>Record scope is for each record of a loop function like Filter.</p>

<p>Value is a special field for single columns data table sources.</p>

<p>ThisRecord symbol is for the most immidiate data context.</p>

<h2 id="references">References</h2>
<ul>
  <li><a href="https://learn.microsoft.com/en-us/power-apps/maker/canvas-apps/working-with-tables#record-scope"></a></li>
</ul>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="AI" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Grouping Example // Does not work ClearCollect( colEmployeesGrouped, ForAll( Distinct(colEmployees, Department), With( { currentDept: ThisRecord.Value, matchingRecords: Filter(colEmployees, Department = ThisRecord.Department) }, { currentDeptTest: currentDept, matchingRecordsTemp: matchingRecords, matchingRecordsCount: CountRows(matchingRecords) } ) ) );]]></summary></entry><entry><title type="html">PCF Debug</title><link href="https://blog.kelvinbytes.com/2025-04-03-pcf-debug.html" rel="alternate" type="text/html" title="PCF Debug" /><published>2025-04-03T00:00:00+00:00</published><updated>2025-04-03T00:00:00+00:00</updated><id>https://blog.kelvinbytes.com/pcf-debug</id><content type="html" xml:base="https://blog.kelvinbytes.com/2025-04-03-pcf-debug.html"><![CDATA[<h2 id="debug-method">Debug method</h2>
<h3 id="debugging-with-local-testing-harness">Debugging with local testing harness</h3>
<p>npm start or npm start watch</p>

<p>You can debug the PCF code component locally but the problem is it will not load the real data therefore you cannot test it with real data.</p>

<h3 id="debugging-after-deployment">Debugging after deployment</h3>
<p>After deploy, you can use browser’s developer tools for debugging.</p>

<h3 id="debugging-without-deployment">Debugging without deployment</h3>
<p>It definitely makes the dev/test feedback loop a lot faster with the benefit of testing it with real data.</p>

<h2 id="requestly">Requestly</h2>
<p>Fiddler didn’t work for me either. My browser kept saying there was a suspicious network activity when Fiddler was turned on.</p>

<p>So, I tried the other method (Requestly) based on the following msdoc articles - <a href="https://learn.microsoft.com/en-us/power-apps/developer/component-framework/debugging-custom-controls#using-requestly">Debug code components - Power Apps</a></p>

<p>Just at couple of things to note:
You need to build your PCF control in production mode in order to get the bundle.js as build output
I am using Requestly Chrome add-on for redirect network traffic to my local PCF component bundle.js. The standalone Requestly app may also work but I struggled a little bit and gave up.</p>

<h2 id="references">References</h2>
<ul>
  <li>https://learn.microsoft.com/en-us/power-apps/developer/component-framework/debugging-custom-controls#using-requestly</li>
</ul>

<hr />
<p>Okay, here’s a revised and enriched version of your blog post draft, incorporating details from the Microsoft documentation while maintaining your personal experience and perspective.</p>

<hr />

<h2 id="debugging-your-power-apps-pcf-controls-from-local-harness-to-real-data">Debugging Your Power Apps PCF Controls: From Local Harness to Real Data</h2>

<p>Developing Power Apps Component Framework (PCF) controls unlocks powerful UI customizations, but debugging them effectively can sometimes feel tricky. Fortunately, we have several methods available, ranging from quick local checks to debugging against live Dataverse data without constant redeployments. Let’s explore the main approaches.</p>

<h3 id="1-the-local-test-harness-npm-start-watch">1. The Local Test Harness (<code class="language-plaintext highlighter-rouge">npm start watch</code>)</h3>

<p>When you first start building your component logic, the quickest way to see visual changes is using the local test harness. Running <code class="language-plaintext highlighter-rouge">npm start</code> or <code class="language-plaintext highlighter-rouge">npm start watch</code> in your project directory builds your component and pops it open in a local web page.</p>

<ul>
  <li><strong>Pros:</strong>
    <ul>
      <li><strong>Fast Feedback Loop:</strong> Changes to your <code class="language-plaintext highlighter-rouge">index.ts</code>, imported modules, CSS, or resource files trigger an automatic reload in the browser. Great for rapid UI tweaks and basic logic testing.</li>
      <li><strong>Form Factor Testing:</strong> Easily switch between Web, Tablet, and Phone form factors to test responsiveness.</li>
      <li><strong>Basic Input Simulation:</strong> Allows you to provide mock data for properties defined in your manifest. For datasets, you can even load data from a CSV file.</li>
    </ul>
  </li>
  <li><strong>Cons:</strong>
    <ul>
      <li><strong>No Real Data Context:</strong> This is the biggest limitation. The harness runs entirely locally. You <strong>cannot</strong> interact with actual Dataverse data.</li>
      <li><strong>Limited API Support:</strong> Features like <code class="language-plaintext highlighter-rouge">context.webAPI</code>, dataset paging, sorting, filtering, complex lookups/choices, or navigation APIs won’t work. They’ll typically throw errors because the harness doesn’t provide the necessary Power Apps runtime context.</li>
      <li><strong>Property Updates:</strong> The <code class="language-plaintext highlighter-rouge">updatedProperties</code> array in <code class="language-plaintext highlighter-rouge">updateView</code> isn’t populated correctly when you change inputs in the harness.</li>
    </ul>
  </li>
</ul>

<p><strong>Verdict:</strong> Excellent for initial development and UI layout, but insufficient for testing logic that interacts with Dataverse.</p>

<h3 id="2-browser-developer-tools-your-best-friend-everywhere">2. Browser Developer Tools (Your Best Friend Everywhere)</h3>

<p>No matter which debugging method you use, your browser’s built-in developer tools (F12 or Ctrl+Shift+I) are essential.</p>

<ul>
  <li><strong>Key Uses:</strong>
    <ul>
      <li><strong>Inspecting the DOM:</strong> Use the ‘Elements’ tab to see the HTML structure your component generates and debug CSS styling issues.</li>
      <li><strong>Debugging JavaScript:</strong> Use the ‘Sources’ tab. Thanks to source maps (generated during development builds via webpack), you can usually find your original TypeScript (<code class="language-plaintext highlighter-rouge">.ts</code>) file (often under <code class="language-plaintext highlighter-rouge">webpack://</code>), set breakpoints directly in your TS code, step through execution, and inspect variable values.</li>
      <li><strong>Console:</strong> Check for errors and log output using <code class="language-plaintext highlighter-rouge">console.log()</code>.</li>
      <li><strong>Network:</strong> Analyze API calls (though less relevant for the local harness).</li>
    </ul>
  </li>
  <li><strong>Tip:</strong> In Power Apps Studio, the F12 key might be mapped to something else. Use <strong>Ctrl+Shift+I</strong> to reliably open the developer tools.</li>
</ul>

<h3 id="3-debugging-against-real-data-without-constant-redeployment-the-redirect-method">3. Debugging Against Real Data Without Constant Redeployment (The Redirect Method)</h3>

<p>This is where things get powerful. You need to test your component with real data and interact with Dataverse APIs, but constantly rebuilding, deploying (<code class="language-plaintext highlighter-rouge">pac pcf push</code>), and publishing after every small code change is incredibly slow.</p>

<p>The solution is to deploy your component <em>once</em> to your Dataverse environment, and then use a tool like <strong>Fiddler</strong> or <strong>Requestly</strong> to intercept the browser’s request for your component’s code and redirect it to your <em>local machine</em> where you’re running <code class="language-plaintext highlighter-rouge">npm start watch</code>.</p>

<ul>
  <li><strong>The Concept:</strong>
    <ol>
      <li><strong>Deploy Once:</strong> Build and deploy your PCF control to your target Dataverse environment. The Microsoft documentation recommends deploying a <em>production</em> build (<code class="language-plaintext highlighter-rouge">PcfBuildMode</code> set to <code class="language-plaintext highlighter-rouge">production</code> in <code class="language-plaintext highlighter-rouge">.pcfproj</code> or using <code class="language-plaintext highlighter-rouge">npm run build -- --buildMode production</code> before <code class="language-plaintext highlighter-rouge">pac pcf push</code>). This ensures the component structure matches what the platform expects and avoids potential size limits of development builds.</li>
      <li><strong>Run Locally:</strong> Start your local development server using <code class="language-plaintext highlighter-rouge">npm start watch</code>. This continuously rebuilds your component locally as you make changes.</li>
      <li><strong>Intercept &amp; Redirect:</strong> Use Fiddler or Requestly to tell your browser: “When you try to load the <code class="language-plaintext highlighter-rouge">bundle.js</code> (and related CSS/HTML) for this specific PCF control from Dataverse, load it from my local development server instead.”</li>
      <li><strong>Debug:</strong> Open Power Apps (model-driven or canvas) where your component is configured. Your browser now loads the code directly from your local machine. Set breakpoints in your TS code using the browser dev tools, interact with your app, and debug against real data! Make code changes, save, let <code class="language-plaintext highlighter-rouge">npm start watch</code> rebuild, refresh the Power Apps browser page (a hard refresh Ctrl+Shift+R might be needed initially), and test the new code instantly.</li>
    </ol>
  </li>
  <li>
    <p><strong>Tools for Redirection:</strong></p>

    <ul>
      <li><strong>Fiddler:</strong> A powerful web debugging proxy. The MS docs detail setting up its AutoResponder feature with REGEX rules to match requests for your component’s files (<code class="language-plaintext highlighter-rouge">bundle.js</code>, CSS, etc.) and map them to your local output folder (e.g., <code class="language-plaintext highlighter-rouge">YourProject\out\controls\YourControlName</code>).
        <ul>
          <li><em>My Experience:</em> As noted in my draft, I personally ran into issues where my browser flagged Fiddler’s HTTPS decryption as suspicious network activity, making it unusable. Setup can also involve certificate installation and specific rule configurations.</li>
        </ul>
      </li>
      <li><strong>Requestly:</strong> A browser extension (and standalone app) focused on modifying network requests. This often feels simpler for this specific use case.
        <ul>
          <li><em>My Experience:</em> This worked much better for me! I used the <strong>Requestly Chrome add-on</strong>. The standalone app might work too, but the extension felt more straightforward.</li>
          <li><strong>Simplified Requestly Setup:</strong>
            <ol>
              <li><strong>Host Locally:</strong> Ensure your local build output (e.g., <code class="language-plaintext highlighter-rouge">C:\PCFProject\out\controls\MyControl</code>) is accessible via a local web server. Enabling IIS on Windows and setting up a simple website pointing to this folder on a specific port (e.g., <code class="language-plaintext highlighter-rouge">http://localhost:7777</code>) is one way described in the docs.</li>
              <li><strong>Install Requestly:</strong> Add the extension to your browser.</li>
              <li><strong>Create Rule:</strong> Create a “Replace Host” or “Redirect Request” rule.
                <ul>
                  <li><strong>Source Condition:</strong> The URL should contain a pattern unique to your deployed component, like <code class="language-plaintext highlighter-rouge">[YourOrg].crm.dynamics.com/WebResources/your_namespace.your_control_name</code> (the exact pattern might vary slightly).</li>
                  <li><strong>Destination:</strong> Redirect to your local server address pointing to the <em>specific file</em> being requested, often the <code class="language-plaintext highlighter-rouge">bundle.js</code>. You might need a rule like: Replace <code class="language-plaintext highlighter-rouge">https://[YourOrgUrl]/.../WebResources/your_namespace.your_control_name/bundle.js</code> with <code class="language-plaintext highlighter-rouge">http://localhost:7777/bundle.js</code>. You might need separate rules or a more flexible rule for CSS/other resources if applicable.</li>
                </ul>
              </li>
              <li><strong>Activate &amp; Refresh:</strong> Enable the rule in Requestly. Clear your browser cache and perform a hard refresh (Ctrl+Shift+R) on the Power App page the first time. Subsequent code changes should only require a normal refresh.</li>
            </ol>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p><strong>Key Benefit:</strong> This redirect method provides the <strong>best of both worlds</strong>: the rapid feedback loop of local development combined with the full context and real data of a live Dataverse environment.</p>

<h3 id="final-tips">Final Tips</h3>

<ul>
  <li><strong>Source Maps:</strong> Ensure source maps are generated in your local development build (<code class="language-plaintext highlighter-rouge">npm start watch</code> does this by default). They are crucial for debugging your original TypeScript in the browser’s developer tools.</li>
  <li><strong>ES5 vs ES6:</strong> By default, PCF components target ES5 JavaScript for broader browser compatibility. If you only need to support modern browsers, changing the <code class="language-plaintext highlighter-rouge">target</code> in <code class="language-plaintext highlighter-rouge">tsconfig.json</code> to <code class="language-plaintext highlighter-rouge">ES6</code> can sometimes lead to cleaner transpiled code and slightly better debugging experiences, as ES6 features like classes map more directly to TypeScript. Remember to switch back to ES5 before creating your final production build if needed.</li>
</ul>

<p>By understanding these different methods, you can choose the most efficient approach for debugging your PCF controls at each stage of development. While the local harness is great for quick UI checks, mastering the redirect technique with Requestly or Fiddler is key for efficiently tackling complex issues involving real data.</p>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="AI" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Debug method Debugging with local testing harness npm start or npm start watch]]></summary></entry><entry><title type="html">navigation open side panel</title><link href="https://blog.kelvinbytes.com/2025-04-03-ribbon-navigation.html" rel="alternate" type="text/html" title="navigation open side panel" /><published>2025-04-03T00:00:00+00:00</published><updated>2025-04-03T00:00:00+00:00</updated><id>https://blog.kelvinbytes.com/ribbon-navigation</id><content type="html" xml:base="https://blog.kelvinbytes.com/2025-04-03-ribbon-navigation.html"><![CDATA[<p>Open the far side panel</p>

<h2 id="datetime">Datetime</h2>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">date</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="dl">"</span><span class="s2">2025-03-31T20:00:00Z</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">date</span><span class="p">.</span><span class="nx">toString</span><span class="p">());</span> <span class="c1">// Outputs: "Tue Apr 01 2025 09:00:00 GMT+1300 (New Zealand Daylight Time)"</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">date</span><span class="p">.</span><span class="nx">toISOString</span><span class="p">());</span> <span class="c1">// Outputs: "2025-03-31T20:00:00.000Z"</span>
</code></pre></div></div>

<p>Without the suffix Z, the date time string is considered in local time zone. If it is actually in UTC, you will have an time zone offset issue.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">mydate</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="dl">"</span><span class="s2">2025-03-31</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">mydate</span><span class="p">.</span><span class="nx">toString</span><span class="p">());</span> <span class="c1">// Outputs: "Mon Mar 31 2025 00:00:00 GMT+1300 (New Zealand Daylight Time)"</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">mydate</span><span class="p">.</span><span class="nx">toISOString</span><span class="p">());</span> <span class="c1">// Outputs: "2025-03-30T11:00:00.000Z"</span>
</code></pre></div></div>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="AI" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Open the far side panel]]></summary></entry><entry><title type="html">navigation open side panel</title><link href="https://blog.kelvinbytes.com/2025-04-03-fakexrmeasy.html" rel="alternate" type="text/html" title="navigation open side panel" /><published>2025-04-03T00:00:00+00:00</published><updated>2025-04-03T00:00:00+00:00</updated><id>https://blog.kelvinbytes.com/fakexrmeasy</id><content type="html" xml:base="https://blog.kelvinbytes.com/2025-04-03-fakexrmeasy.html"><![CDATA[<h2 id="background">Background</h2>
<p>FakeXrmEasy made unit tests D365 really easy.</p>

<p>Unit testing CRUD operations is well proven and on the beaten path, however, the framework still have gaps in implemented fake message executors. For example, if the function you are testing contains QueryExpressionToFetchXmlRequest.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FakeXrmEasy.PullRequestException: Exception: The organization request type 'Microsoft.Crm.Sdk.Messages.QueryExpressionToFetchXmlRequest' is not yet supported...
</code></pre></div></div>

<h2 id="soution">Soution</h2>
<p>Implement a FakeMessageExecutor</p>

<h3 id="futher-limitation-and-a-workaround">Futher limitation and a workaround</h3>
<p>In the QueryExpressionToFetchXmlRequest instance, the FetchXml property of the QueryExpressionToFetchXmlResponse class is readonly.</p>

<p>So, let’s look into the matter. The QueryExpressionToFetchXmlResponse class inherits from the fundamental Microsoft.Xrm.Sdk.OrganizationResponse class. This base class provides a standardized structure for all responses returned from messages executed via the IOrganizationService. A key component of OrganizationResponse is the Results property.  </p>

<p>The Results property is of type ParameterCollection. This collection functions much like a dictionary, mapping string keys to object values. It serves as the standard mechanism through which output parameters from any organization service message are communicated back to the calling code.</p>

<p>So the solution is rather than populate the FetchXml property, populate the Results collection in the underlining class.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">QueryExpressionToFetchXmlResponse</span><span class="p">();</span>
<span class="n">response</span><span class="p">.</span><span class="n">Results</span><span class="p">[</span><span class="s">"FetchXml"</span><span class="p">]</span> <span class="p">=</span> <span class="n">sb</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="net-library-requirements">.Net library Requirements</h3>
<p>Add System.Runtime.CompilerServices.Unsafe nuget package to your test project.</p>

<h3 id="references">References</h3>
<ul>
  <li>http://www.bwmodular.org/blog/mocking-unimplemented-organisation-requests-in-fakexrmeasy</li>
  <li>[FakeXrmEasy v2 and v3 Custom APIs]https://dynamicsvalue.github.io/fake-xrm-easy-docs/quickstart/messages/custom-apis/</li>
</ul>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="AI" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Background FakeXrmEasy made unit tests D365 really easy.]]></summary></entry><entry><title type="html">navigation open side panel</title><link href="https://blog.kelvinbytes.com/2025-04-03-pcf.html" rel="alternate" type="text/html" title="navigation open side panel" /><published>2025-04-03T00:00:00+00:00</published><updated>2025-04-03T00:00:00+00:00</updated><id>https://blog.kelvinbytes.com/pcf</id><content type="html" xml:base="https://blog.kelvinbytes.com/2025-04-03-pcf.html"><![CDATA[<h2 id="how-to-get-input-and-output-to-and-from-pcf">How to get input and output to and from PCF</h2>

<p>entry point input: context</p>

<p>output has two parts: the notification party and get output part</p>

<p>component property bags</p>

<p>service layer: json parsing</p>

<p>date time type</p>

<hr />
<p>UserSettings &gt; getTimeZoneOffsetMinutes
https://learn.microsoft.com/en-us/power-apps/developer/component-framework/reference/usersettings/gettimezoneoffsetminutes</p>

<p>For format date time if we use format string we need moment
If we use Locale string we need resx</p>

<p>Utility &gt;  Lookup property
https://dianabirkelbach.wordpress.com/2021/06/19/lookup-pcf-lets-dive-deeper/</p>

<h2 id="notification-bar">Notification bar</h2>
<h3 id="ui-">UI &gt;</h3>
<p>MessageBarType</p>

<h3 id="scenario-title-and-details-message">Scenario: Title and details message</h3>
<p>https://developer.microsoft.com/en-us/fluentui#/controls/web/messagebar</p>

<h3 id="scenario-dismiss">Scenario: Dismiss</h3>
<p>onDismiss</p>

<h3 id="scenario-multiple-notifications">Scenario: Multiple notifications</h3>
<p>NotificationMessage[] then map to MessageBar JSX</p>

<h3 id="scenario-timeout">Scenario: Timeout</h3>

<h3 id="pagination">Pagination:</h3>
<p>https://markcarrington.dev/2021/02/23/msdyn365-internals-paging-gotchas/?utm_source=FetchXMLBuilder&amp;utm_medium=XrmToolBox#multiple_linked_entities</p>

<p>### 
Browser event</p>

<p>In the Power Apps Component Framework (PCF), browser events are primarily managed through the notifyOutputChanged method and custom events defined in the component manifest.</p>

<p>It works better with canvas app but one issue I had is it triggers the whole PCF to refersh which leads it to loose all its states…</p>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="AI" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[How to get input and output to and from PCF]]></summary></entry><entry><title type="html">PCF cheat sheat</title><link href="https://blog.kelvinbytes.com/2025-04-03-pcf-cheatsheet.html" rel="alternate" type="text/html" title="PCF cheat sheat" /><published>2025-04-03T00:00:00+00:00</published><updated>2025-04-03T00:00:00+00:00</updated><id>https://blog.kelvinbytes.com/pcf-cheatsheet</id><content type="html" xml:base="https://blog.kelvinbytes.com/2025-04-03-pcf-cheatsheet.html"><![CDATA[<h2 id="install-the-pac-cli-tool">Install the PAC CLI Tool</h2>
<p><a href="https://learn.microsoft.com/en-us/power-platform/developer/howto/install-vs-code-extension#enable-pac-cli-in-command-prompt-cmd-and-powershell-terminals-for-windows">Install the PAC CLI tool for both the VS Code and Command Prompt for Windows</a></p>

<p><a href="https://learn.microsoft.com/en-us/power-platform/developer/howto/install-cli-msi">Install Power Platform CLI using Windows MSI</a></p>

<p>##
Create a PCF solution folder</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mkdir</span><span class="w"> </span><span class="nx">FileExplorerV9</span><span class="w">
</span></code></pre></div></div>

<p>Initialize the PCF solution structure</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pac</span><span class="w"> </span><span class="nx">pcf</span><span class="w"> </span><span class="nx">init</span><span class="w"> </span><span class="nt">-ns</span><span class="w"> </span><span class="nx">Kys.CustomControl.FileExplorer</span><span class="w"> </span><span class="nt">-n</span><span class="w"> </span><span class="nx">src</span><span class="w"> </span><span class="nt">--template</span><span class="w"> </span><span class="nx">dataset</span><span class="w"> </span><span class="nt">-fw</span><span class="w"> </span><span class="nx">react</span><span class="w"> </span><span class="nt">-npm</span><span class="w">
</span></code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pac</span><span class="w"> </span><span class="nx">pcf</span><span class="w"> </span><span class="nx">init</span><span class="w"> </span><span class="nt">--namespace</span><span class="w"> </span><span class="nx">SampleNamespace</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nx">LinearInputControl</span><span class="w"> </span><span class="nt">--template</span><span class="w"> </span><span class="nx">field</span><span class="w"> </span><span class="nt">--run-npm-install</span><span class="w">
</span></code></pre></div></div>

<p>Initialize the Dataverse solution project</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pac</span><span class="w"> </span><span class="nx">solution</span><span class="w"> </span><span class="nx">init</span><span class="w"> </span><span class="nt">--publisher-name</span><span class="w"> </span><span class="nx">KelvinBytes</span><span class="w"> </span><span class="nt">--publisher-prefix</span><span class="w"> </span><span class="nx">kys</span><span class="w">
</span></code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pac</span><span class="w"> </span><span class="nx">solution</span><span class="w"> </span><span class="nx">add-reference</span><span class="w"> </span><span class="nt">--path</span><span class="w"> </span><span class="nx">FileExplorer</span><span class="w">
</span></code></pre></div></div>]]></content><author><name>Kelvin Shen</name></author><category term="Technology" /><category term="AI" /><category term="Twitter" /><category term="Facebook" /><category term="LinkedIn" /><summary type="html"><![CDATA[Install the PAC CLI Tool Install the PAC CLI tool for both the VS Code and Command Prompt for Windows]]></summary></entry></feed>